¤ œ{"files":{"browser":{"files":{"api":{"files":{"app.js":{"size":2551,"offset":"0"},"auto-updater":{"files":{"auto-updater-native.js":{"size":289,"offset":"2725"},"auto-updater-win.js":{"size":1690,"offset":"3014"},"squirrel-update-win.js":{"size":3444,"offset":"4704"}}},"auto-updater.js":{"size":174,"offset":"2551"},"browser-view.js":{"size":391,"offset":"8148"},"browser-window.js":{"size":6574,"offset":"8539"},"content-tracing.js":{"size":56,"offset":"15113"},"dialog.js":{"size":8495,"offset":"15169"},"exports":{"files":{"electron.js":{"size":409,"offset":"23664"}}},"global-shortcut.js":{"size":71,"offset":"24073"},"ipc-main.js":{"size":514,"offset":"24144"},"menu-item-roles.js":{"size":6057,"offset":"24658"},"menu-item.js":{"size":2480,"offset":"30715"},"menu.js":{"size":8661,"offset":"33195"},"module-list.js":{"size":1126,"offset":"41856"},"navigation-controller.js":{"size":5272,"offset":"42982"},"net.js":{"size":9805,"offset":"48254"},"notification.js":{"size":255,"offset":"58059"},"power-monitor.js":{"size":285,"offset":"58314"},"power-save-blocker.js":{"size":76,"offset":"58599"},"protocol.js":{"size":775,"offset":"58675"},"screen.js":{"size":242,"offset":"59450"},"session.js":{"size":1022,"offset":"59692"},"system-preferences.js":{"size":320,"offset":"60714"},"touch-bar.js":{"size":9378,"offset":"61034"},"tray.js":{"size":170,"offset":"70412"},"web-contents.js":{"size":9336,"offset":"70582"}}},"chrome-extension.js":{"size":13593,"offset":"79918"},"desktop-capturer.js":{"size":2271,"offset":"93511"},"guest-view-manager.js":{"size":10956,"offset":"95782"},"guest-window-manager.js":{"size":12258,"offset":"106738"},"init.js":{"size":5052,"offset":"118996"},"objects-registry.js":{"size":2837,"offset":"124048"},"rpc-server.js":{"size":15222,"offset":"126885"}}},"common":{"files":{"api":{"files":{"callbacks-registry.js":{"size":1394,"offset":"142107"},"clipboard.js":{"size":613,"offset":"143501"},"crash-reporter.js":{"size":3782,"offset":"144114"},"deprecate.js":{"size":2905,"offset":"147896"},"deprecations.js":{"size":231,"offset":"150801"},"exports":{"files":{"electron.js":{"size":374,"offset":"151032"}}},"is-promise.js":{"size":320,"offset":"151406"},"module-list.js":{"size":558,"offset":"151726"},"native-image.js":{"size":53,"offset":"152284"},"shell.js":{"size":46,"offset":"152337"}}},"atom-binding-setup.js":{"size":342,"offset":"152383"},"init.js":{"size":2117,"offset":"152725"},"parse-features-string.js":{"size":712,"offset":"154842"},"reset-search-paths.js":{"size":1285,"offset":"155554"}}},"renderer":{"files":{"api":{"files":{"desktop-capturer.js":{"size":1248,"offset":"156839"},"exports":{"files":{"electron.js":{"size":336,"offset":"158087"}}},"ipc-renderer.js":{"size":1379,"offset":"158423"},"module-list.js":{"size":289,"offset":"159802"},"remote.js":{"size":10355,"offset":"160091"},"screen.js":{"size":51,"offset":"170446"},"web-frame.js":{"size":357,"offset":"170497"}}},"chrome-api.js":{"size":5695,"offset":"170854"},"content-scripts-injector.js":{"size":3006,"offset":"176549"},"extensions":{"files":{"event.js":{"size":406,"offset":"179555"},"i18n.js":{"size":2520,"offset":"179961"},"storage.js":{"size":3338,"offset":"182481"},"web-navigation.js":{"size":508,"offset":"185819"}}},"init.js":{"size":6637,"offset":"186327"},"inspector.js":{"size":3045,"offset":"192964"},"override.js":{"size":335,"offset":"196009"},"web-view":{"files":{"guest-view-internal.js":{"size":4300,"offset":"196344"},"web-view-attributes.js":{"size":11712,"offset":"200644"},"web-view-constants.js":{"size":1326,"offset":"212356"},"web-view.js":{"size":15482,"offset":"213682"}}},"window-setup.js":{"size":6532,"offset":"229164"}}},"worker":{"files":{"init.js":{"size":1189,"offset":"235696"}}}}}'use strict' const bindings = process.atomBinding('app') const {app, App} = bindings // Only one app object permitted. module.exports = app const electron = require('electron') const {deprecate, Menu} = electron const {EventEmitter} = require('events') // App is an EventEmitter. Object.setPrototypeOf(App.prototype, EventEmitter.prototype) EventEmitter.call(app) Object.assign(app, { setApplicationMenu (menu) { return Menu.setApplicationMenu(menu) }, getApplicationMenu () { 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) } } }) if (process.platform === 'darwin') { app.dock = { bounce (type = 'informational') { return bindings.dockBounce(type) }, cancelBounce: bindings.dockCancelBounce, downloadFinished: bindings.dockDownloadFinished, setBadge: bindings.dockSetBadgeText, getBadge: bindings.dockGetBadgeText, hide: bindings.dockHide, show: bindings.dockShow, isVisible: bindings.dockIsVisible, setMenu: bindings.dockSetMenu, setIcon: bindings.dockSetIcon } } if (process.platform === 'linux') { app.launcher = { setBadgeCount: bindings.unityLauncherSetBadgeCount, getBadgeCount: bindings.unityLauncherGetBadgeCount, isCounterBadgeAvailable: bindings.unityLauncherAvailable, isUnityRunning: bindings.unityLauncherAvailable } } app.allowNTLMCredentialsForAllDomains = function (allow) { if (!process.noDeprecations) { deprecate.warn('app.allowNTLMCredentialsForAllDomains', 'session.allowNTLMCredentialsForDomains') } let domains = allow ? '*' : '' if (!this.isReady()) { this.commandLine.appendSwitch('auth-server-whitelist', domains) } else { electron.session.defaultSession.allowNTLMCredentialsForDomains(domains) } } // Routes the events to webContents. const events = ['login', 'certificate-error', 'select-client-certificate'] for (let name of events) { app.on(name, (event, webContents, ...args) => { webContents.emit(name, event, ...args) }) } // Wrappers for native classes. const {DownloadItem} = process.atomBinding('download_item') Object.setPrototypeOf(DownloadItem.prototype, EventEmitter.prototype) if (process.platform === 'win32') { module.exports = require('./auto-updater/auto-updater-win') } else { module.exports = require('./auto-updater/auto-updater-native') } const EventEmitter = require('events').EventEmitter const {autoUpdater, AutoUpdater} = process.atomBinding('auto_updater') // AutoUpdater is an EventEmitter. Object.setPrototypeOf(AutoUpdater.prototype, EventEmitter.prototype) EventEmitter.call(autoUpdater) module.exports = autoUpdater 'use strict' const {app} = require('electron') const {EventEmitter} = require('events') const squirrelUpdate = require('./squirrel-update-win') class AutoUpdater extends EventEmitter { quitAndInstall () { if (!this.updateAvailable) { return this.emitError('No update available, can\'t quit and install') } squirrelUpdate.processStart() app.quit() } getFeedURL () { return this.updateURL } setFeedURL (updateURL, headers) { this.updateURL = updateURL } checkForUpdates () { if (!this.updateURL) { return this.emitError('Update URL is not set') } if (!squirrelUpdate.supported()) { return this.emitError('Can not find Squirrel') } this.emit('checking-for-update') squirrelUpdate.checkForUpdate(this.updateURL, (error, update) => { if (error != null) { return this.emitError(error) } if (update == null) { return this.emit('update-not-available') } this.updateAvailable = true this.emit('update-available') squirrelUpdate.update(this.updateURL, (error) => { if (error != null) { return this.emitError(error) } const {releaseNotes, version} = update // Date is not available on Windows, so fake it. const date = new Date() this.emit('update-downloaded', {}, releaseNotes, version, date, this.updateURL, () => { this.quitAndInstall() }) }) }) } // Private: Emit both error object and message, this is to keep compatibility // with Old APIs. emitError (message) { this.emit('error', new Error(message), message) } } module.exports = new AutoUpdater() const fs = require('fs') const path = require('path') const spawn = require('child_process').spawn // i.e. my-app/app-0.1.13/ const appFolder = path.dirname(process.execPath) // i.e. my-app/Update.exe const updateExe = path.resolve(appFolder, '..', 'Update.exe') const exeName = path.basename(process.execPath) var spawnedArgs = [] var spawnedProcess var isSameArgs = function (args) { return (args.length === spawnedArgs.length) && args.every(function (e, i) { return e === spawnedArgs[i] }) } // Spawn a command and invoke the callback when it completes with an error // and the output from standard out. var spawnUpdate = function (args, detached, callback) { var error, errorEmitted, stderr, stdout try { // Ensure we don't spawn multiple squirrel processes // Process spawned, same args: Attach events to alread running process // Process spawned, different args: Return with error // No process spawned: Spawn new process if (spawnedProcess && !isSameArgs(args)) { return callback('AutoUpdater process with arguments ' + args + ' is already running') } else if (!spawnedProcess) { spawnedProcess = spawn(updateExe, args, { detached: detached }) spawnedArgs = args || [] } } catch (error1) { error = error1 // Shouldn't happen, but still guard it. process.nextTick(function () { return callback(error) }) return } stdout = '' stderr = '' spawnedProcess.stdout.on('data', function (data) { stdout += data }) spawnedProcess.stderr.on('data', function (data) { stderr += data }) errorEmitted = false spawnedProcess.on('error', function (error) { errorEmitted = true callback(error) }) return spawnedProcess.on('exit', function (code, signal) { spawnedProcess = undefined spawnedArgs = [] // We may have already emitted an error. if (errorEmitted) { return } // Process terminated with error. if (code !== 0) { return callback('Command failed: ' + (signal != null ? signal : code) + '\n' + stderr) } // Success. callback(null, stdout) }) } // Start an instance of the installed app. exports.processStart = function () { return spawnUpdate(['--processStartAndWait', exeName], true, function () {}) } // Download the releases specified by the URL and write new results to stdout. exports.checkForUpdate = function (updateURL, callback) { return spawnUpdate(['--checkForUpdate', updateURL], false, function (error, stdout) { var json, ref, ref1, update if (error != null) { return callback(error) } try { // Last line of output is the JSON details about the releases json = stdout.trim().split('\n').pop() update = (ref = JSON.parse(json)) != null ? (ref1 = ref.releasesToApply) != null ? typeof ref1.pop === 'function' ? ref1.pop() : void 0 : void 0 : void 0 } catch (jsonError) { return callback('Invalid result:\n' + stdout) } return callback(null, update) }) } // Update the application to the latest remote version specified by URL. exports.update = function (updateURL, callback) { return spawnUpdate(['--update', updateURL], false, callback) } // Is the Update.exe installed with the current application? exports.supported = function () { try { fs.accessSync(updateExe, fs.R_OK) return true } catch (error) { return false } } 'use strict' const {EventEmitter} = require('events') const {BrowserView} = process.atomBinding('browser_view') Object.setPrototypeOf(BrowserView.prototype, EventEmitter.prototype) BrowserView.fromWebContents = (webContents) => { for (const view of BrowserView.getAllViews()) { if (view.webContents.equal(webContents)) return view } return null } module.exports = BrowserView 'use strict' const electron = require('electron') const {ipcMain} = electron const {EventEmitter} = require('events') const {BrowserWindow} = process.atomBinding('window') const v8Util = process.atomBinding('v8_util') Object.setPrototypeOf(BrowserWindow.prototype, EventEmitter.prototype) BrowserWindow.prototype._init = function () { // Avoid recursive require. const {app} = require('electron') // Simulate the application menu on platforms other than macOS. if (process.platform !== 'darwin') { const menu = app.getApplicationMenu() if (menu) this.setMenu(menu) } // Make new windows requested by links behave like "window.open" this.webContents.on('-new-window', (event, url, frameName, disposition, additionalFeatures, postData) => { const options = { show: true, width: 800, height: 600 } ipcMain.emit('ELECTRON_GUEST_WINDOW_MANAGER_INTERNAL_WINDOW_OPEN', event, url, 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) => { let urlFrameName = v8Util.getHiddenValue(webContents, 'url-framename') if ((disposition !== 'foreground-tab' && disposition !== 'new-window' && disposition !== 'background-tab') || !urlFrameName) { event.preventDefault() return } let {url, frameName} = urlFrameName v8Util.deleteHiddenValue(webContents, 'url-framename') const options = { show: true, x: left, y: top, width: width || 800, height: height || 600, webContents: webContents } ipcMain.emit('ELECTRON_GUEST_WINDOW_MANAGER_INTERNAL_WINDOW_OPEN', event, url, frameName, disposition, options) }) // window.resizeTo(...) // window.moveTo(...) this.webContents.on('move', (event, size) => { this.setBounds(size) }) // Hide the auto-hide menu when webContents is focused. this.webContents.on('activate', () => { if (process.platform !== 'darwin' && this.isMenuBarAutoHide() && this.isMenuBarVisible()) { this.setMenuBarVisibility(false) } }) // Change window title to page title. this.webContents.on('page-title-updated', (event, title) => { // Route the event to BrowserWindow. this.emit('page-title-updated', event, title) if (!this.isDestroyed() && !event.defaultPrevented) this.setTitle(title) }) // Sometimes the webContents doesn't get focus when window is shown, so we // have to force focusing on webContents in this case. The safest way is to // focus it when we first start to load URL, if we do it earlier it won't // have effect, if we do it later we might move focus in the page. // // Though this hack is only needed on macOS when the app is launched from // Finder, we still do it on all platforms in case of other bugs we don't // know. this.webContents.once('load-url', function () { this.focus() }) // Redirect focus/blur event to app instance too. this.on('blur', (event) => { app.emit('browser-window-blur', event, this) }) this.on('focus', (event) => { app.emit('browser-window-focus', event, this) }) // Subscribe to visibilityState changes and pass to renderer process. let isVisible = this.isVisible() && !this.isMinimized() const visibilityChanged = () => { const newState = this.isVisible() && !this.isMinimized() if (isVisible !== newState) { isVisible = newState const visibilityState = isVisible ? 'visible' : 'hidden' this.webContents.emit('-window-visibility-change', visibilityState) } } const visibilityEvents = ['show', 'hide', 'minimize', 'maximize', 'restore'] for (let event of visibilityEvents) { this.on(event, visibilityChanged) } // Notify the creation of the window. app.emit('browser-window-created', {}, this) Object.defineProperty(this, 'devToolsWebContents', { enumerable: true, configurable: false, get () { return this.webContents.devToolsWebContents } }) } BrowserWindow.getFocusedWindow = () => { for (let window of BrowserWindow.getAllWindows()) { if (window.isFocused()) return window } return null } BrowserWindow.fromWebContents = (webContents) => { for (const window of BrowserWindow.getAllWindows()) { if (window.webContents.equal(webContents)) return window } } BrowserWindow.fromBrowserView = (browserView) => { for (const window of BrowserWindow.getAllWindows()) { if (window.getBrowserView() === browserView) return window } return null } BrowserWindow.fromDevToolsWebContents = (webContents) => { for (const window of BrowserWindow.getAllWindows()) { const {devToolsWebContents} = window if (devToolsWebContents != null && devToolsWebContents.equal(webContents)) { return window } } } // Helpers. Object.assign(BrowserWindow.prototype, { loadURL (...args) { return this.webContents.loadURL(...args) }, getURL (...args) { return this.webContents.getURL() }, reload (...args) { return this.webContents.reload(...args) }, send (...args) { return this.webContents.send(...args) }, openDevTools (...args) { return this.webContents.openDevTools(...args) }, closeDevTools () { return this.webContents.closeDevTools() }, isDevToolsOpened () { return this.webContents.isDevToolsOpened() }, isDevToolsFocused () { return this.webContents.isDevToolsFocused() }, toggleDevTools () { return this.webContents.toggleDevTools() }, inspectElement (...args) { return this.webContents.inspectElement(...args) }, inspectServiceWorker () { return this.webContents.inspectServiceWorker() }, showDefinitionForSelection () { return this.webContents.showDefinitionForSelection() }, capturePage (...args) { return this.webContents.capturePage(...args) }, setTouchBar (touchBar) { electron.TouchBar._setOnWindow(touchBar, this) } }) module.exports = BrowserWindow module.exports = process.atomBinding('content_tracing') 'use strict' const {app, BrowserWindow} = require('electron') const binding = process.atomBinding('dialog') const v8Util = process.atomBinding('v8_util') const fileDialogProperties = { openFile: 1 << 0, openDirectory: 1 << 1, multiSelections: 1 << 2, createDirectory: 1 << 3, showHiddenFiles: 1 << 4, promptToCreate: 1 << 5, noResolveAliases: 1 << 6, treatPackageAsDirectory: 1 << 7 } const messageBoxTypes = ['none', 'info', 'warning', 'error', 'question'] const messageBoxOptions = { noLink: 1 << 0 } const parseArgs = function (window, options, callback, ...args) { if (window != null && window.constructor !== BrowserWindow) { // Shift. [callback, options, window] = [options, window, null] } if ((callback == null) && typeof options === 'function') { // Shift. [callback, options] = [options, null] } // Fallback to using very last argument as the callback function const lastArgument = args[args.length - 1] if ((callback == null) && typeof lastArgument === 'function') { callback = lastArgument } return [window, options, callback] } const normalizeAccessKey = (text) => { if (typeof text !== 'string') return text // macOS does not have access keys so remove single ampersands // and replace double ampersands with a single ampersand if (process.platform === 'darwin') { return text.replace(/&(&?)/g, '$1') } // Linux uses a single underscore as an access key prefix so escape // existing single underscores with a second underscore, replace double // ampersands with a single ampersand, and replace a single ampersand with // a single underscore if (process.platform === 'linux') { return text.replace(/_/g, '__').replace(/&(.?)/g, (match, after) => { if (after === '&') return after return `_${after}` }) } return text } const checkAppInitialized = function () { if (!app.isReady()) { throw new Error('dialog module can only be used after app is ready') } } module.exports = { showOpenDialog: function (...args) { checkAppInitialized() let [window, options, callback] = parseArgs(...args) if (options == null) { options = { title: 'Open', properties: ['openFile'] } } let {buttonLabel, defaultPath, filters, properties, title, message} = options if (properties == null) { properties = ['openFile'] } else if (!Array.isArray(properties)) { throw new TypeError('Properties must be an array') } let dialogProperties = 0 for (const prop in fileDialogProperties) { if (properties.includes(prop)) { dialogProperties |= fileDialogProperties[prop] } } if (title == null) { title = '' } else if (typeof title !== 'string') { throw new TypeError('Title must be a string') } if (buttonLabel == null) { buttonLabel = '' } else if (typeof buttonLabel !== 'string') { throw new TypeError('Button label must be a string') } if (defaultPath == null) { defaultPath = '' } else if (typeof defaultPath !== 'string') { throw new TypeError('Default path must be a string') } if (filters == null) { filters = [] } if (message == null) { message = '' } else if (typeof message !== 'string') { throw new TypeError('Message must be a string') } const wrappedCallback = typeof callback === 'function' ? function (success, result) { return callback(success ? result : void 0) } : null const settings = {title, buttonLabel, defaultPath, filters, message, window} settings.properties = dialogProperties return binding.showOpenDialog(settings, wrappedCallback) }, showSaveDialog: function (...args) { checkAppInitialized() let [window, options, callback] = parseArgs(...args) if (options == null) { options = { title: 'Save' } } let {buttonLabel, defaultPath, filters, title, message, nameFieldLabel, showsTagField} = options if (title == null) { title = '' } else if (typeof title !== 'string') { throw new TypeError('Title must be a string') } if (buttonLabel == null) { buttonLabel = '' } else if (typeof buttonLabel !== 'string') { throw new TypeError('Button label must be a string') } if (defaultPath == null) { defaultPath = '' } else if (typeof defaultPath !== 'string') { throw new TypeError('Default path must be a string') } if (filters == null) { filters = [] } if (message == null) { message = '' } else if (typeof message !== 'string') { throw new TypeError('Message must be a string') } if (nameFieldLabel == null) { nameFieldLabel = '' } else if (typeof nameFieldLabel !== 'string') { throw new TypeError('Name field label must be a string') } if (showsTagField == null) { showsTagField = true } const wrappedCallback = typeof callback === 'function' ? function (success, result) { return callback(success ? result : void 0) } : null const settings = {title, buttonLabel, defaultPath, filters, message, nameFieldLabel, showsTagField, window} return binding.showSaveDialog(settings, wrappedCallback) }, showMessageBox: function (...args) { checkAppInitialized() let [window, options, callback] = parseArgs(...args) if (options == null) { options = { type: 'none' } } let { buttons, cancelId, checkboxLabel, checkboxChecked, defaultId, detail, icon, message, title, type } = options if (type == null) { type = 'none' } const messageBoxType = messageBoxTypes.indexOf(type) if (messageBoxType === -1) { throw new TypeError('Invalid message box type') } if (buttons == null) { buttons = [] } else if (!Array.isArray(buttons)) { throw new TypeError('Buttons must be an array') } if (options.normalizeAccessKeys) { buttons = buttons.map(normalizeAccessKey) } if (title == null) { title = '' } else if (typeof title !== 'string') { throw new TypeError('Title must be a string') } if (message == null) { message = '' } else if (typeof message !== 'string') { throw new TypeError('Message must be a string') } if (detail == null) { detail = '' } else if (typeof detail !== 'string') { throw new TypeError('Detail must be a string') } checkboxChecked = !!checkboxChecked if (checkboxLabel == null) { checkboxLabel = '' } else if (typeof checkboxLabel !== 'string') { throw new TypeError('checkboxLabel must be a string') } if (icon == null) { icon = null } if (defaultId == null) { defaultId = -1 } // Choose a default button to get selected when dialog is cancelled. if (cancelId == null) { cancelId = 0 for (let i = 0; i < buttons.length; i++) { const text = buttons[i].toLowerCase() if (text === 'cancel' || text === 'no') { cancelId = i break } } } const flags = options.noLink ? messageBoxOptions.noLink : 0 return binding.showMessageBox(messageBoxType, buttons, defaultId, cancelId, flags, title, message, detail, checkboxLabel, checkboxChecked, icon, window, callback) }, showErrorBox: function (...args) { return binding.showErrorBox(...args) }, showCertificateTrustDialog: function (...args) { let [window, options, callback] = parseArgs(...args) if (options == null || typeof options !== 'object') { throw new TypeError('options must be an object') } let {certificate, message} = options if (certificate == null || typeof certificate !== 'object') { throw new TypeError('certificate must be an object') } if (message == null) { message = '' } else if (typeof message !== 'string') { throw new TypeError('message must be a string') } return binding.showCertificateTrustDialog(window, certificate, message, callback) } } // Mark standard asynchronous functions. v8Util.setHiddenValue(module.exports.showMessageBox, 'asynchronous', true) v8Util.setHiddenValue(module.exports.showOpenDialog, 'asynchronous', true) v8Util.setHiddenValue(module.exports.showSaveDialog, 'asynchronous', true) const common = require('../../../common/api/exports/electron') // since browser module list is also used in renderer, keep it separate. const moduleList = require('../module-list') // Import common modules. common.defineProperties(exports) for (const module of moduleList) { Object.defineProperty(exports, module.name, { enumerable: !module.private, get: () => require(`../${module.file}`) }) } module.exports = process.atomBinding('global_shortcut').globalShortcut const EventEmitter = require('events').EventEmitter const emitter = new EventEmitter() const removeAllListeners = emitter.removeAllListeners.bind(emitter) emitter.removeAllListeners = function (...args) { if (args.length === 0) { throw new Error('Removing all listeners from ipcMain will make Electron internals stop working. Please specify a event name') } removeAllListeners(...args) } // Do not throw exception when channel name is "error". emitter.on('error', () => {}) module.exports = emitter const {app} = require('electron') const roles = { about: { get label () { return process.platform === 'linux' ? 'About' : `About ${app.getName()}` } }, close: { label: process.platform === 'darwin' ? 'Close Window' : 'Close', accelerator: 'CommandOrControl+W', windowMethod: 'close' }, copy: { label: 'Copy', accelerator: 'CommandOrControl+C', webContentsMethod: 'copy' }, cut: { label: 'Cut', accelerator: 'CommandOrControl+X', webContentsMethod: 'cut' }, delete: { label: 'Delete', webContentsMethod: 'delete' }, forcereload: { label: 'Force Reload', accelerator: 'Shift+CmdOrCtrl+R', nonNativeMacOSRole: true, windowMethod: (window) => { window.webContents.reloadIgnoringCache() } }, front: { label: 'Bring All to Front' }, help: { label: 'Help' }, hide: { get label () { return `Hide ${app.getName()}` }, accelerator: 'Command+H' }, hideothers: { label: 'Hide Others', accelerator: 'Command+Alt+H' }, minimize: { label: 'Minimize', accelerator: 'CommandOrControl+M', windowMethod: 'minimize' }, paste: { label: 'Paste', accelerator: 'CommandOrControl+V', webContentsMethod: 'paste' }, pasteandmatchstyle: { label: 'Paste and Match Style', accelerator: 'Shift+CommandOrControl+V', webContentsMethod: 'pasteAndMatchStyle' }, quit: { get label () { switch (process.platform) { case 'darwin': return `Quit ${app.getName()}` case 'win32': return 'Exit' default: return 'Quit' } }, accelerator: process.platform === 'win32' ? null : 'CommandOrControl+Q', appMethod: 'quit' }, redo: { label: 'Redo', accelerator: process.platform === 'win32' ? 'Control+Y' : 'Shift+CommandOrControl+Z', webContentsMethod: 'redo' }, reload: { label: 'Reload', accelerator: 'CmdOrCtrl+R', nonNativeMacOSRole: true, windowMethod: 'reload' }, resetzoom: { label: 'Actual Size', accelerator: 'CommandOrControl+0', nonNativeMacOSRole: true, webContentsMethod: (webContents) => { webContents.setZoomLevel(0) } }, selectall: { label: 'Select All', accelerator: 'CommandOrControl+A', webContentsMethod: 'selectAll' }, services: { label: 'Services' }, startspeaking: { label: 'Start Speaking' }, stopspeaking: { label: 'Stop Speaking' }, toggledevtools: { label: 'Toggle Developer Tools', accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I', nonNativeMacOSRole: true, windowMethod: 'toggleDevTools' }, togglefullscreen: { label: 'Toggle Full Screen', accelerator: process.platform === 'darwin' ? 'Control+Command+F' : 'F11', windowMethod: (window) => { window.setFullScreen(!window.isFullScreen()) } }, undo: { label: 'Undo', accelerator: 'CommandOrControl+Z', webContentsMethod: 'undo' }, unhide: { label: 'Show All' }, window: { label: 'Window' }, zoom: { label: 'Zoom' }, zoomin: { label: 'Zoom In', accelerator: 'CommandOrControl+Plus', nonNativeMacOSRole: true, webContentsMethod: (webContents) => { webContents.getZoomLevel((zoomLevel) => { webContents.setZoomLevel(zoomLevel + 0.5) }) } }, zoomout: { label: 'Zoom Out', accelerator: 'CommandOrControl+-', nonNativeMacOSRole: true, webContentsMethod: (webContents) => { webContents.getZoomLevel((zoomLevel) => { webContents.setZoomLevel(zoomLevel - 0.5) }) } }, // Edit submenu (should fit both Mac & Windows) 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' } ] }, // Window submenu should be used for Mac only windowMenu: { label: 'Window', submenu: [ { role: 'minimize' }, { role: 'close' }, process.platform === 'darwin' ? { type: 'separator' } : null, process.platform === 'darwin' ? { role: 'front' } : null ] } } const canExecuteRole = (role) => { if (!roles.hasOwnProperty(role)) return false if (process.platform !== 'darwin') return true // macOS handles all roles natively except for a few return roles[role].nonNativeMacOSRole } exports.getDefaultLabel = (role) => { if (roles.hasOwnProperty(role)) { return roles[role].label } else { return '' } } exports.getDefaultAccelerator = (role) => { if (roles.hasOwnProperty(role)) return roles[role].accelerator } exports.getDefaultSubmenu = (role) => { if (!roles.hasOwnProperty(role)) return let {submenu} = roles[role] // remove null items from within the submenu if (Array.isArray(submenu)) { submenu = submenu.filter((item) => item != null) } return submenu } exports.execute = (role, focusedWindow, focusedWebContents) => { if (!canExecuteRole(role)) return false const {appMethod, webContentsMethod, windowMethod} = roles[role] if (appMethod) { app[appMethod]() return true } if (windowMethod && focusedWindow != null) { if (typeof windowMethod === 'function') { windowMethod(focusedWindow) } else { focusedWindow[windowMethod]() } return true } if (webContentsMethod && focusedWebContents != null) { if (typeof webContentsMethod === 'function') { webContentsMethod(focusedWebContents) } else { focusedWebContents[webContentsMethod]() } return true } return false } 'use strict' const roles = require('./menu-item-roles') let nextCommandId = 0 const MenuItem = function (options) { const {Menu} = require('electron') // Preserve extra fields specified by user for (let key in options) { if (!(key in this)) this[key] = options[key] } this.submenu = this.submenu || roles.getDefaultSubmenu(this.role) if (this.submenu != null && this.submenu.constructor !== Menu) { this.submenu = Menu.buildFromTemplate(this.submenu) } if (this.type == null && this.submenu != null) { this.type = 'submenu' } if (this.type === 'submenu' && (this.submenu == null || this.submenu.constructor !== Menu)) { throw new Error('Invalid submenu') } this.overrideReadOnlyProperty('type', 'normal') this.overrideReadOnlyProperty('role') this.overrideReadOnlyProperty('accelerator') this.overrideReadOnlyProperty('icon') this.overrideReadOnlyProperty('submenu') this.overrideProperty('label', roles.getDefaultLabel(this.role)) this.overrideProperty('sublabel', '') this.overrideProperty('enabled', true) this.overrideProperty('visible', true) this.overrideProperty('checked', false) if (!MenuItem.types.includes(this.type)) { throw new Error(`Unknown menu item type: ${this.type}`) } this.overrideReadOnlyProperty('commandId', ++nextCommandId) const click = options.click this.click = (event, focusedWindow, focusedWebContents) => { // Manually flip the checked flags when clicked. if (this.type === 'checkbox' || this.type === 'radio') { this.checked = !this.checked } if (!roles.execute(this.role, focusedWindow, focusedWebContents)) { if (typeof click === 'function') { click(this, focusedWindow, event) } else if (typeof this.selector === 'string' && process.platform === 'darwin') { Menu.sendActionToFirstResponder(this.selector) } } } } MenuItem.types = ['normal', 'separator', 'submenu', 'checkbox', 'radio'] MenuItem.prototype.getDefaultRoleAccelerator = function () { return roles.getDefaultAccelerator(this.role) } MenuItem.prototype.overrideProperty = function (name, defaultValue = null) { if (this[name] == null) { this[name] = defaultValue } } MenuItem.prototype.overrideReadOnlyProperty = function (name, defaultValue) { this.overrideProperty(name, defaultValue) Object.defineProperty(this, name, { enumerable: true, writable: false, value: this[name] }) } module.exports = MenuItem 'use strict' const {BrowserWindow, MenuItem, webContents} = require('electron') const EventEmitter = require('events').EventEmitter const v8Util = process.atomBinding('v8_util') const bindings = process.atomBinding('menu') const {Menu} = bindings let applicationMenu = null let groupIdIndex = 0 Object.setPrototypeOf(Menu.prototype, EventEmitter.prototype) /* Instance Methods */ Menu.prototype._init = function () { this.commandsMap = {} this.groupsMap = {} this.items = [] this.delegate = { isCommandIdChecked: id => this.commandsMap[id] ? this.commandsMap[id].checked : undefined, isCommandIdEnabled: id => this.commandsMap[id] ? this.commandsMap[id].enabled : undefined, isCommandIdVisible: id => this.commandsMap[id] ? this.commandsMap[id].visible : undefined, getAcceleratorForCommandId: (id, useDefaultAccelerator) => { const command = this.commandsMap[id] if (!command) return if (command.accelerator != null) return command.accelerator if (useDefaultAccelerator) return command.getDefaultRoleAccelerator() }, getIconForCommandId: id => this.commandsMap[id] ? this.commandsMap[id].icon : undefined, executeCommand: (event, id) => { const command = this.commandsMap[id] if (!command) return command.click(event, BrowserWindow.getFocusedWindow(), webContents.getFocusedWebContents()) }, menuWillShow: () => { // Ensure radio groups have at least one menu item seleted for (const id in this.groupsMap) { const found = this.groupsMap[id].find(item => item.checked) || null if (!found) v8Util.setHiddenValue(this.groupsMap[id][0], 'checked', true) } } } } Menu.prototype.popup = function (window, x, y, positioningItem) { let asyncPopup, opts let [newX, newY, newPosition, newWindow] = [x, y, positioningItem, window] // menu.popup(x, y, positioningItem) if (window != null && !(window instanceof BrowserWindow)) { [newPosition, newY, newX, newWindow] = [y, x, window, null] } // menu.popup({}) if (window != null && window.constructor === Object) { opts = window // menu.popup(window, {}) } else if (x && typeof x === 'object') { opts = x } if (opts) { newX = opts.x newY = opts.y newPosition = opts.positioningItem asyncPopup = opts.async } // set defaults if (typeof newX !== 'number') newX = -1 if (typeof newY !== 'number') newY = -1 if (typeof newPosition !== 'number') newPosition = -1 if (typeof asyncPopup !== 'boolean') asyncPopup = false if (!newWindow || (newWindow && newWindow.constructor !== BrowserWindow)) { newWindow = BrowserWindow.getFocusedWindow() // No window focused? if (!newWindow) { const browserWindows = BrowserWindow.getAllWindows() if (browserWindows && browserWindows.length > 0) { newWindow = browserWindows[0] } else { throw new Error(`Cannot open Menu without a BrowserWindow present`) } } } this.popupAt(newWindow, newX, newY, newPosition, asyncPopup) return { browserWindow: newWindow, x: newX, y: newY, position: newPosition, async: asyncPopup } } Menu.prototype.closePopup = function (window) { if (!window || window.constructor !== BrowserWindow) { window = BrowserWindow.getFocusedWindow() } if (window) this.closePopupAt(window.id) } Menu.prototype.getMenuItemById = function (id) { const items = this.items let found = items.find(item => item.id === id) || null for (let i = 0; !found && i < items.length; i++) { if (items[i].submenu) { found = items[i].submenu.getMenuItemById(id) } } return found } Menu.prototype.append = function (item) { return this.insert(this.getItemCount(), item) } Menu.prototype.insert = function (pos, item) { if ((item ? item.constructor : void 0) !== MenuItem) { throw new TypeError('Invalid item') } // insert item depending on its type insertItemByType.call(this, item, pos) // set item properties if (item.sublabel) this.setSublabel(pos, item.sublabel) if (item.icon) this.setIcon(pos, item.icon) if (item.role) this.setRole(pos, item.role) // Make menu accessable to items. item.overrideReadOnlyProperty('menu', this) // Remember the items. this.items.splice(pos, 0, item) this.commandsMap[item.commandId] = item } Menu.prototype._callMenuWillShow = function () { if (this.delegate) this.delegate.menuWillShow() this.items.forEach(item => { if (item.submenu) item.submenu._callMenuWillShow() }) } /* Static Methods */ Menu.getApplicationMenu = () => applicationMenu Menu.sendActionToFirstResponder = bindings.sendActionToFirstResponder // set application menu with a preexisting menu Menu.setApplicationMenu = function (menu) { if (menu && menu.constructor !== Menu) { throw new TypeError('Invalid menu') } applicationMenu = menu if (process.platform === 'darwin') { if (!menu) return menu._callMenuWillShow() bindings.setApplicationMenu(menu) } else { const windows = BrowserWindow.getAllWindows() return windows.map(w => w.setMenu(menu)) } } Menu.buildFromTemplate = function (template) { if (!Array.isArray(template)) { throw new TypeError('Invalid template for Menu') } const menu = new Menu() const positioned = [] let idx = 0 // sort template by position template.forEach(item => { idx = (item.position) ? indexToInsertByPosition(positioned, item.position) : idx += 1 positioned.splice(idx, 0, item) }) // add each item from positioned menu to application menu positioned.forEach((item) => { if (typeof item !== 'object') { throw new TypeError('Invalid template for MenuItem') } menu.append(new MenuItem(item)) }) return menu } /* Helper Functions */ // Search between separators to find a radio menu item and return its group id function generateGroupId (items, pos) { if (pos > 0) { for (let idx = pos - 1; idx >= 0; idx--) { if (items[idx].type === 'radio') return items[idx].groupId if (items[idx].type === 'separator') break } } else if (pos < items.length) { for (let idx = pos; idx <= items.length - 1; idx++) { if (items[idx].type === 'radio') return items[idx].groupId if (items[idx].type === 'separator') break } } groupIdIndex += 1 return groupIdIndex } function indexOfItemById (items, id) { const foundItem = items.find(item => item.id === id) || -1 return items.indexOf(foundItem) } function indexToInsertByPosition (items, position) { if (!position) return items.length const [query, id] = position.split('=') // parse query and id from position const idx = indexOfItemById(items, id) // calculate initial index of item // warn if query doesn't exist if (idx === -1 && query !== 'endof') { console.warn(`Item with id ${id} is not found`) return items.length } // compute new index based on query const queries = { after: (index) => { index += 1 return index }, endof: (index) => { if (index === -1) { items.push({id, type: 'separator'}) index = items.length - 1 } index += 1 while (index < items.length && items[index].type !== 'separator') index += 1 return index } } // return new index if needed, or original indexOfItemById return (query in queries) ? queries[query](idx) : idx } function insertItemByType (item, pos) { const types = { normal: () => this.insertItem(pos, item.commandId, item.label), checkbox: () => this.insertCheckItem(pos, item.commandId, item.label), separator: () => this.insertSeparator(pos), submenu: () => this.insertSubMenu(pos, item.commandId, item.label, item.submenu), radio: () => { // Grouping radio menu items item.overrideReadOnlyProperty('groupId', generateGroupId(this.items, pos)) if (this.groupsMap[item.groupId] == null) { this.groupsMap[item.groupId] = [] } this.groupsMap[item.groupId].push(item) // Setting a radio menu item should flip other items in the group. v8Util.setHiddenValue(item, 'checked', item.checked) Object.defineProperty(item, 'checked', { enumerable: true, get: () => v8Util.getHiddenValue(item, 'checked'), set: () => { this.groupsMap[item.groupId].forEach(other => { if (other !== item) v8Util.setHiddenValue(other, 'checked', false) }) v8Util.setHiddenValue(item, 'checked', true) } }) this.insertRadioItem(pos, item.commandId, item.label, item.groupId) } } types[item.type]() } module.exports = Menu // Browser side modules, please sort alphabetically. module.exports = [ {name: 'app', file: 'app'}, {name: 'autoUpdater', file: 'auto-updater'}, {name: 'BrowserView', file: 'browser-view'}, {name: 'BrowserWindow', file: 'browser-window'}, {name: 'contentTracing', file: 'content-tracing'}, {name: 'dialog', file: 'dialog'}, {name: 'globalShortcut', file: 'global-shortcut'}, {name: 'ipcMain', file: 'ipc-main'}, {name: 'Menu', file: 'menu'}, {name: 'MenuItem', file: 'menu-item'}, {name: 'net', file: 'net'}, {name: 'Notification', file: 'notification'}, {name: 'powerMonitor', file: 'power-monitor'}, {name: 'powerSaveBlocker', file: 'power-save-blocker'}, {name: 'protocol', file: 'protocol'}, {name: 'screen', file: 'screen'}, {name: 'session', file: 'session'}, {name: 'systemPreferences', file: 'system-preferences'}, {name: 'TouchBar', file: 'touch-bar'}, {name: 'Tray', file: 'tray'}, {name: 'webContents', file: 'web-contents'}, // The internal modules, invisible unless you know their names. {name: 'NavigationController', file: 'navigation-controller', private: true} ] 'use strict' const {ipcMain} = require('electron') // 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_SYNC_NAVIGATION_CONTROLLER', function (event, method, ...args) { event.returnValue = event.sender[method](...args) }) // JavaScript implementation of Chromium's NavigationController. // Instead of relying on Chromium for history control, we compeletely do history // control on user land, and only rely on WebContents.loadURL for navigation. // This helps us avoid Chromium's various optimizations so we can ensure renderer // process is restarted everytime. var NavigationController = (function () { function NavigationController (webContents) { this.webContents = webContents this.clearHistory() // webContents may have already navigated to a page. if (this.webContents._getURL()) { this.currentIndex++ this.history.push(this.webContents._getURL()) } this.webContents.on('navigation-entry-commited', (event, url, inPage, replaceEntry) => { if (this.inPageIndex > -1 && !inPage) { // Navigated to a new page, clear in-page mark. this.inPageIndex = -1 } else if (this.inPageIndex === -1 && inPage && !replaceEntry) { // Started in-page navigations. this.inPageIndex = this.currentIndex } if (this.pendingIndex >= 0) { // Go to index. this.currentIndex = this.pendingIndex this.pendingIndex = -1 this.history[this.currentIndex] = url } else if (replaceEntry) { // Non-user initialized navigation. this.history[this.currentIndex] = url } else { // Normal navigation. Clear history. this.history = this.history.slice(0, this.currentIndex + 1) this.currentIndex++ this.history.push(url) } }) } NavigationController.prototype.loadURL = function (url, options) { if (options == null) { options = {} } this.pendingIndex = -1 this.webContents._loadURL(url, options) return this.webContents.emit('load-url', url, options) } NavigationController.prototype.getURL = function () { if (this.currentIndex === -1) { return '' } else { return this.history[this.currentIndex] } } NavigationController.prototype.stop = function () { this.pendingIndex = -1 return this.webContents._stop() } NavigationController.prototype.reload = function () { this.pendingIndex = this.currentIndex return this.webContents._loadURL(this.getURL(), {}) } NavigationController.prototype.reloadIgnoringCache = function () { this.pendingIndex = this.currentIndex return this.webContents._loadURL(this.getURL(), { extraHeaders: 'pragma: no-cache\n' }) } NavigationController.prototype.canGoBack = function () { return this.getActiveIndex() > 0 } NavigationController.prototype.canGoForward = function () { return this.getActiveIndex() < this.history.length - 1 } NavigationController.prototype.canGoToIndex = function (index) { return index >= 0 && index < this.history.length } NavigationController.prototype.canGoToOffset = function (offset) { return this.canGoToIndex(this.currentIndex + offset) } NavigationController.prototype.clearHistory = function () { this.history = [] this.currentIndex = -1 this.pendingIndex = -1 this.inPageIndex = -1 } NavigationController.prototype.goBack = function () { if (!this.canGoBack()) { return } this.pendingIndex = this.getActiveIndex() - 1 if (this.inPageIndex > -1 && this.pendingIndex >= this.inPageIndex) { return this.webContents._goBack() } else { return this.webContents._loadURL(this.history[this.pendingIndex], {}) } } NavigationController.prototype.goForward = function () { if (!this.canGoForward()) { return } this.pendingIndex = this.getActiveIndex() + 1 if (this.inPageIndex > -1 && this.pendingIndex >= this.inPageIndex) { return this.webContents._goForward() } else { return this.webContents._loadURL(this.history[this.pendingIndex], {}) } } NavigationController.prototype.goToIndex = function (index) { if (!this.canGoToIndex(index)) { return } this.pendingIndex = index return this.webContents._loadURL(this.history[this.pendingIndex], {}) } NavigationController.prototype.goToOffset = function (offset) { var pendingIndex if (!this.canGoToOffset(offset)) { return } pendingIndex = this.currentIndex + offset if (this.inPageIndex > -1 && pendingIndex >= this.inPageIndex) { this.pendingIndex = pendingIndex return this.webContents._goToOffset(offset) } else { return this.goToIndex(pendingIndex) } } NavigationController.prototype.getActiveIndex = function () { if (this.pendingIndex === -1) { return this.currentIndex } else { return this.pendingIndex } } NavigationController.prototype.length = function () { return this.history.length } return NavigationController })() module.exports = NavigationController 'use strict' const url = require('url') const {EventEmitter} = require('events') const {Readable} = require('stream') const {app} = require('electron') const {Session} = process.atomBinding('session') const {net, Net} = process.atomBinding('net') const {URLRequest} = net // Net is an EventEmitter. Object.setPrototypeOf(Net.prototype, EventEmitter.prototype) EventEmitter.call(net) Object.setPrototypeOf(URLRequest.prototype, EventEmitter.prototype) const kSupportedProtocols = new Set(['http:', 'https:']) class IncomingMessage extends Readable { constructor (urlRequest) { super() this.urlRequest = urlRequest this.shouldPush = false this.data = [] this.urlRequest.on('data', (event, chunk) => { this._storeInternalData(chunk) this._pushInternalData() }) this.urlRequest.on('end', () => { this._storeInternalData(null) this._pushInternalData() }) } get statusCode () { return this.urlRequest.statusCode } get statusMessage () { return this.urlRequest.statusMessage } get headers () { return this.urlRequest.rawResponseHeaders } get httpVersion () { return `${this.httpVersionMajor}.${this.httpVersionMinor}` } get httpVersionMajor () { return this.urlRequest.httpVersionMajor } get httpVersionMinor () { return this.urlRequest.httpVersionMinor } get rawTrailers () { throw new Error('HTTP trailers are not supported.') } get trailers () { throw new Error('HTTP trailers are not supported.') } _storeInternalData (chunk) { this.data.push(chunk) } _pushInternalData () { while (this.shouldPush && this.data.length > 0) { const chunk = this.data.shift() this.shouldPush = this.push(chunk) } } _read () { this.shouldPush = true this._pushInternalData() } } URLRequest.prototype._emitRequestEvent = function (isAsync, ...rest) { if (isAsync) { process.nextTick(() => { this.clientRequest.emit(...rest) }) } else { this.clientRequest.emit(...rest) } } URLRequest.prototype._emitResponseEvent = function (isAsync, ...rest) { if (isAsync) { process.nextTick(() => { this._response.emit(...rest) }) } else { this._response.emit(...rest) } } class ClientRequest extends EventEmitter { constructor (options, callback) { super() if (!app.isReady()) { throw new Error('net module can only be used after app is ready') } if (typeof options === 'string') { options = url.parse(options) } else { options = Object.assign({}, options) } const method = (options.method || 'GET').toUpperCase() let urlStr = options.url if (!urlStr) { let urlObj = {} const protocol = options.protocol || 'http:' if (!kSupportedProtocols.has(protocol)) { throw new Error('Protocol "' + protocol + '" not supported. ') } urlObj.protocol = protocol if (options.host) { urlObj.host = options.host } else { if (options.hostname) { urlObj.hostname = options.hostname } else { urlObj.hostname = 'localhost' } if (options.port) { urlObj.port = options.port } } if (options.path && / /.test(options.path)) { // The actual regex is more like /[^A-Za-z0-9\-._~!$&'()*+,;=/:@]/ // with an additional rule for ignoring percentage-escaped characters // but that's a) hard to capture in a regular expression that performs // well, and b) possibly too restrictive for real-world usage. That's // why it only scans for spaces because those are guaranteed to create // an invalid request. throw new TypeError('Request path contains unescaped characters.') } let pathObj = url.parse(options.path || '/') urlObj.pathname = pathObj.pathname urlObj.search = pathObj.search urlObj.hash = pathObj.hash urlStr = url.format(urlObj) } const redirectPolicy = options.redirect || 'follow' if (!['follow', 'error', 'manual'].includes(redirectPolicy)) { throw new Error('redirect mode should be one of follow, error or manual') } let urlRequestOptions = { method: method, url: urlStr, redirect: redirectPolicy } if (options.session) { if (options.session instanceof Session) { urlRequestOptions.session = options.session } else { throw new TypeError('`session` should be an instance of the Session class.') } } else if (options.partition) { if (typeof options.partition === 'string') { urlRequestOptions.partition = options.partition } else { throw new TypeError('`partition` should be an a string.') } } let urlRequest = new URLRequest(urlRequestOptions) // Set back and forward links. this.urlRequest = urlRequest urlRequest.clientRequest = this // This is a copy of the extra headers structure held by the native // net::URLRequest. The main reason is to keep the getHeader API synchronous // after the request starts. this.extraHeaders = {} if (options.headers) { for (let key in options.headers) { this.setHeader(key, options.headers[key]) } } // Set when the request uses chunked encoding. Can be switched // to true only once and never set back to false. this.chunkedEncodingEnabled = false urlRequest.on('response', () => { const response = new IncomingMessage(urlRequest) urlRequest._response = response this.emit('response', response) }) urlRequest.on('login', (event, authInfo, callback) => { this.emit('login', authInfo, (username, password) => { // If null or undefined username/password, force to empty string. if (username === null || username === undefined) { username = '' } if (typeof username !== 'string') { throw new Error('username must be a string') } if (password === null || password === undefined) { password = '' } if (typeof password !== 'string') { throw new Error('password must be a string') } callback(username, password) }) }) if (callback) { this.once('response', callback) } } get chunkedEncoding () { return this.chunkedEncodingEnabled } set chunkedEncoding (value) { if (!this.urlRequest.notStarted) { throw new Error('Can\'t set the transfer encoding, headers have been sent.') } this.chunkedEncodingEnabled = value } setHeader (name, value) { if (typeof name !== 'string') { throw new TypeError('`name` should be a string in setHeader(name, value).') } if (value == null) { throw new Error('`value` required in setHeader("' + name + '", value).') } if (!this.urlRequest.notStarted) { throw new Error('Can\'t set headers after they are sent.') } const key = name.toLowerCase() this.extraHeaders[key] = value this.urlRequest.setExtraHeader(name, value.toString()) } getHeader (name) { if (name == null) { throw new Error('`name` is required for getHeader(name).') } if (!this.extraHeaders) { return } const key = name.toLowerCase() return this.extraHeaders[key] } removeHeader (name) { if (name == null) { throw new Error('`name` is required for removeHeader(name).') } if (!this.urlRequest.notStarted) { throw new Error('Can\'t remove headers after they are sent.') } const key = name.toLowerCase() delete this.extraHeaders[key] this.urlRequest.removeExtraHeader(name) } _write (chunk, encoding, callback, isLast) { let chunkIsString = typeof chunk === 'string' let chunkIsBuffer = chunk instanceof Buffer if (!chunkIsString && !chunkIsBuffer) { throw new TypeError('First argument must be a string or Buffer.') } if (chunkIsString) { // We convert all strings into binary buffers. chunk = Buffer.from(chunk, encoding) } // Since writing to the network is asynchronous, we conservatively // assume that request headers are written after delivering the first // buffer to the network IO thread. if (this.urlRequest.notStarted) { this.urlRequest.setChunkedUpload(this.chunkedEncoding) } // Headers are assumed to be sent on first call to _writeBuffer, // i.e. after the first call to write or end. let result = this.urlRequest.write(chunk, isLast) // The write callback is fired asynchronously to mimic Node.js. if (callback) { process.nextTick(callback) } return result } write (data, encoding, callback) { if (this.urlRequest.finished) { let error = new Error('Write after end.') process.nextTick(writeAfterEndNT, this, error, callback) return true } return this._write(data, encoding, callback, false) } end (data, encoding, callback) { if (this.urlRequest.finished) { return false } if (typeof data === 'function') { callback = data encoding = null data = null } else if (typeof encoding === 'function') { callback = encoding encoding = null } data = data || '' return this._write(data, encoding, callback, true) } followRedirect () { this.urlRequest.followRedirect() } abort () { this.urlRequest.cancel() } } function writeAfterEndNT (self, error, callback) { self.emit('error', error) if (callback) callback(error) } Net.prototype.request = function (options, callback) { return new ClientRequest(options, callback) } net.ClientRequest = ClientRequest module.exports = net const {EventEmitter} = require('events') const {Notification, isSupported} = process.atomBinding('notification') Object.setPrototypeOf(Notification.prototype, EventEmitter.prototype) Notification.isSupported = isSupported module.exports = Notification const {EventEmitter} = require('events') const {powerMonitor, PowerMonitor} = process.atomBinding('power_monitor') // PowerMonitor is an EventEmitter. Object.setPrototypeOf(PowerMonitor.prototype, EventEmitter.prototype) EventEmitter.call(powerMonitor) module.exports = powerMonitor module.exports = process.atomBinding('power_save_blocker').powerSaveBlocker const {app, session} = require('electron') // Global protocol APIs. module.exports = process.atomBinding('protocol') // Fallback protocol APIs of default session. Object.setPrototypeOf(module.exports, new Proxy({}, { get (target, property) { if (!app.isReady()) return const protocol = session.defaultSession.protocol if (!Object.getPrototypeOf(protocol).hasOwnProperty(property)) return // Returning a native function directly would throw error. return (...args) => protocol[property](...args) }, ownKeys () { if (!app.isReady()) return [] return Object.getOwnPropertyNames(Object.getPrototypeOf(session.defaultSession.protocol)) }, getOwnPropertyDescriptor (target) { return { configurable: true, enumerable: true } } })) const {EventEmitter} = require('events') const {screen, Screen} = process.atomBinding('screen') // Screen is an EventEmitter. Object.setPrototypeOf(Screen.prototype, EventEmitter.prototype) EventEmitter.call(screen) module.exports = screen const {EventEmitter} = require('events') const {app} = require('electron') const {fromPartition, Session, Cookies} = process.atomBinding('session') // Public API. Object.defineProperties(exports, { defaultSession: { enumerable: true, get () { return fromPartition('') } }, fromPartition: { enumerable: true, value: fromPartition } }) Object.setPrototypeOf(Session.prototype, EventEmitter.prototype) Object.setPrototypeOf(Cookies.prototype, EventEmitter.prototype) Session.prototype._init = function () { app.emit('session-created', this) } Session.prototype.setCertificateVerifyProc = function (verifyProc) { if (verifyProc != null && verifyProc.length > 2) { // TODO(kevinsawicki): Remove in 2.0, deprecate before then with warnings this._setCertificateVerifyProc(({hostname, certificate, verificationResult}, cb) => { verifyProc(hostname, certificate, (result) => { cb(result ? 0 : -2) }) }) } else { this._setCertificateVerifyProc(verifyProc) } } const {EventEmitter} = require('events') const {systemPreferences, SystemPreferences} = process.atomBinding('system_preferences') // SystemPreferences is an EventEmitter. Object.setPrototypeOf(SystemPreferences.prototype, EventEmitter.prototype) EventEmitter.call(systemPreferences) module.exports = systemPreferences const {EventEmitter} = require('events') let nextItemID = 1 class TouchBar extends EventEmitter { // Bind a touch bar to a window static _setOnWindow (touchBar, window) { if (window._touchBar != null) { window._touchBar._removeFromWindow(window) } if (touchBar == null) { window._setTouchBarItems([]) return } if (Array.isArray(touchBar)) { touchBar = new TouchBar(touchBar) } touchBar._addToWindow(window) } constructor (options) { super() if (options == null) { throw new Error('Must specify options object as first argument') } 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 = [] } this.changeListener = (item) => { this.emit('change', item.id, item.type) } this.windowListeners = {} this.items = {} this.ordereredItems = [] this.escapeItem = escapeItem const registerItem = (item) => { this.items[item.id] = item item.on('change', this.changeListener) if (item.child instanceof TouchBar) { item.child.ordereredItems.forEach(registerItem) } } items.forEach((item) => { if (!(item instanceof TouchBarItem)) { throw new Error('Each item must be an instance of TouchBarItem') } this.ordereredItems.push(item) registerItem(item) }) } set escapeItem (item) { if (item != null && !(item instanceof TouchBarItem)) { throw new Error('Escape item must be an instance of TouchBarItem') } if (this.escapeItem != null) { this.escapeItem.removeListener('change', this.changeListener) } this._escapeItem = item if (this.escapeItem != null) { this.escapeItem.on('change', this.changeListener) } this.emit('escape-item-change', item) } get escapeItem () { return this._escapeItem } _addToWindow (window) { const {id} = window // Already added to window if (this.windowListeners.hasOwnProperty(id)) return window._touchBar = this const changeListener = (itemID) => { window._refreshTouchBarItem(itemID) } this.on('change', changeListener) const escapeItemListener = (item) => { window._setEscapeTouchBarItem(item != null ? item : {}) } this.on('escape-item-change', escapeItemListener) const interactionListener = (event, itemID, details) => { let item = this.items[itemID] if (item == null && this.escapeItem != null && this.escapeItem.id === itemID) { item = this.escapeItem } if (item != null && item.onInteraction != null) { item.onInteraction(details) } } window.on('-touch-bar-interaction', interactionListener) const removeListeners = () => { this.removeListener('change', changeListener) this.removeListener('escape-item-change', escapeItemListener) window.removeListener('-touch-bar-interaction', interactionListener) window.removeListener('closed', removeListeners) window._touchBar = null delete this.windowListeners[id] const unregisterItems = (items) => { for (const item of items) { item.removeListener('change', this.changeListener) if (item.child instanceof TouchBar) { unregisterItems(item.child.ordereredItems) } } } unregisterItems(this.ordereredItems) if (this.escapeItem) { this.escapeItem.removeListener('change', this.changeListener) } } window.once('closed', removeListeners) this.windowListeners[id] = removeListeners window._setTouchBarItems(this.ordereredItems) escapeItemListener(this.escapeItem) } _removeFromWindow (window) { const removeListeners = this.windowListeners[window.id] if (removeListeners != null) removeListeners() } } class TouchBarItem extends EventEmitter { constructor () { super() this.id = `${nextItemID++}` } _addLiveProperty (name, initialValue) { const privateName = `_${name}` this[privateName] = initialValue Object.defineProperty(this, name, { get: function () { return this[privateName] }, set: function (value) { this[privateName] = value this.emit('change', this) }, enumerable: true }) } } TouchBar.TouchBarButton = class TouchBarButton extends TouchBarItem { constructor (config) { super() if (config == null) config = {} this.type = 'button' const {click, icon, iconPosition, label, backgroundColor} = config this._addLiveProperty('label', label) this._addLiveProperty('backgroundColor', backgroundColor) this._addLiveProperty('icon', icon) this._addLiveProperty('iconPosition', iconPosition) if (typeof click === 'function') { this.onInteraction = () => { config.click() } } } } TouchBar.TouchBarColorPicker = class TouchBarColorPicker extends TouchBarItem { constructor (config) { super() if (config == null) config = {} this.type = 'colorpicker' const {availableColors, change, selectedColor} = config this._addLiveProperty('availableColors', availableColors) this._addLiveProperty('selectedColor', selectedColor) if (typeof change === 'function') { this.onInteraction = (details) => { this._selectedColor = details.color change(details.color) } } } } TouchBar.TouchBarGroup = class TouchBarGroup extends TouchBarItem { constructor (config) { super() if (config == null) config = {} this.type = 'group' this.child = config.items if (!(this.child instanceof TouchBar)) { this.child = new TouchBar(this.child) } } } TouchBar.TouchBarLabel = class TouchBarLabel extends TouchBarItem { constructor (config) { super() if (config == null) config = {} this.type = 'label' this._addLiveProperty('label', config.label) this._addLiveProperty('textColor', config.textColor) } } TouchBar.TouchBarPopover = class TouchBarPopover extends TouchBarItem { constructor (config) { super() if (config == null) config = {} this.type = 'popover' this._addLiveProperty('label', config.label) this._addLiveProperty('icon', config.icon) this.showCloseButton = config.showCloseButton this.child = config.items if (!(this.child instanceof TouchBar)) { this.child = new TouchBar(this.child) } this.child.ordereredItems.forEach((item) => { item._popover = item._popover || [] if (!item._popover.includes(this.id)) item._popover.push(this.id) }) } } TouchBar.TouchBarSlider = class TouchBarSlider extends TouchBarItem { constructor (config) { super() if (config == null) config = {} this.type = 'slider' const {change, label, minValue, maxValue, value} = config this._addLiveProperty('label', label) this._addLiveProperty('minValue', minValue) this._addLiveProperty('maxValue', maxValue) this._addLiveProperty('value', value) if (typeof change === 'function') { this.onInteraction = (details) => { this._value = details.value change(details.value) } } } } TouchBar.TouchBarSpacer = class TouchBarSpacer extends TouchBarItem { constructor (config) { super() if (config == null) config = {} this.type = 'spacer' this.size = config.size } } TouchBar.TouchBarSegmentedControl = class TouchBarSegmentedControl extends TouchBarItem { constructor (config) { super() if (config == null) config = {} const {segmentStyle, segments, selectedIndex, change, mode} = config this.type = 'segmented_control' this._addLiveProperty('segmentStyle', segmentStyle) this._addLiveProperty('segments', segments || []) this._addLiveProperty('selectedIndex', selectedIndex) this._addLiveProperty('mode', mode) if (typeof change === 'function') { this.onInteraction = (details) => { this._selectedIndex = details.selectedIndex change(details.selectedIndex, details.isSelected) } } } } TouchBar.TouchBarScrubber = class TouchBarScrubber extends TouchBarItem { constructor (config) { super() if (config == null) config = {} const {items, selectedStyle, overlayStyle, showArrowButtons, continuous, mode} = config let {select, highlight} = config this.type = 'scrubber' this._addLiveProperty('items', items) this._addLiveProperty('selectedStyle', selectedStyle || null) this._addLiveProperty('overlayStyle', overlayStyle || null) this._addLiveProperty('showArrowButtons', showArrowButtons || false) this._addLiveProperty('mode', mode || 'free') this._addLiveProperty('continuous', typeof continuous === 'undefined' ? true : continuous) if (typeof select === 'function' || typeof highlight === 'function') { if (select == null) select = () => {} if (highlight == null) highlight = () => {} this.onInteraction = (details) => { if (details.type === 'select') { select(details.selectedIndex) } else if (details.type === 'highlight') { highlight(details.highlightedIndex) } } } } } module.exports = TouchBar const {EventEmitter} = require('events') const {Tray} = process.atomBinding('tray') Object.setPrototypeOf(Tray.prototype, EventEmitter.prototype) module.exports = Tray 'use strict' const {EventEmitter} = require('events') const electron = require('electron') const {app, ipcMain, session, NavigationController} = electron // session is not used here, the purpose is to make sure session is initalized // before the webContents module. session let nextId = 0 const getNextId = function () { return ++nextId } // Stock page sizes const PDFPageSizes = { A5: { custom_display_name: 'A5', height_microns: 210000, name: 'ISO_A5', width_microns: 148000 }, A4: { custom_display_name: 'A4', height_microns: 297000, name: 'ISO_A4', is_default: 'true', width_microns: 210000 }, A3: { custom_display_name: 'A3', height_microns: 420000, name: 'ISO_A3', width_microns: 297000 }, Legal: { custom_display_name: 'Legal', height_microns: 355600, name: 'NA_LEGAL', width_microns: 215900 }, Letter: { custom_display_name: 'Letter', height_microns: 279400, name: 'NA_LETTER', width_microns: 215900 }, Tabloid: { height_microns: 431800, name: 'NA_LEDGER', width_microns: 279400, custom_display_name: 'Tabloid' } } // Default printing setting const defaultPrintingSetting = { pageRage: [], mediaSize: {}, landscape: false, color: 2, headerFooterEnabled: false, marginsType: 0, isFirstRequest: false, requestID: getNextId(), previewModifiable: true, printToPDF: true, printWithCloudPrint: false, printWithPrivet: false, printWithExtension: false, deviceName: 'Save as PDF', generateDraftData: true, fitToPageEnabled: false, scaleFactor: 1, dpiHorizontal: 72, dpiVertical: 72, rasterizePDF: false, duplex: 0, copies: 1, collate: true, shouldPrintBackgrounds: false, shouldPrintSelectionOnly: false } // JavaScript implementations of WebContents. const binding = process.atomBinding('web_contents') const {WebContents} = binding Object.setPrototypeOf(NavigationController.prototype, EventEmitter.prototype) Object.setPrototypeOf(WebContents.prototype, NavigationController.prototype) // WebContents::send(channel, args..) // WebContents::sendToAll(channel, args..) WebContents.prototype.send = function (channel, ...args) { if (channel == null) throw new Error('Missing required channel argument') return this._send(false, channel, args) } WebContents.prototype.sendToAll = function (channel, ...args) { if (channel == null) throw new Error('Missing required channel argument') return this._send(true, channel, args) } // Following methods are mapped to webFrame. const webFrameMethods = [ 'insertCSS', 'insertText', 'setLayoutZoomLevelLimits', 'setVisualZoomLevelLimits', // TODO(kevinsawicki): Remove in 2.0, deprecate before then with warnings 'setZoomLevelLimits' ] const webFrameMethodsWithResult = [] const errorConstructors = { Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError } const asyncWebFrameMethods = function (requestId, method, callback, ...args) { return new Promise((resolve, reject) => { this.send('ELECTRON_INTERNAL_RENDERER_ASYNC_WEB_FRAME_METHOD', requestId, method, args) ipcMain.once(`ELECTRON_INTERNAL_BROWSER_ASYNC_WEB_FRAME_RESPONSE_${requestId}`, function (event, error, result) { if (error == null) { if (typeof callback === 'function') callback(result) resolve(result) } else { if (error.__ELECTRON_SERIALIZED_ERROR__ && errorConstructors[error.name]) { const rehydratedError = new errorConstructors[error.name](error.message) rehydratedError.stack = error.stack reject(rehydratedError) } else { reject(error) } } }) }) } const syncWebFrameMethods = function (requestId, method, callback, ...args) { this.send('ELECTRON_INTERNAL_RENDERER_SYNC_WEB_FRAME_METHOD', requestId, method, args) ipcMain.once(`ELECTRON_INTERNAL_BROWSER_SYNC_WEB_FRAME_RESPONSE_${requestId}`, function (event, result) { if (callback) callback(result) }) } for (const method of webFrameMethods) { WebContents.prototype[method] = function (...args) { this.send('ELECTRON_INTERNAL_RENDERER_WEB_FRAME_METHOD', method, args) } } for (const method of webFrameMethodsWithResult) { WebContents.prototype[method] = function (...args) { const callback = args[args.length - 1] const actualArgs = args.slice(0, args.length - 2) syncWebFrameMethods.call(this, getNextId(), method, callback, ...actualArgs) } } // Make sure WebContents::executeJavaScript would run the code only when the // WebContents has been loaded. WebContents.prototype.executeJavaScript = function (code, hasUserGesture, callback) { const requestId = getNextId() if (typeof hasUserGesture === 'function') { // Shift. callback = hasUserGesture hasUserGesture = null } if (hasUserGesture == null) { hasUserGesture = false } if (this.getURL() && !this.isLoadingMainFrame()) { return asyncWebFrameMethods.call(this, requestId, 'executeJavaScript', callback, code, hasUserGesture) } else { return new Promise((resolve, reject) => { this.once('did-finish-load', () => { asyncWebFrameMethods.call(this, requestId, 'executeJavaScript', callback, code, hasUserGesture).then(resolve).catch(reject) }) }) } } // Translate the options of printToPDF. WebContents.prototype.printToPDF = function (options, callback) { const printingSetting = Object.assign({}, defaultPrintingSetting) if (options.landscape) { printingSetting.landscape = options.landscape } if (options.marginsType) { printingSetting.marginsType = options.marginsType } if (options.printSelectionOnly) { printingSetting.shouldPrintSelectionOnly = options.printSelectionOnly } if (options.printBackground) { printingSetting.shouldPrintBackgrounds = options.printBackground } if (options.pageSize) { const pageSize = options.pageSize if (typeof pageSize === 'object') { if (!pageSize.height || !pageSize.width) { return callback(new Error('Must define height and width for pageSize')) } // Dimensions in Microns // 1 meter = 10^6 microns printingSetting.mediaSize = { name: 'CUSTOM', custom_display_name: 'Custom', height_microns: Math.ceil(pageSize.height), width_microns: Math.ceil(pageSize.width) } } else if (PDFPageSizes[pageSize]) { printingSetting.mediaSize = PDFPageSizes[pageSize] } else { return callback(new Error(`Does not support pageSize with ${pageSize}`)) } } else { printingSetting.mediaSize = PDFPageSizes['A4'] } this._printToPDF(printingSetting, callback) } 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.getZoomFactor = function (callback) { if (typeof callback !== 'function') { throw new Error('Must pass function as an argument') } process.nextTick(() => { const zoomFactor = this._getZoomFactor() callback(zoomFactor) }) } // Add JavaScript wrappers for WebContents class. 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. this.setMaxListeners(0) // Dispatch IPC messages to the ipc module. this.on('ipc-message', function (event, [channel, ...args]) { ipcMain.emit(channel, event, ...args) }) this.on('ipc-message-sync', function (event, [channel, ...args]) { Object.defineProperty(event, 'returnValue', { set: function (value) { return event.sendReply(JSON.stringify(value)) }, get: function () {} }) ipcMain.emit(channel, event, ...args) }) // Handle context menu action request from pepper plugin. this.on('pepper-context-menu', function (event, params) { // Access Menu via electron.Menu to prevent circular require const menu = electron.Menu.buildFromTemplate(params.menu) menu.popup(event.sender.getOwnerBrowserWindow(), params.x, params.y) }) // The devtools requests the webContents to reload. this.on('devtools-reload-page', function () { this.reload() }) app.emit('web-contents-created', {}, this) } // JavaScript wrapper of Debugger. const {Debugger} = process.atomBinding('debugger') Object.setPrototypeOf(Debugger.prototype, EventEmitter.prototype) // Public APIs. module.exports = { create (options = {}) { return binding.create(options) }, fromId (id) { return binding.fromId(id) }, getFocusedWebContents () { let focused = null for (let contents of binding.getAllWebContents()) { if (!contents.isFocused()) continue if (focused == null) focused = contents // Return webview web contents which may be embedded inside another // web contents that is also reporting as focused if (contents.getType() === 'webview') return contents } return focused }, getAllWebContents () { return binding.getAllWebContents() } } const {app, ipcMain, webContents, BrowserWindow} = require('electron') const {getAllWebContents} = process.atomBinding('web_contents') const renderProcessPreferences = process.atomBinding('render_process_preferences').forAllWebContents() const {Buffer} = require('buffer') const fs = require('fs') const path = require('path') const url = require('url') // TODO(zcbenz): Remove this when we have Object.values(). const objectValues = function (object) { return Object.keys(object).map(function (key) { return object[key] }) } // Mapping between extensionId(hostname) and manifest. const manifestMap = {} // extensionId => manifest const manifestNameMap = {} // name => manifest const devToolsExtensionNames = new Set() const generateExtensionIdFromName = function (name) { return name.replace(/[\W_]+/g, '-').toLowerCase() } const isWindowOrWebView = function (webContents) { const type = webContents.getType() return type === 'window' || type === 'webview' } // Create or get manifest object from |srcDirectory|. const getManifestFromPath = function (srcDirectory) { let manifest let manifestContent try { manifestContent = fs.readFileSync(path.join(srcDirectory, 'manifest.json')) } catch (readError) { console.warn(`Reading ${path.join(srcDirectory, 'manifest.json')} failed.`) console.warn(readError.stack || readError) throw readError } try { manifest = JSON.parse(manifestContent) } catch (parseError) { console.warn(`Parsing ${path.join(srcDirectory, 'manifest.json')} failed.`) console.warn(parseError.stack || parseError) throw parseError } if (!manifestNameMap[manifest.name]) { const extensionId = generateExtensionIdFromName(manifest.name) manifestMap[extensionId] = manifestNameMap[manifest.name] = manifest Object.assign(manifest, { srcDirectory: srcDirectory, extensionId: extensionId, // We can not use 'file://' directly because all resources in the extension // will be treated as relative to the root in Chrome. startPage: url.format({ protocol: 'chrome-extension', slashes: true, hostname: extensionId, pathname: manifest.devtools_page }) }) return manifest } else if (manifest && manifest.name) { console.warn(`Attempted to load extension "${manifest.name}" that has already been loaded.`) return manifest } } // Manage the background pages. const backgroundPages = {} const startBackgroundPages = function (manifest) { if (backgroundPages[manifest.extensionId] || !manifest.background) return let html let name if (manifest.background.page) { name = manifest.background.page html = fs.readFileSync(path.join(manifest.srcDirectory, manifest.background.page)) } else { name = '_generated_background_page.html' const scripts = manifest.background.scripts.map((name) => { return `` }).join('') html = Buffer.from(`${scripts}`) } const contents = webContents.create({ partition: 'persist:__chrome_extension', isBackgroundPage: true, commandLineSwitches: ['--background-page'] }) backgroundPages[manifest.extensionId] = { html: html, webContents: contents, name: name } contents.loadURL(url.format({ protocol: 'chrome-extension', slashes: true, hostname: manifest.extensionId, pathname: name })) } const removeBackgroundPages = function (manifest) { if (!backgroundPages[manifest.extensionId]) return backgroundPages[manifest.extensionId].webContents.destroy() delete backgroundPages[manifest.extensionId] } const sendToBackgroundPages = function (...args) { for (const page of objectValues(backgroundPages)) { page.webContents.sendToAll(...args) } } // Dispatch web contents events to Chrome APIs const hookWebContentsEvents = function (webContents) { const tabId = webContents.id sendToBackgroundPages('CHROME_TABS_ONCREATED') webContents.on('will-navigate', (event, url) => { sendToBackgroundPages('CHROME_WEBNAVIGATION_ONBEFORENAVIGATE', { frameId: 0, parentFrameId: -1, processId: webContents.getProcessId(), tabId: tabId, timeStamp: Date.now(), url: url }) }) webContents.on('did-navigate', (event, url) => { sendToBackgroundPages('CHROME_WEBNAVIGATION_ONCOMPLETED', { frameId: 0, parentFrameId: -1, processId: webContents.getProcessId(), tabId: tabId, timeStamp: Date.now(), url: url }) }) webContents.once('destroyed', () => { sendToBackgroundPages('CHROME_TABS_ONREMOVED', tabId) }) } // Handle the chrome.* API messages. let nextId = 0 ipcMain.on('CHROME_RUNTIME_CONNECT', function (event, extensionId, connectInfo) { const page = backgroundPages[extensionId] if (!page) { console.error(`Connect to unknown extension ${extensionId}`) return } const portId = ++nextId event.returnValue = {tabId: page.webContents.id, portId: portId} event.sender.once('render-view-deleted', () => { if (page.webContents.isDestroyed()) return page.webContents.sendToAll(`CHROME_PORT_DISCONNECT_${portId}`) }) page.webContents.sendToAll(`CHROME_RUNTIME_ONCONNECT_${extensionId}`, event.sender.id, portId, connectInfo) }) ipcMain.on('CHROME_I18N_MANIFEST', function (event, extensionId) { event.returnValue = manifestMap[extensionId] }) let resultID = 1 ipcMain.on('CHROME_RUNTIME_SENDMESSAGE', function (event, extensionId, message, originResultID) { const page = backgroundPages[extensionId] if (!page) { console.error(`Connect to unknown extension ${extensionId}`) return } page.webContents.sendToAll(`CHROME_RUNTIME_ONMESSAGE_${extensionId}`, event.sender.id, message, resultID) ipcMain.once(`CHROME_RUNTIME_ONMESSAGE_RESULT_${resultID}`, (event, result) => { event.sender.send(`CHROME_RUNTIME_SENDMESSAGE_RESULT_${originResultID}`, result) }) resultID++ }) ipcMain.on('CHROME_TABS_SEND_MESSAGE', function (event, tabId, extensionId, isBackgroundPage, message, originResultID) { const contents = webContents.fromId(tabId) if (!contents) { console.error(`Sending message to unknown tab ${tabId}`) return } const senderTabId = isBackgroundPage ? null : event.sender.id contents.sendToAll(`CHROME_RUNTIME_ONMESSAGE_${extensionId}`, senderTabId, message, resultID) ipcMain.once(`CHROME_RUNTIME_ONMESSAGE_RESULT_${resultID}`, (event, result) => { event.sender.send(`CHROME_TABS_SEND_MESSAGE_RESULT_${originResultID}`, result) }) resultID++ }) ipcMain.on('CHROME_TABS_EXECUTESCRIPT', function (event, requestId, tabId, extensionId, details) { const contents = webContents.fromId(tabId) if (!contents) { console.error(`Sending message to unknown tab ${tabId}`) return } let code, url if (details.file) { const manifest = manifestMap[extensionId] code = String(fs.readFileSync(path.join(manifest.srcDirectory, details.file))) url = `chrome-extension://${extensionId}${details.file}` } else { code = details.code url = `chrome-extension://${extensionId}/${String(Math.random()).substr(2, 8)}.js` } contents.send('CHROME_TABS_EXECUTESCRIPT', event.sender.id, requestId, extensionId, url, code) }) // Transfer the content scripts to renderer. const contentScripts = {} const injectContentScripts = function (manifest) { if (contentScripts[manifest.name] || !manifest.content_scripts) return const readArrayOfFiles = function (relativePath) { return { url: `chrome-extension://${manifest.extensionId}/${relativePath}`, code: String(fs.readFileSync(path.join(manifest.srcDirectory, relativePath))) } } const contentScriptToEntry = function (script) { return { matches: script.matches, js: script.js ? script.js.map(readArrayOfFiles) : [], css: script.css ? script.css.map(readArrayOfFiles) : [], runAt: script.run_at || 'document_idle' } } try { const entry = { extensionId: manifest.extensionId, contentScripts: manifest.content_scripts.map(contentScriptToEntry) } contentScripts[manifest.name] = renderProcessPreferences.addEntry(entry) } catch (e) { console.error('Failed to read content scripts', e) } } const removeContentScripts = function (manifest) { if (!contentScripts[manifest.name]) return renderProcessPreferences.removeEntry(contentScripts[manifest.name]) delete contentScripts[manifest.name] } // Transfer the |manifest| to a format that can be recognized by the // |DevToolsAPI.addExtensions|. const manifestToExtensionInfo = function (manifest) { return { startPage: manifest.startPage, srcDirectory: manifest.srcDirectory, name: manifest.name, exposeExperimentalAPIs: true } } // Load the extensions for the window. const loadExtension = function (manifest) { startBackgroundPages(manifest) injectContentScripts(manifest) } const loadDevToolsExtensions = function (win, manifests) { if (!win.devToolsWebContents) return manifests.forEach(loadExtension) const extensionInfoArray = manifests.map(manifestToExtensionInfo) extensionInfoArray.forEach((extension) => { win.devToolsWebContents._grantOriginAccess(extension.startPage) }) win.devToolsWebContents.executeJavaScript(`DevToolsAPI.addExtensions(${JSON.stringify(extensionInfoArray)})`) } app.on('web-contents-created', function (event, webContents) { if (!isWindowOrWebView(webContents)) return hookWebContentsEvents(webContents) webContents.on('devtools-opened', function () { loadDevToolsExtensions(webContents, objectValues(manifestMap)) }) }) // The chrome-extension: can map a extension URL request to real file path. const chromeExtensionHandler = function (request, callback) { const parsed = url.parse(request.url) if (!parsed.hostname || !parsed.path) return callback() const manifest = manifestMap[parsed.hostname] if (!manifest) return callback() const page = backgroundPages[parsed.hostname] if (page && parsed.path === `/${page.name}`) { return callback({ mimeType: 'text/html', data: page.html }) } fs.readFile(path.join(manifest.srcDirectory, parsed.path), function (err, content) { if (err) { return callback(-6) // FILE_NOT_FOUND } else { return callback(content) } }) } app.on('session-created', function (ses) { ses.protocol.registerBufferProtocol('chrome-extension', chromeExtensionHandler, function (error) { if (error) { console.error(`Unable to register chrome-extension protocol: ${error}`) } }) }) // The persistent path of "DevTools Extensions" preference file. let loadedDevToolsExtensionsPath = null app.on('will-quit', function () { try { const loadedDevToolsExtensions = Array.from(devToolsExtensionNames) .map(name => manifestNameMap[name].srcDirectory) if (loadedDevToolsExtensions.length > 0) { try { fs.mkdirSync(path.dirname(loadedDevToolsExtensionsPath)) } catch (error) { // Ignore error } fs.writeFileSync(loadedDevToolsExtensionsPath, JSON.stringify(loadedDevToolsExtensions)) } else { fs.unlinkSync(loadedDevToolsExtensionsPath) } } catch (error) { // Ignore error } }) // We can not use protocol or BrowserWindow until app is ready. app.once('ready', function () { // Load persisted extensions. loadedDevToolsExtensionsPath = path.join(app.getPath('userData'), 'DevTools Extensions') try { const loadedDevToolsExtensions = JSON.parse(fs.readFileSync(loadedDevToolsExtensionsPath)) if (Array.isArray(loadedDevToolsExtensions)) { for (const srcDirectory of loadedDevToolsExtensions) { // Start background pages and set content scripts. BrowserWindow.addDevToolsExtension(srcDirectory) } } } catch (error) { // Ignore error } // The public API to add/remove extensions. BrowserWindow.addExtension = function (srcDirectory) { const manifest = getManifestFromPath(srcDirectory) if (manifest) { loadExtension(manifest) for (const webContents of getAllWebContents()) { if (isWindowOrWebView(webContents)) { loadDevToolsExtensions(webContents, [manifest]) } } return manifest.name } } BrowserWindow.removeExtension = function (name) { const manifest = manifestNameMap[name] if (!manifest) return removeBackgroundPages(manifest) removeContentScripts(manifest) delete manifestMap[manifest.extensionId] delete manifestNameMap[name] } BrowserWindow.getExtensions = function () { const extensions = {} Object.keys(manifestNameMap).forEach(function (name) { const manifest = manifestNameMap[name] extensions[name] = {name: manifest.name, version: manifest.version} }) return extensions } BrowserWindow.addDevToolsExtension = function (srcDirectory) { const manifestName = BrowserWindow.addExtension(srcDirectory) if (manifestName) { devToolsExtensionNames.add(manifestName) } return manifestName } BrowserWindow.removeDevToolsExtension = function (name) { BrowserWindow.removeExtension(name) devToolsExtensionNames.delete(name) } BrowserWindow.getDevToolsExtensions = function () { const extensions = BrowserWindow.getExtensions() const devExtensions = {} Array.from(devToolsExtensionNames).forEach(function (name) { if (!extensions[name]) return devExtensions[name] = extensions[name] }) return devExtensions } }) 'use strict' const {ipcMain} = require('electron') const {desktopCapturer} = process.atomBinding('desktop_capturer') const deepEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b) // A queue for holding all requests from renderer process. 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) => { const request = { id, options: { captureWindow, captureScreen, thumbnailSize }, webContents: event.sender } requestsQueue.push(request) if (requestsQueue.length === 1) { desktopCapturer.startHandling(captureWindow, captureScreen, thumbnailSize) } // If the WebContents is destroyed before receiving result, just remove the // reference from requestsQueue to make the module not send the result to it. event.sender.once('destroyed', () => { request.webContents = null }) }) desktopCapturer.emit = (event, name, sources) => { // 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 => { return { id: source.id, name: source.name, thumbnail: source.thumbnail.toDataURL() } }) if (handledWebContents) { handledWebContents.send(capturerResult(handledRequest.id), result) } // Check the queue to see whether there is another identical request & handle requestsQueue.forEach(request => { const webContents = request.webContents if (deepEqual(handledRequest.options, request.options)) { if (webContents) webContents.send(capturerResult(request.id), result) } else { unhandledRequestsQueue.push(request) } }) requestsQueue = unhandledRequestsQueue // 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) } } 'use strict' const {ipcMain, webContents} = require('electron') const parseFeaturesString = require('../common/parse-features-string') // Doesn't exist in early initialization. let webViewManager = null const supportedWebViewEvents = [ 'load-commit', 'did-attach', 'did-finish-load', 'did-fail-load', 'did-frame-finish-load', 'did-start-loading', 'did-stop-loading', 'did-get-response-details', 'did-get-redirect-request', 'dom-ready', 'console-message', 'context-menu', 'devtools-opened', 'devtools-closed', 'devtools-focused', 'new-window', 'will-navigate', 'did-navigate', 'did-navigate-in-page', 'close', 'crashed', 'gpu-crashed', 'plugin-crashed', 'destroyed', 'page-title-updated', 'page-favicon-updated', 'enter-html-full-screen', 'leave-html-full-screen', 'media-started-playing', 'media-paused', 'found-in-page', 'did-change-theme-color', 'update-target-url' ] let nextGuestInstanceId = 0 const guestInstances = {} const embedderElementsMap = {} // Moves the last element of array to the first one. const moveLastToFirst = function (list) { list.unshift(list.pop()) } // Generate guestInstanceId. const getNextGuestInstanceId = function () { return ++nextGuestInstanceId } // Create a new guest instance. const createGuest = function (embedder, params) { if (webViewManager == null) { webViewManager = process.atomBinding('web_view_manager') } const guestInstanceId = getNextGuestInstanceId(embedder) const guest = webContents.create({ isGuest: true, partition: params.partition, embedder: embedder }) guestInstances[guestInstanceId] = { guest: guest, embedder: embedder } watchEmbedder(embedder) // Init guest web view after attached. guest.on('did-attach', function (event) { params = this.attachParams delete this.attachParams const previouslyAttached = this.viewInstanceId != null this.viewInstanceId = params.instanceId // Only load URL and set size on first attach if (previouslyAttached) { return } this.setSize({ normal: { width: params.elementWidth, height: params.elementHeight }, enableAutoSize: params.autosize, min: { width: params.minwidth, height: params.minheight }, max: { width: params.maxwidth, height: params.maxheight } }) if (params.src) { const opts = {} if (params.httpreferrer) { opts.httpReferrer = params.httpreferrer } if (params.useragent) { opts.userAgent = params.useragent } this.loadURL(params.src, opts) } guest.allowPopups = params.allowpopups embedder.emit('did-attach-webview', event, guest) }) const sendToEmbedder = (channel, ...args) => { const embedder = getEmbedder(guestInstanceId) if (embedder != null) { embedder.send(`${channel}-${guest.viewInstanceId}`, ...args) } } // Dispatch events to embedder. const fn = function (event) { guest.on(event, function (_, ...args) { sendToEmbedder('ELECTRON_GUEST_VIEW_INTERNAL_DISPATCH_EVENT', event, ...args) }) } for (const event of supportedWebViewEvents) { fn(event) } // Dispatch guest's IPC messages to embedder. guest.on('ipc-message-host', function (_, [channel, ...args]) { sendToEmbedder('ELECTRON_GUEST_VIEW_INTERNAL_IPC_MESSAGE', channel, ...args) }) // Autosize. guest.on('size-changed', function (_, ...args) { sendToEmbedder('ELECTRON_GUEST_VIEW_INTERNAL_SIZE_CHANGED', ...args) }) // Notify guest of embedder window visibility when it is ready // FIXME Remove once https://github.com/electron/electron/issues/6828 is fixed guest.on('dom-ready', function () { const guestInstance = guestInstances[guestInstanceId] if (guestInstance != null && guestInstance.visibilityState != null) { guest.send('ELECTRON_GUEST_INSTANCE_VISIBILITY_CHANGE', guestInstance.visibilityState) } }) // Forward internal web contents event to embedder to handle // native window.open setup guest.on('-add-new-contents', (...args) => { if (guest.getLastWebPreferences().nativeWindowOpen === true) { const embedder = getEmbedder(guestInstanceId) if (embedder != null) { embedder.emit('-add-new-contents', ...args) } } }) 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 } // Attach the guest to an element of embedder. const attachGuest = function (event, elementInstanceId, guestInstanceId, params) { const embedder = event.sender // Destroy the old guest when attaching. const key = `${embedder.getId()}-${elementInstanceId}` const oldGuestInstanceId = embedderElementsMap[key] if (oldGuestInstanceId != null) { // Reattachment to the same guest is just a no-op. if (oldGuestInstanceId === guestInstanceId) { return } destroyGuest(embedder, oldGuestInstanceId) } const guestInstance = guestInstances[guestInstanceId] // If this isn't a valid guest instance then do nothing. if (!guestInstance) { return } const {guest} = guestInstance // If this guest is already attached to an element then remove it if (guestInstance.elementInstanceId) { const oldKey = `${guestInstance.embedder.getId()}-${guestInstance.elementInstanceId}` delete embedderElementsMap[oldKey] // Remove guest from embedder if moving across web views if (guest.viewInstanceId !== params.instanceId) { webViewManager.removeGuest(guestInstance.embedder, guestInstanceId) guestInstance.embedder.send(`ELECTRON_GUEST_VIEW_INTERNAL_DESTROY_GUEST-${guest.viewInstanceId}`) } } const webPreferences = { guestInstanceId: guestInstanceId, nodeIntegration: params.nodeintegration != null ? params.nodeintegration : false, plugins: params.plugins, zoomFactor: embedder._getZoomFactor(), webSecurity: !params.disablewebsecurity, blinkFeatures: params.blinkfeatures, disableBlinkFeatures: params.disableblinkfeatures } // parse the 'webpreferences' attribute string, if set // this uses the same parsing rules as window.open uses for its features if (typeof params.webpreferences === 'string') { parseFeaturesString(params.webpreferences, function (key, value) { if (value === undefined) { // no value was specified, default it to true value = true } webPreferences[key] = value }) } if (params.preload) { webPreferences.preloadURL = params.preload } // Return null from native window.open if allowpopups is unset if (webPreferences.nativeWindowOpen === true && !params.allowpopups) { webPreferences.disablePopups = true } embedder.emit('will-attach-webview', event, webPreferences, params) if (event.defaultPrevented) { if (guest.viewInstanceId == null) guest.viewInstanceId = params.instanceId destroyGuest(embedder, guestInstanceId) return } webViewManager.addGuest(guestInstanceId, elementInstanceId, embedder, guest, webPreferences) guest.attachParams = params embedderElementsMap[key] = guestInstanceId guest.setEmbedder(embedder) guestInstance.embedder = embedder guestInstance.elementInstanceId = elementInstanceId watchEmbedder(embedder) } // Destroy an existing guest instance. const destroyGuest = function (embedder, guestInstanceId) { if (!(guestInstanceId in guestInstances)) { return } const guestInstance = guestInstances[guestInstanceId] if (embedder !== guestInstance.embedder) { return } webViewManager.removeGuest(embedder, guestInstanceId) guestInstance.guest.destroy() delete guestInstances[guestInstanceId] const key = `${embedder.getId()}-${guestInstance.elementInstanceId}` delete embedderElementsMap[key] } // Once an embedder has had a guest attached we watch it for destruction to // destroy any remaining guests. const watchedEmbedders = new Set() const watchEmbedder = function (embedder) { if (watchedEmbedders.has(embedder)) { return } watchedEmbedders.add(embedder) // Forward embedder window visiblity change events to guest const onVisibilityChange = function (visibilityState) { for (const guestInstanceId of Object.keys(guestInstances)) { const guestInstance = guestInstances[guestInstanceId] guestInstance.visibilityState = visibilityState if (guestInstance.embedder === embedder) { guestInstance.guest.send('ELECTRON_GUEST_INSTANCE_VISIBILITY_CHANGE', visibilityState) } } } embedder.on('-window-visibility-change', onVisibilityChange) const destroyEvents = ['will-destroy', 'crashed', 'did-navigate'] const destroy = function () { for (const guestInstanceId of Object.keys(guestInstances)) { if (guestInstances[guestInstanceId].embedder === embedder) { destroyGuest(embedder, parseInt(guestInstanceId)) } } for (const event of destroyEvents) { embedder.removeListener(event, destroy) } embedder.removeListener('-window-visibility-change', onVisibilityChange) watchedEmbedders.delete(embedder) } for (const event of destroyEvents) { embedder.once(event, destroy) // Users might also listen to the crashed event, so we must ensure the guest // is destroyed before users' listener gets called. It is done by moving our // listener to the first one in queue. const listeners = embedder._events[event] if (Array.isArray(listeners)) { moveLastToFirst(listeners) } } } ipcMain.on('ELECTRON_GUEST_VIEW_MANAGER_CREATE_GUEST', function (event, params, requestId) { event.sender.send(`ELECTRON_RESPONSE_${requestId}`, createGuest(event.sender, params)) }) ipcMain.on('ELECTRON_GUEST_VIEW_MANAGER_ATTACH_GUEST', function (event, elementInstanceId, guestInstanceId, params) { attachGuest(event, elementInstanceId, guestInstanceId, params) }) ipcMain.on('ELECTRON_GUEST_VIEW_MANAGER_DESTROY_GUEST', function (event, guestInstanceId) { destroyGuest(event.sender, guestInstanceId) }) ipcMain.on('ELECTRON_GUEST_VIEW_MANAGER_SET_SIZE', function (event, guestInstanceId, params) { const guest = getGuest(guestInstanceId) if (guest != null) guest.setSize(params) }) // Returns WebContents from its guest id. const getGuest = function (guestInstanceId) { const guestInstance = guestInstances[guestInstanceId] if (guestInstance != null) return guestInstance.guest } // Returns the embedder of the guest. const getEmbedder = function (guestInstanceId) { const guestInstance = guestInstances[guestInstanceId] if (guestInstance != null) return guestInstance.embedder } exports.getGuest = getGuest exports.getEmbedder = getEmbedder 'use strict' const {BrowserWindow, ipcMain, webContents} = require('electron') const {isSameOrigin} = process.atomBinding('v8_util') const parseFeaturesString = require('../common/parse-features-string') const hasProp = {}.hasOwnProperty const frameToGuest = new Map() // Security options that child windows will always inherit from parent windows const inheritedWebPreferences = new Map([ ['contextIsolation', true], ['javascript', false], ['nativeWindowOpen', true], ['nodeIntegration', false], ['sandbox', true], ['webviewTag', false] ]) // Copy attribute of |parent| to |child| if it is not defined in |child|. const mergeOptions = function (child, parent, visited) { // Check for circular reference. if (visited == null) visited = new Set() if (visited.has(parent)) return visited.add(parent) for (const key in parent) { if (!hasProp.call(parent, key)) continue if (key in child && key !== 'webPreferences') continue const value = parent[key] if (typeof value === 'object') { child[key] = mergeOptions(child[key] || {}, value, visited) } else { child[key] = value } } visited.delete(parent) return child } // Merge |options| with the |embedder|'s window's options. const mergeBrowserWindowOptions = function (embedder, options) { if (options.webPreferences == null) { options.webPreferences = {} } if (embedder.browserWindowOptions != null) { let parentOptions = embedder.browserWindowOptions // if parent's visibility is available, that overrides 'show' flag (#12125) const win = BrowserWindow.fromWebContents(embedder.webContents) if (win != null) { parentOptions = Object.assign({}, embedder.browserWindowOptions, {show: win.isVisible()}) } // Inherit the original options if it is a BrowserWindow. mergeOptions(options, parentOptions) } else { // Or only inherit webPreferences if it is a webview. mergeOptions(options.webPreferences, embedder.getLastWebPreferences()) } // Inherit certain option values from parent window for (const [name, value] of inheritedWebPreferences) { if (embedder.getLastWebPreferences()[name] === value) { options.webPreferences[name] = value } } // Sets correct openerId here to give correct options to 'new-window' event handler options.webPreferences.openerId = embedder.id return options } // Setup a new guest with |embedder| const setupGuest = function (embedder, frameName, guest, options) { // When |embedder| is destroyed we should also destroy attached guest, and if // guest is closed by user then we should prevent |embedder| from double // closing guest. const guestId = guest.webContents.id const closedByEmbedder = function () { guest.removeListener('closed', closedByUser) guest.destroy() } const closedByUser = function () { embedder.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_CLOSED_' + guestId) embedder.removeListener('render-view-deleted', closedByEmbedder) } embedder.once('render-view-deleted', closedByEmbedder) guest.once('closed', closedByUser) if (frameName) { frameToGuest.set(frameName, guest) guest.frameName = frameName guest.once('closed', function () { frameToGuest.delete(frameName) }) } return guestId } // Create a new guest created by |embedder| with |options|. const createGuest = function (embedder, url, frameName, options, postData) { let guest = frameToGuest.get(frameName) if (frameName && (guest != null)) { guest.loadURL(url) return guest.webContents.id } // Remember the embedder window's id. if (options.webPreferences == null) { options.webPreferences = {} } guest = new BrowserWindow(options) if (!options.webContents || url !== 'about:blank') { // 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'. // // 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. const loadOptions = {} if (postData != null) { loadOptions.postData = postData loadOptions.extraHeaders = 'content-type: application/x-www-form-urlencoded' if (postData.length > 0) { const postDataFront = postData[0].bytes.toString() const boundary = /^--.*[^-\r\n]/.exec(postDataFront) if (boundary != null) { loadOptions.extraHeaders = `content-type: multipart/form-data; boundary=${boundary[0].substr(2)}` } } } guest.loadURL(url, loadOptions) } return setupGuest(embedder, frameName, guest, options) } const getGuestWindow = function (guestContents) { let guestWindow = BrowserWindow.fromWebContents(guestContents) if (guestWindow == null) { const hostContents = guestContents.hostWebContents if (hostContents != null) { guestWindow = BrowserWindow.fromWebContents(hostContents) } } return guestWindow } // Checks whether |sender| can access the |target|: // 1. Check whether |sender| is the parent of |target|. // 2. Check whether |sender| has node integration, if so it is allowed to // do anything it wants. // 3. Check whether the origins match. // // However it allows a child window without node integration but with same // origin to do anything it wants, when its opener window has node integration. // The W3C does not have anything on this, but from my understanding of the // security model of |window.opener|, this should be fine. const canAccessWindow = function (sender, target) { return (target.getLastWebPreferences().openerId === sender.id) || (sender.getLastWebPreferences().nodeIntegration === true) || isSameOrigin(sender.getURL(), target.getURL()) } // Routed window.open messages with raw options ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN', (event, url, frameName, features) => { if (url == null || url === '') url = 'about:blank' if (frameName == null) frameName = '' if (features == null) features = '' const options = {} const ints = ['x', 'y', 'width', 'height', 'minWidth', 'maxWidth', 'minHeight', 'maxHeight', 'zoomFactor'] const webPreferences = ['zoomFactor', 'nodeIntegration', 'preload', 'javascript', 'contextIsolation', 'webviewTag'] const disposition = 'new-window' // Used to store additional features const additionalFeatures = [] // Parse the features parseFeaturesString(features, function (key, value) { if (value === undefined) { additionalFeatures.push(key) } else { // Don't allow webPreferences to be set since it must be an object // that cannot be directly overridden if (key === 'webPreferences') return if (webPreferences.includes(key)) { if (options.webPreferences == null) { options.webPreferences = {} } options.webPreferences[key] = value } else { options[key] = value } } }) if (options.left) { if (options.x == null) { options.x = options.left } } if (options.top) { if (options.y == null) { options.y = options.top } } if (options.title == null) { options.title = frameName } if (options.width == null) { options.width = 800 } if (options.height == null) { options.height = 600 } for (const name of ints) { if (options[name] != null) { options[name] = parseInt(options[name], 10) } } ipcMain.emit('ELECTRON_GUEST_WINDOW_MANAGER_INTERNAL_WINDOW_OPEN', event, url, frameName, disposition, options, additionalFeatures) }) // Routed window.open messages with fully parsed options ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_INTERNAL_WINDOW_OPEN', function (event, url, frameName, disposition, options, additionalFeatures, postData) { options = mergeBrowserWindowOptions(event.sender, options) event.sender.emit('new-window', event, url, frameName, disposition, options, additionalFeatures) const {newGuest} = event if ((event.sender.isGuest() && !event.sender.allowPopups) || event.defaultPrevented) { if (newGuest != null) { if (options.webContents === newGuest.webContents) { // the webContents is not changed, so set defaultPrevented to false to // stop the callers of this event from destroying the webContents. event.defaultPrevented = false } event.returnValue = setupGuest(event.sender, frameName, newGuest, options) } else { event.returnValue = null } } else { event.returnValue = createGuest(event.sender, url, frameName, options, postData) } }) ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_CLOSE', function (event, guestId) { const guestContents = webContents.fromId(guestId) if (guestContents == null) return if (!canAccessWindow(event.sender, guestContents)) { console.error(`Blocked ${event.sender.getURL()} from closing its opener.`) return } const guestWindow = getGuestWindow(guestContents) if (guestWindow != null) guestWindow.destroy() }) ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_METHOD', function (event, guestId, method, ...args) { const guestContents = webContents.fromId(guestId) if (guestContents == null) { event.returnValue = null return } if (!canAccessWindow(event.sender, guestContents)) { console.error(`Blocked ${event.sender.getURL()} from calling ${method} on its opener.`) event.returnValue = null return } const guestWindow = getGuestWindow(guestContents) if (guestWindow != null) { event.returnValue = guestWindow[method](...args) } else { event.returnValue = null } }) ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_POSTMESSAGE', function (event, guestId, message, targetOrigin, sourceOrigin) { if (targetOrigin == null) { targetOrigin = '*' } const guestContents = webContents.fromId(guestId) if (guestContents == null) return // The W3C does not seem to have word on how postMessage should work when the // origins do not match, so we do not do |canAccessWindow| check here since // postMessage across origins is useful and not harmful. if (targetOrigin === '*' || isSameOrigin(guestContents.getURL(), targetOrigin)) { const sourceId = event.sender.id guestContents.send('ELECTRON_GUEST_WINDOW_POSTMESSAGE', sourceId, message, sourceOrigin) } }) 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)) { guestContents[method](...args) } else { console.error(`Blocked ${event.sender.getURL()} from calling ${method} on its opener.`) } }) ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD_SYNC', function (event, guestId, method, ...args) { const guestContents = webContents.fromId(guestId) if (guestContents == null) { event.returnValue = null return } if (canAccessWindow(event.sender, guestContents)) { event.returnValue = guestContents[method](...args) } else { console.error(`Blocked ${event.sender.getURL()} from calling ${method} on its opener.`) event.returnValue = null } }) 'use strict' const {Buffer} = require('buffer') const fs = require('fs') const path = require('path') const util = require('util') const Module = require('module') const v8 = require('v8') // We modified the original process.argv to let node.js load the atom.js, // we need to restore it here. process.argv.splice(1, 1) // Clear search paths. require('../common/reset-search-paths') // Import common settings. require('../common/init') var globalPaths = Module.globalPaths // Expose public APIs. globalPaths.push(path.join(__dirname, 'api', 'exports')) if (process.platform === 'win32') { // Redirect node's console to use our own implementations, since node can not // handle console output when running as GUI program. var consoleLog = function (...args) { return process.log(util.format(...args) + '\n') } var streamWrite = function (chunk, encoding, callback) { if (Buffer.isBuffer(chunk)) { chunk = chunk.toString(encoding) } process.log(chunk) if (callback) { callback() } return true } console.log = console.error = console.warn = consoleLog process.stdout.write = process.stderr.write = streamWrite } // Don't quit on fatal error. process.on('uncaughtException', function (error) { // Do nothing if the user has a custom uncaught exception handler. var dialog, message, ref, stack if (process.listeners('uncaughtException').length > 1) { return } // Show error in GUI. dialog = require('electron').dialog stack = (ref = error.stack) != null ? ref : error.name + ': ' + error.message message = 'Uncaught Exception:\n' + stack dialog.showErrorBox('A JavaScript error occurred in the main process', message) }) // Emit 'exit' event on quit. const {app} = require('electron') app.on('quit', function (event, exitCode) { process.emit('exit', exitCode) }) if (process.platform === 'win32') { // If we are a Squirrel.Windows-installed app, set app user model ID // so that users don't have to do this. // // Squirrel packages are always of the form: // // PACKAGE-NAME // - Update.exe // - app-VERSION // - OUREXE.exe // // Squirrel itself will always set the shortcut's App User Model ID to the // form `com.squirrel.PACKAGE-NAME.OUREXE`. We need to call // app.setAppUserModelId with a matching identifier so that renderer processes // will inherit this value. const updateDotExe = path.join(path.dirname(process.execPath), '..', 'update.exe') if (fs.existsSync(updateDotExe)) { const packageDir = path.dirname(path.resolve(updateDotExe)) const packageName = path.basename(packageDir).replace(/\s/g, '') const exeName = path.basename(process.execPath).replace(/\.exe$/i, '').replace(/\s/g, '') app.setAppUserModelId(`com.squirrel.${packageName}.${exeName}`) } } // Map process.exit to app.exit, which quits gracefully. process.exit = app.exit // Load the RPC server. require('./rpc-server') // Load the guest view manager. require('./guest-view-manager') require('./guest-window-manager') // Now we try to load app's package.json. let packagePath = null let packageJson = null const searchPaths = ['app', 'app.asar', 'default_app.asar'] for (packagePath of searchPaths) { try { packagePath = path.join(process.resourcesPath, packagePath) packageJson = require(path.join(packagePath, 'package.json')) break } catch (error) { continue } } if (packageJson == null) { process.nextTick(function () { return process.exit(1) }) throw new Error('Unable to find a valid app') } // Set application's version. if (packageJson.version != null) { app.setVersion(packageJson.version) } // Set application's name. if (packageJson.productName != null) { app.setName(packageJson.productName) } else if (packageJson.name != null) { app.setName(packageJson.name) } // Set application's desktop name. if (packageJson.desktopName != null) { app.setDesktopName(packageJson.desktopName) } else { app.setDesktopName((app.getName()) + '.desktop') } // Set v8 flags if (packageJson.v8Flags != null) { v8.setFlagsFromString(packageJson.v8Flags) } // Set the user path according to application's name. app.setPath('userData', path.join(app.getPath('appData'), app.getName())) app.setPath('userCache', path.join(app.getPath('cache'), app.getName())) app.setAppPath(packagePath) // Load the chrome extension support. require('./chrome-extension') // Load internal desktop-capturer module. require('./desktop-capturer') // Load protocol module to ensure it is populated on app ready require('./api/protocol') // Set main startup script of the app. const mainStartupScript = packageJson.main || 'index.js' // Workaround for electron/electron#5050 and electron/electron#9046 if (process.platform === 'linux' && ['Pantheon', 'Unity:Unity7', 'ubuntu:GNOME'].includes(process.env.XDG_CURRENT_DESKTOP)) { process.env.XDG_CURRENT_DESKTOP = 'Unity' } // Finally load app's main.js and transfer control to C++. Module._load(path.join(packagePath, mainStartupScript), Module, true) 'use strict' const v8Util = process.atomBinding('v8_util') class ObjectsRegistry { constructor () { this.nextId = 0 // Stores all objects by ref-counting. // (id) => {object, count} this.storage = {} // Stores the IDs of objects referenced by WebContents. // (webContentsId) => [id] this.owners = {} } // Register a new object and return its assigned ID. If the object is already // registered then the already assigned ID would be returned. add (webContents, obj) { // Get or assign an ID to the object. const id = this.saveToStorage(obj) // Add object to the set of referenced objects. const webContentsId = webContents.getId() let owner = this.owners[webContentsId] if (!owner) { owner = this.owners[webContentsId] = new Set() this.registerDeleteListener(webContents, webContentsId) } if (!owner.has(id)) { owner.add(id) // Increase reference count if not referenced before. this.storage[id].count++ } return id } // Get an object according to its ID. get (id) { const pointer = this.storage[id] if (pointer != null) return pointer.object } // Dereference an object according to its ID. remove (webContentsId, id) { // Dereference from the storage. this.dereference(id) // Also remove the reference in owner. let owner = this.owners[webContentsId] if (owner) { owner.delete(id) } } // Clear all references to objects refrenced by the WebContents. clear (webContentsId) { let owner = this.owners[webContentsId] if (!owner) return for (let id of owner) this.dereference(id) delete this.owners[webContentsId] } // Private: Saves the object into storage and assigns an ID for it. saveToStorage (object) { let id = v8Util.getHiddenValue(object, 'atomId') if (!id) { id = ++this.nextId this.storage[id] = { count: 0, object: object } v8Util.setHiddenValue(object, 'atomId', id) } return id } // Private: Dereference the object from store. dereference (id) { let pointer = this.storage[id] if (pointer == null) { return } pointer.count -= 1 if (pointer.count === 0) { v8Util.deleteHiddenValue(pointer.object, 'atomId') delete this.storage[id] } } // Private: Clear the storage when webContents is reloaded/navigated. registerDeleteListener (webContents, webContentsId) { const processId = webContents.getProcessId() const listener = (event, deletedProcessId) => { if (deletedProcessId === processId) { webContents.removeListener('render-view-deleted', listener) this.clear(webContentsId) } } webContents.on('render-view-deleted', listener) } } module.exports = new ObjectsRegistry() 'use strict' const {Buffer} = require('buffer') const electron = require('electron') const v8Util = process.atomBinding('v8_util') const {WebContents} = process.atomBinding('web_contents') const {ipcMain, isPromise, webContents} = electron const objectsRegistry = require('./objects-registry') const hasProp = {}.hasOwnProperty // The internal properties of Function. const FUNCTION_PROPERTIES = [ 'length', 'name', 'arguments', 'caller', 'prototype' ] // The remote functions in renderer processes. // id => Function let rendererFunctions = v8Util.createDoubleIDWeakMap() // Return the description of object's members: let getObjectMembers = function (object) { let names = Object.getOwnPropertyNames(object) // For Function, we should not override following properties even though they // are "own" properties. if (typeof object === 'function') { names = names.filter((name) => { return !FUNCTION_PROPERTIES.includes(name) }) } // Map properties to descriptors. return names.map((name) => { let descriptor = Object.getOwnPropertyDescriptor(object, name) let member = {name, enumerable: descriptor.enumerable, writable: false} if (descriptor.get === undefined && typeof object[name] === 'function') { member.type = 'method' } else { if (descriptor.set || descriptor.writable) member.writable = true member.type = 'get' } return member }) } // Return the description of object's prototype. let getObjectPrototype = function (object) { let proto = Object.getPrototypeOf(object) if (proto === null || proto === Object.prototype) return null return { members: getObjectMembers(proto), proto: getObjectPrototype(proto) } } // Convert a real value into meta data. let valueToMeta = function (sender, value, optimizeSimpleObject = false) { // Determine the type of value. const meta = { type: typeof value } if (meta.type === 'object') { // Recognize certain types of objects. if (value === null) { meta.type = 'value' } else if (ArrayBuffer.isView(value)) { meta.type = 'buffer' } else if (Array.isArray(value)) { meta.type = 'array' } else if (value instanceof Error) { meta.type = 'error' } else if (value instanceof Date) { meta.type = 'date' } else if (isPromise(value)) { meta.type = 'promise' } else if (hasProp.call(value, 'callee') && value.length != null) { // Treat the arguments object as array. meta.type = 'array' } else if (optimizeSimpleObject && v8Util.getHiddenValue(value, 'simple')) { // Treat simple objects as value. meta.type = 'value' } } // Fill the meta object according to value's type. if (meta.type === 'array') { meta.members = value.map((el) => valueToMeta(sender, el)) } else if (meta.type === 'object' || meta.type === 'function') { meta.name = value.constructor ? value.constructor.name : '' // Reference the original value if it's an object, because when it's // passed to renderer we would assume the renderer keeps a reference of // it. meta.id = objectsRegistry.add(sender, value) meta.members = getObjectMembers(value) meta.proto = getObjectPrototype(value) } else if (meta.type === 'buffer') { meta.value = Buffer.from(value) } else if (meta.type === 'promise') { // Add default handler to prevent unhandled rejections in main process // Instead they should appear in the renderer process value.then(function () {}, function () {}) meta.then = valueToMeta(sender, function (onFulfilled, onRejected) { value.then(onFulfilled, onRejected) }) } else if (meta.type === 'error') { meta.members = plainObjectToMeta(value) // Error.name is not part of own properties. meta.members.push({ name: 'name', value: value.name }) } else if (meta.type === 'date') { meta.value = value.getTime() } else { meta.type = 'value' meta.value = value } return meta } // Convert object to meta by value. const plainObjectToMeta = function (obj) { return Object.getOwnPropertyNames(obj).map(function (name) { return { name: name, value: obj[name] } }) } // Convert Error into meta data. const exceptionToMeta = function (error) { return { type: 'exception', message: error.message, stack: error.stack || error } } const throwRPCError = function (message) { const error = new Error(message) error.code = 'EBADRPC' error.errno = -72 throw error } const removeRemoteListenersAndLogWarning = (meta, args, callIntoRenderer) => { let message = `Attempting to call a function in a renderer window that has been closed or released.` + `\nFunction provided here: ${meta.location}` if (args.length > 0 && (args[0].sender instanceof WebContents)) { const {sender} = args[0] const remoteEvents = sender.eventNames().filter((eventName) => { return sender.listeners(eventName).includes(callIntoRenderer) }) if (remoteEvents.length > 0) { message += `\nRemote event names: ${remoteEvents.join(', ')}` remoteEvents.forEach((eventName) => { sender.removeListener(eventName, callIntoRenderer) }) } } console.warn(message) } // Convert array of meta data from renderer into array of real values. const unwrapArgs = function (sender, args) { const metaToValue = function (meta) { let i, len, member, ref, returnValue switch (meta.type) { case 'value': return meta.value case 'remote-object': return objectsRegistry.get(meta.id) case 'array': return unwrapArgs(sender, meta.value) case 'buffer': return Buffer.from(meta.value) case 'date': return new Date(meta.value) case 'promise': return Promise.resolve({ then: metaToValue(meta.then) }) case 'object': { let ret = {} Object.defineProperty(ret.constructor, 'name', { value: meta.name }) ref = meta.members for (i = 0, len = ref.length; i < len; i++) { member = ref[i] ret[member.name] = metaToValue(member.value) } return ret } case 'function-with-return-value': returnValue = metaToValue(meta.value) return function () { return returnValue } case 'function': { // Merge webContentsId and meta.id, since meta.id can be the same in // different webContents. const webContentsId = sender.getId() const objectId = [webContentsId, meta.id] // Cache the callbacks in renderer. if (rendererFunctions.has(objectId)) { return rendererFunctions.get(objectId) } let callIntoRenderer = function (...args) { if (!sender.isDestroyed() && webContentsId === sender.getId()) { sender.send('ELECTRON_RENDERER_CALLBACK', meta.id, valueToMeta(sender, args)) } else { removeRemoteListenersAndLogWarning(meta, args, callIntoRenderer) } } Object.defineProperty(callIntoRenderer, 'length', { value: meta.length }) v8Util.setRemoteCallbackFreer(callIntoRenderer, meta.id, sender) rendererFunctions.set(objectId, callIntoRenderer) return callIntoRenderer } default: throw new TypeError(`Unknown type: ${meta.type}`) } } return args.map(metaToValue) } // Call a function and send reply asynchronously if it's a an asynchronous // style function and the caller didn't pass a callback. const callFunction = function (event, func, caller, args) { let funcMarkedAsync, funcName, funcPassedCallback, ref, ret funcMarkedAsync = v8Util.getHiddenValue(func, 'asynchronous') funcPassedCallback = typeof args[args.length - 1] === 'function' try { if (funcMarkedAsync && !funcPassedCallback) { args.push(function (ret) { event.returnValue = valueToMeta(event.sender, ret, true) }) func.apply(caller, args) } else { ret = func.apply(caller, args) event.returnValue = valueToMeta(event.sender, ret, true) } } catch (error) { // Catch functions thrown further down in function invocation and wrap // them with the function name so it's easier to trace things like // `Error processing argument -1.` funcName = ((ref = func.name) != null) ? ref : 'anonymous' throw new Error(`Could not call remote function '${funcName}'. Check that the function signature is correct. Underlying error: ${error.message}`) } } ipcMain.on('ELECTRON_BROWSER_REQUIRE', function (event, module) { try { event.returnValue = valueToMeta(event.sender, process.mainModule.require(module)) } catch (error) { event.returnValue = exceptionToMeta(error) } }) ipcMain.on('ELECTRON_BROWSER_GET_BUILTIN', function (event, module) { try { event.returnValue = valueToMeta(event.sender, electron[module]) } catch (error) { event.returnValue = exceptionToMeta(error) } }) ipcMain.on('ELECTRON_BROWSER_GLOBAL', function (event, name) { try { event.returnValue = valueToMeta(event.sender, global[name]) } catch (error) { event.returnValue = exceptionToMeta(error) } }) ipcMain.on('ELECTRON_BROWSER_CURRENT_WINDOW', function (event) { try { event.returnValue = valueToMeta(event.sender, event.sender.getOwnerBrowserWindow()) } catch (error) { event.returnValue = exceptionToMeta(error) } }) ipcMain.on('ELECTRON_BROWSER_CURRENT_WEB_CONTENTS', function (event) { event.returnValue = valueToMeta(event.sender, event.sender) }) ipcMain.on('ELECTRON_BROWSER_CONSTRUCTOR', function (event, id, args) { try { args = unwrapArgs(event.sender, args) let constructor = objectsRegistry.get(id) if (constructor == null) { throwRPCError(`Cannot call constructor on missing remote object ${id}`) } // Call new with array of arguments. // http://stackoverflow.com/questions/1606797/use-of-apply-with-new-operator-is-this-possible let obj = new (Function.prototype.bind.apply(constructor, [null].concat(args)))() event.returnValue = valueToMeta(event.sender, obj) } catch (error) { event.returnValue = exceptionToMeta(error) } }) ipcMain.on('ELECTRON_BROWSER_FUNCTION_CALL', function (event, id, args) { try { args = unwrapArgs(event.sender, args) let func = objectsRegistry.get(id) if (func == null) { throwRPCError(`Cannot call function on missing remote object ${id}`) } callFunction(event, func, global, args) } catch (error) { event.returnValue = exceptionToMeta(error) } }) ipcMain.on('ELECTRON_BROWSER_MEMBER_CONSTRUCTOR', function (event, id, method, args) { try { args = unwrapArgs(event.sender, args) let object = objectsRegistry.get(id) if (object == null) { throwRPCError(`Cannot call constructor '${method}' on missing remote object ${id}`) } // Call new with array of arguments. let constructor = object[method] let obj = new (Function.prototype.bind.apply(constructor, [null].concat(args)))() event.returnValue = valueToMeta(event.sender, obj) } catch (error) { event.returnValue = exceptionToMeta(error) } }) ipcMain.on('ELECTRON_BROWSER_MEMBER_CALL', function (event, id, method, args) { try { args = unwrapArgs(event.sender, args) let obj = objectsRegistry.get(id) if (obj == null) { throwRPCError(`Cannot call function '${method}' on missing remote object ${id}`) } callFunction(event, obj[method], obj, args) } catch (error) { event.returnValue = exceptionToMeta(error) } }) ipcMain.on('ELECTRON_BROWSER_MEMBER_SET', function (event, id, name, args) { try { args = unwrapArgs(event.sender, args) let obj = objectsRegistry.get(id) if (obj == null) { throwRPCError(`Cannot set property '${name}' on missing remote object ${id}`) } obj[name] = args[0] event.returnValue = null } catch (error) { event.returnValue = exceptionToMeta(error) } }) ipcMain.on('ELECTRON_BROWSER_MEMBER_GET', function (event, id, name) { try { let obj = objectsRegistry.get(id) if (obj == null) { throwRPCError(`Cannot get property '${name}' on missing remote object ${id}`) } event.returnValue = valueToMeta(event.sender, obj[name]) } catch (error) { event.returnValue = exceptionToMeta(error) } }) ipcMain.on('ELECTRON_BROWSER_DEREFERENCE', function (event, id) { objectsRegistry.remove(event.sender.getId(), id) }) ipcMain.on('ELECTRON_BROWSER_CONTEXT_RELEASE', (e) => { objectsRegistry.clear(e.sender.getId()) e.returnValue = null }) ipcMain.on('ELECTRON_BROWSER_GUEST_WEB_CONTENTS', function (event, guestInstanceId) { try { let guestViewManager = require('./guest-view-manager') event.returnValue = valueToMeta(event.sender, guestViewManager.getGuest(guestInstanceId)) } catch (error) { event.returnValue = exceptionToMeta(error) } }) ipcMain.on('ELECTRON_BROWSER_ASYNC_CALL_TO_GUEST_VIEW', function (event, requestId, guestInstanceId, method, ...args) { try { let guestViewManager = require('./guest-view-manager') let guest = guestViewManager.getGuest(guestInstanceId) if (requestId) { const responseCallback = function (result) { event.sender.send(`ELECTRON_RENDERER_ASYNC_CALL_TO_GUEST_VIEW_RESPONSE_${requestId}`, result) } args.push(responseCallback) } guest[method].apply(guest, args) } catch (error) { event.returnValue = exceptionToMeta(error) } }) ipcMain.on('ELECTRON_BROWSER_SEND_TO', function (event, sendToAll, webContentsId, channel, ...args) { let contents = webContents.fromId(webContentsId) if (!contents) { console.error(`Sending message to WebContents with unknown ID ${webContentsId}`) return } if (sendToAll) { contents.sendToAll(channel, ...args) } else { contents.send(channel, ...args) } }) // Implements window.alert(message, title) ipcMain.on('ELECTRON_BROWSER_WINDOW_ALERT', function (event, message, title) { if (message == null) message = '' if (title == null) title = '' const dialogProperties = { message: `${message}`, title: `${title}`, buttons: ['OK'] } event.returnValue = event.sender.isOffscreen() ? electron.dialog.showMessageBox(dialogProperties) : electron.dialog.showMessageBox( event.sender.getOwnerBrowserWindow(), dialogProperties) }) // Implements window.confirm(message, title) ipcMain.on('ELECTRON_BROWSER_WINDOW_CONFIRM', function (event, message, title) { if (message == null) message = '' if (title == null) title = '' const dialogProperties = { message: `${message}`, title: `${title}`, buttons: ['OK', 'Cancel'], cancelId: 1 } event.returnValue = !(event.sender.isOffscreen() ? electron.dialog.showMessageBox(dialogProperties) : electron.dialog.showMessageBox( event.sender.getOwnerBrowserWindow(), dialogProperties)) }) // Implements window.close() ipcMain.on('ELECTRON_BROWSER_WINDOW_CLOSE', function (event) { const window = event.sender.getOwnerBrowserWindow() if (window) { window.close() } event.returnValue = null }) 'use strict' const v8Util = process.atomBinding('v8_util') class CallbacksRegistry { constructor () { this.nextId = 0 this.callbacks = {} } add (callback) { // The callback is already added. let id = v8Util.getHiddenValue(callback, 'callbackId') if (id != null) return id id = this.nextId += 1 // Capture the location of the function and put it in the ID string, // so that release errors can be tracked down easily. const regexp = /at (.*)/gi const stackString = (new Error()).stack let filenameAndLine let match while ((match = regexp.exec(stackString)) !== null) { const location = match[1] if (location.includes('native')) continue if (location.includes('electron.asar')) continue const ref = /([^/^)]*)\)?$/gi.exec(location) filenameAndLine = ref[1] break } this.callbacks[id] = callback v8Util.setHiddenValue(callback, 'callbackId', id) v8Util.setHiddenValue(callback, 'location', filenameAndLine) return id } get (id) { return this.callbacks[id] || function () {} } apply (id, ...args) { return this.get(id).apply(global, ...args) } remove (id) { const callback = this.callbacks[id] if (callback) { v8Util.deleteHiddenValue(callback, 'callbackId') delete this.callbacks[id] } } } module.exports = CallbacksRegistry if (process.platform === 'linux' && process.type === 'renderer') { // On Linux we could not access clipboard in renderer process. module.exports = require('electron').remote.clipboard } else { const clipboard = process.atomBinding('clipboard') // Read/write to find pasteboard over IPC since only main process is notified // of changes if (process.platform === 'darwin' && process.type === 'renderer') { clipboard.readFindText = require('electron').remote.clipboard.readFindText clipboard.writeFindText = require('electron').remote.clipboard.writeFindText } module.exports = clipboard } 'use strict' const {spawn} = require('child_process') const os = require('os') const path = require('path') const electron = require('electron') const {app} = process.type === 'browser' ? electron : electron.remote const binding = process.atomBinding('crash_reporter') class CrashReporter { start (options) { if (options == null) options = {} this.productName = options.productName != null ? options.productName : app.getName() let { companyName, extra, ignoreSystemCrashHandler, submitURL, uploadToServer } = options if (uploadToServer == null) uploadToServer = options.autoSubmit if (uploadToServer == null) uploadToServer = true if (ignoreSystemCrashHandler == null) ignoreSystemCrashHandler = false if (extra == null) extra = {} if (extra._productName == null) extra._productName = this.getProductName() if (extra._companyName == null) extra._companyName = companyName if (extra._version == null) extra._version = app.getVersion() if (companyName == null) { throw new Error('companyName is a required option to crashReporter.start') } if (submitURL == null) { throw new Error('submitURL is a required option to crashReporter.start') } if (process.platform === 'win32') { const env = { ELECTRON_INTERNAL_CRASH_SERVICE: 1 } const args = [ '--reporter-url=' + submitURL, '--application-name=' + this.getProductName(), '--crashes-directory=' + this.getCrashesDirectory(), '--v=1' ] this._crashServiceProcess = spawn(process.execPath, args, { env: env, detached: true }) } binding.start(this.getProductName(), companyName, submitURL, this.getCrashesDirectory(), uploadToServer, ignoreSystemCrashHandler, extra) } getLastCrashReport () { const reports = this.getUploadedReports() return (reports.length > 0) ? reports[0] : null } getUploadedReports () { return binding.getUploadedReports(this.getCrashesDirectory()) } getCrashesDirectory () { const crashesDir = `${this.getProductName()} Crashes` return path.join(this.getTempDirectory(), crashesDir) } getProductName () { if (this.productName == null) { this.productName = app.getName() } return this.productName } getTempDirectory () { if (this.tempDirectory == null) { try { this.tempDirectory = app.getPath('temp') } catch (error) { this.tempDirectory = os.tmpdir() } } return this.tempDirectory } getUploadToServer () { if (process.type === 'browser') { return binding.getUploadToServer() } else { throw new Error('getUploadToServer can only be called from the main process') } } setUploadToServer (uploadToServer) { if (process.type === 'browser') { return binding.setUploadToServer(uploadToServer) } else { throw new Error('setUploadToServer can only be called from the main process') } } // TODO(2.0) Remove setExtraParameter (key, value) { // TODO(alexeykuzmin): Warning disabled since it caused // a couple of Crash Reported tests to time out on Mac. Add it back. // https://github.com/electron/electron/issues/11012 // if (!process.noDeprecations) { // deprecate.warn('crashReporter.setExtraParameter', // 'crashReporter.addExtraParameter or crashReporter.removeExtraParameter') // } binding.setExtraParameter(key, value) } addExtraParameter (key, value) { binding.addExtraParameter(key, value) } removeExtraParameter (key) { binding.removeExtraParameter(key) } getParameters (key, value) { return binding.getParameters() } } module.exports = new CrashReporter() // Deprecate a method. const deprecate = function (oldName, newName, fn) { let warned = false return function () { if (!(warned || process.noDeprecation)) { warned = true deprecate.warn(oldName, newName) } return fn.apply(this, arguments) } } // The method is aliases and the old method is retained for backwards compat deprecate.alias = function (object, deprecatedName, existingName) { let warned = false const newMethod = function () { if (!(warned || process.noDeprecation)) { warned = true deprecate.warn(deprecatedName, existingName) } return this[existingName].apply(this, arguments) } if (typeof object === 'function') { object.prototype[deprecatedName] = newMethod } else { object[deprecatedName] = newMethod } } deprecate.warn = (oldName, newName) => { return deprecate.log(`'${oldName}' is deprecated. Use '${newName}' instead.`) } let deprecationHandler = null // Print deprecation message. deprecate.log = (message) => { if (typeof deprecationHandler === 'function') { deprecationHandler(message) } else if (process.throwDeprecation) { throw new Error(message) } else if (process.traceDeprecation) { return console.trace(message) } else { return console.warn(`(electron) ${message}`) } } deprecate.setHandler = (handler) => { deprecationHandler = handler } deprecate.getHandler = () => deprecationHandler // None of the below methods are used, and so will be commented // out until such time that they are needed to be used and tested. // // Forward the method to member. // deprecate.member = (object, method, member) => { // let warned = false // object.prototype[method] = function () { // if (!(warned || process.noDeprecation)) { // warned = true // deprecate.warn(method, `${member}.${method}`) // } // return this[member][method].apply(this[member], arguments) // } // } // // // Deprecate a property. // deprecate.property = (object, property, method) => { // return Object.defineProperty(object, property, { // get: function () { // let warned = false // if (!(warned || process.noDeprecation)) { // warned = true // deprecate.warn(`${property} property`, `${method} method`) // } // return this[method]() // } // }) // } // // // Deprecate an event. // deprecate.event = (emitter, oldName, newName, fn) => { // let warned = false // return emitter.on(newName, function (...args) { // if (this.listenerCount(oldName) > 0) { // if (!(warned || process.noDeprecation)) { // warned = true // deprecate.warn(`'${oldName}' event`, `'${newName}' event`) // } // if (fn != null) { // fn.apply(this, arguments) // } else { // this.emit.apply(this, [oldName].concat(args)) // } // } // }) // } module.exports = deprecate 'use strict' const deprecate = require('electron').deprecate exports.setHandler = function (deprecationHandler) { deprecate.setHandler(deprecationHandler) } exports.getHandler = function () { return deprecate.getHandler() } const moduleList = require('../module-list') // Attaches properties to |exports|. exports.defineProperties = function (exports) { const descriptors = {} for (const module of moduleList) { descriptors[module.name] = { enumerable: !module.private, get: () => require(`../${module.file}`) } } return Object.defineProperties(exports, descriptors) } 'use strict' module.exports = function isPromise (val) { return ( val && val.then && val.then instanceof Function && val.constructor && val.constructor.reject && val.constructor.reject instanceof Function && val.constructor.resolve && val.constructor.resolve instanceof Function ) } // Common modules, please sort alphabetically module.exports = [ {name: 'clipboard', file: 'clipboard'}, {name: 'crashReporter', file: 'crash-reporter'}, {name: 'nativeImage', file: 'native-image'}, {name: 'shell', file: 'shell'}, // The internal modules, invisible unless you know their names. {name: 'CallbacksRegistry', file: 'callbacks-registry', private: true}, {name: 'deprecate', file: 'deprecate', private: true}, {name: 'deprecations', file: 'deprecations', private: true}, {name: 'isPromise', file: 'is-promise', private: true} ] module.exports = process.atomBinding('native_image') module.exports = process.atomBinding('shell') module.exports = function atomBindingSetup (binding, processType) { return function atomBinding (name) { try { return binding(`atom_${processType}_${name}`) } catch (error) { if (/No such module/.test(error.message)) { return binding(`atom_common_${name}`) } else { throw error } } } } const timers = require('timers') const util = require('util') process.atomBinding = require('./atom-binding-setup')(process.binding, process.type) // setImmediate and process.nextTick makes use of uv_check and uv_prepare to // run the callbacks, however since we only run uv loop on requests, the // callbacks wouldn't be called until something else activated the uv loop, // which would delay the callbacks for arbitrary long time. So we should // initiatively activate the uv loop once setImmediate and process.nextTick is // called. const wrapWithActivateUvLoop = function (func) { return wrap(func, function (func) { return function () { process.activateUvLoop() return func.apply(this, arguments) } }) } function wrap (func, wrapper) { const wrapped = wrapper(func) if (func[util.promisify.custom]) { wrapped[util.promisify.custom] = wrapper(func[util.promisify.custom]) } return wrapped } process.nextTick = wrapWithActivateUvLoop(process.nextTick) global.setImmediate = wrapWithActivateUvLoop(timers.setImmediate) global.clearImmediate = timers.clearImmediate if (process.type === 'browser') { // setTimeout needs to update the polling timeout of the event loop, when // called under Chromium's event loop the node's event loop won't get a chance // to update the timeout, so we have to force the node's event loop to // recalculate the timeout in browser process. global.setTimeout = wrapWithActivateUvLoop(timers.setTimeout) global.setInterval = wrapWithActivateUvLoop(timers.setInterval) } if (process.platform === 'win32') { // Always returns EOF for stdin stream. const {Readable} = require('stream') const stdin = new Readable() stdin.push(null) process.__defineGetter__('stdin', function () { return stdin }) // If we're running as a Windows Store app, __dirname will be set // to C:/Program Files/WindowsApps. // // Nobody else get's to install there, changing the path is forbidden // We can therefore say that we're running as appx if (__dirname.includes('\\WindowsApps\\')) { process.windowsStore = true } } // parses a feature string that has the format used in window.open() // - `features` input string // - `emit` function(key, value) - called for each parsed KV module.exports = function parseFeaturesString (features, emit) { features = `${features}` // split the string by ',' features.split(/,\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*=/) if (!key) return // interpret the value as a boolean, if possible value = (value === 'yes' || value === '1') ? true : (value === 'no' || value === '0') ? false : value // emit the parsed pair emit(key, value) }) } const path = require('path') const Module = require('module') // Clear Node's global search paths. Module.globalPaths.length = 0 // Clear current and parent(init.js)'s search paths. module.paths = [] module.parent.paths = [] // Prevent Node from adding paths outside this app to search paths. const resourcesPathWithTrailingSlash = process.resourcesPath + path.sep const originalNodeModulePaths = Module._nodeModulePaths Module._nodeModulePaths = function (from) { const paths = originalNodeModulePaths(from) const fromPath = path.resolve(from) + path.sep // If "from" is outside the app then we do nothing. if (fromPath.startsWith(resourcesPathWithTrailingSlash)) { return paths.filter(function (candidate) { return candidate.startsWith(resourcesPathWithTrailingSlash) }) } else { return paths } } // Patch Module._resolveFilename to always require the Electron API when // require('electron') is done. const electronPath = path.join(__dirname, '..', process.type, 'api', 'exports', 'electron.js') const originalResolveFilename = Module._resolveFilename Module._resolveFilename = function (request, parent, isMain) { if (request === 'electron') { return electronPath } else { return originalResolveFilename(request, parent, isMain) } } const {ipcRenderer, nativeImage} = require('electron') const includes = [].includes let currentId = 0 const incrementId = () => { currentId += 1 return currentId } // |options.types| can't be empty and must be an array function isValid (options) { const types = options ? options.types : undefined 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') if (options.thumbnailSize == null) { options.thumbnailSize = { width: 150, height: 150 } } 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) }) }) return results })()) }) } const common = require('../../../common/api/exports/electron') const moduleList = require('../module-list') // Import common modules. common.defineProperties(exports) for (const module of moduleList) { Object.defineProperty(exports, module.name, { enumerable: !module.private, get: () => require(`../${module.file}`) }) } 'use strict' const binding = process.atomBinding('ipc') const v8Util = process.atomBinding('v8_util') // Created by init.js. const ipcRenderer = v8Util.getHiddenValue(global, 'ipc') ipcRenderer.send = function (...args) { return binding.send('ipc-message', args) } ipcRenderer.sendSync = function (...args) { return JSON.parse(binding.sendSync('ipc-message-sync', args)) } ipcRenderer.sendToHost = function (...args) { return binding.send('ipc-message-host', args) } ipcRenderer.sendTo = function (webContentsId, channel, ...args) { if (typeof webContentsId !== 'number') { throw new TypeError('First argument has to be webContentsId') } ipcRenderer.send('ELECTRON_BROWSER_SEND_TO', false, webContentsId, channel, ...args) } ipcRenderer.sendToAll = function (webContentsId, channel, ...args) { if (typeof webContentsId !== 'number') { throw new TypeError('First argument has to be webContentsId') } ipcRenderer.send('ELECTRON_BROWSER_SEND_TO', true, webContentsId, channel, ...args) } const removeAllListeners = ipcRenderer.removeAllListeners.bind(ipcRenderer) ipcRenderer.removeAllListeners = function (...args) { if (args.length === 0) { throw new Error('Removing all listeners from ipcRenderer will make Electron internals stop working. Please specify a event name') } removeAllListeners(...args) } module.exports = ipcRenderer // Renderer side modules, please sort alphabetically. module.exports = [ {name: 'desktopCapturer', file: 'desktop-capturer'}, {name: 'ipcRenderer', file: 'ipc-renderer'}, {name: 'remote', file: 'remote'}, {name: 'screen', file: 'screen'}, {name: 'webFrame', file: 'web-frame'} ] 'use strict' // Note: Don't use destructuring assignment for `Buffer`, or we'll hit a // browserify bug that makes the statement invalid, throwing an error in // sandboxed renderer. const Buffer = require('buffer').Buffer const v8Util = process.atomBinding('v8_util') const {ipcRenderer, isPromise, CallbacksRegistry} = require('electron') const resolvePromise = Promise.resolve.bind(Promise) const callbacksRegistry = new CallbacksRegistry() const remoteObjectCache = v8Util.createIDWeakMap() // Convert the arguments object into an array of meta data. function wrapArgs (args, visited = new Set()) { const valueToMeta = (value) => { // Check for circular reference. if (visited.has(value)) { return { type: 'value', value: null } } if (Array.isArray(value)) { visited.add(value) let meta = { type: 'array', value: wrapArgs(value, visited) } visited.delete(value) return meta } else if (ArrayBuffer.isView(value)) { return { type: 'buffer', value: Buffer.from(value) } } else if (value instanceof Date) { return { type: 'date', value: value.getTime() } } else if ((value != null) && typeof value === 'object') { if (isPromise(value)) { return { type: 'promise', then: valueToMeta(function (onFulfilled, onRejected) { value.then(onFulfilled, onRejected) }) } } else if (v8Util.getHiddenValue(value, 'atomId')) { return { type: 'remote-object', id: v8Util.getHiddenValue(value, 'atomId') } } let meta = { type: 'object', name: value.constructor ? value.constructor.name : '', members: [] } visited.add(value) for (let prop in value) { meta.members.push({ name: prop, value: valueToMeta(value[prop]) }) } visited.delete(value) return meta } else if (typeof value === 'function' && v8Util.getHiddenValue(value, 'returnValue')) { return { type: 'function-with-return-value', value: valueToMeta(value()) } } else if (typeof value === 'function') { return { type: 'function', id: callbacksRegistry.add(value), location: v8Util.getHiddenValue(value, 'location'), length: value.length } } else { return { type: 'value', value: value } } } return args.map(valueToMeta) } // Populate object's members from descriptors. // The |ref| will be kept referenced by |members|. // This matches |getObjectMemebers| in rpc-server. function setObjectMembers (ref, object, metaId, members) { if (!Array.isArray(members)) return for (let member of members) { if (object.hasOwnProperty(member.name)) continue let descriptor = { enumerable: member.enumerable } if (member.type === 'method') { const remoteMemberFunction = function (...args) { let command if (this && this.constructor === remoteMemberFunction) { command = 'ELECTRON_BROWSER_MEMBER_CONSTRUCTOR' } else { command = 'ELECTRON_BROWSER_MEMBER_CALL' } const ret = ipcRenderer.sendSync(command, metaId, member.name, wrapArgs(args)) return metaToValue(ret) } let descriptorFunction = proxyFunctionProperties(remoteMemberFunction, metaId, member.name) descriptor.get = () => { descriptorFunction.ref = ref // The member should reference its object. return descriptorFunction } // Enable monkey-patch the method descriptor.set = (value) => { descriptorFunction = value return value } descriptor.configurable = true } else if (member.type === 'get') { descriptor.get = () => { const command = 'ELECTRON_BROWSER_MEMBER_GET' const meta = ipcRenderer.sendSync(command, metaId, member.name) return metaToValue(meta) } if (member.writable) { descriptor.set = (value) => { const args = wrapArgs([value]) const command = 'ELECTRON_BROWSER_MEMBER_SET' const meta = ipcRenderer.sendSync(command, metaId, member.name, args) if (meta != null) metaToValue(meta) return value } } } Object.defineProperty(object, member.name, descriptor) } } // Populate object's prototype from descriptor. // This matches |getObjectPrototype| in rpc-server. function setObjectPrototype (ref, object, metaId, descriptor) { if (descriptor === null) return let proto = {} setObjectMembers(ref, proto, metaId, descriptor.members) setObjectPrototype(ref, proto, metaId, descriptor.proto) Object.setPrototypeOf(object, proto) } // Wrap function in Proxy for accessing remote properties function proxyFunctionProperties (remoteMemberFunction, metaId, name) { let loaded = false // Lazily load function properties const loadRemoteProperties = () => { if (loaded) return loaded = true const command = 'ELECTRON_BROWSER_MEMBER_GET' const meta = ipcRenderer.sendSync(command, metaId, name) setObjectMembers(remoteMemberFunction, remoteMemberFunction, meta.id, meta.members) } return new Proxy(remoteMemberFunction, { set: (target, property, value, receiver) => { if (property !== 'ref') loadRemoteProperties() target[property] = value return true }, get: (target, property, receiver) => { if (!target.hasOwnProperty(property)) loadRemoteProperties() const value = target[property] if (property === 'toString' && typeof value === 'function') { return value.bind(target) } return value }, ownKeys: (target) => { loadRemoteProperties() return Object.getOwnPropertyNames(target) }, getOwnPropertyDescriptor: (target, property) => { let descriptor = Object.getOwnPropertyDescriptor(target, property) if (descriptor) return descriptor loadRemoteProperties() return Object.getOwnPropertyDescriptor(target, property) } }) } // Convert meta data from browser into real value. function metaToValue (meta) { const types = { value: () => meta.value, array: () => meta.members.map((member) => metaToValue(member)), buffer: () => Buffer.from(meta.value), promise: () => resolvePromise({then: metaToValue(meta.then)}), error: () => metaToPlainObject(meta), date: () => new Date(meta.value), exception: () => { throw new Error(`${meta.message}\n${meta.stack}`) } } if (meta.type in types) { return types[meta.type]() } else { let ret if (remoteObjectCache.has(meta.id)) { return remoteObjectCache.get(meta.id) } // A shadow class to represent the remote function object. if (meta.type === 'function') { let remoteFunction = function (...args) { let command if (this && this.constructor === remoteFunction) { command = 'ELECTRON_BROWSER_CONSTRUCTOR' } else { command = 'ELECTRON_BROWSER_FUNCTION_CALL' } const obj = ipcRenderer.sendSync(command, meta.id, wrapArgs(args)) return metaToValue(obj) } ret = remoteFunction } else { ret = {} } setObjectMembers(ret, ret, meta.id, meta.members) setObjectPrototype(ret, ret, meta.id, meta.proto) Object.defineProperty(ret.constructor, 'name', { value: meta.name }) // Track delegate obj's lifetime & tell browser to clean up when object is GCed. v8Util.setRemoteObjectFreer(ret, meta.id) v8Util.setHiddenValue(ret, 'atomId', meta.id) remoteObjectCache.set(meta.id, ret) return ret } } // Construct a plain object from the meta. function metaToPlainObject (meta) { const obj = (() => meta.type === 'error' ? new Error() : {})() for (let i = 0; i < meta.members.length; i++) { let {name, value} = meta.members[i] obj[name] = value } return obj } // Browser calls a callback in renderer. ipcRenderer.on('ELECTRON_RENDERER_CALLBACK', (event, id, args) => { callbacksRegistry.apply(id, metaToValue(args)) }) // A callback in browser is released. ipcRenderer.on('ELECTRON_RENDERER_RELEASE_CALLBACK', (event, id) => { callbacksRegistry.remove(id) }) process.on('exit', () => { const command = 'ELECTRON_BROWSER_CONTEXT_RELEASE' ipcRenderer.sendSync(command) }) exports.require = (module) => { const command = 'ELECTRON_BROWSER_REQUIRE' const meta = ipcRenderer.sendSync(command, module) return metaToValue(meta) } // Alias to remote.require('electron').xxx. exports.getBuiltin = (module) => { const command = 'ELECTRON_BROWSER_GET_BUILTIN' const meta = ipcRenderer.sendSync(command, module) return metaToValue(meta) } exports.getCurrentWindow = () => { const command = 'ELECTRON_BROWSER_CURRENT_WINDOW' const meta = ipcRenderer.sendSync(command) return metaToValue(meta) } // Get current WebContents object. exports.getCurrentWebContents = () => { return metaToValue(ipcRenderer.sendSync('ELECTRON_BROWSER_CURRENT_WEB_CONTENTS')) } // Get a global object in browser. exports.getGlobal = (name) => { const command = 'ELECTRON_BROWSER_GLOBAL' const meta = ipcRenderer.sendSync(command, name) return metaToValue(meta) } // Get the process object in browser. exports.__defineGetter__('process', () => exports.getGlobal('process')) // Create a function that will return the specified value when called in browser. exports.createFunctionWithReturnValue = (returnValue) => { const func = () => returnValue v8Util.setHiddenValue(func, 'returnValue', true) return func } // Get the guest WebContents from guestInstanceId. exports.getGuestWebContents = (guestInstanceId) => { const command = 'ELECTRON_BROWSER_GUEST_WEB_CONTENTS' const meta = ipcRenderer.sendSync(command, guestInstanceId) return metaToValue(meta) } const addBuiltinProperty = (name) => { Object.defineProperty(exports, name, { get: () => exports.getBuiltin(name) }) } const browserModules = require('../../common/api/module-list').concat( require('../../browser/api/module-list')) // And add a helper receiver for each one. browserModules .filter((m) => !m.private) .map((m) => m.name) .forEach(addBuiltinProperty) module.exports = require('electron').remote.screen 'use strict' const {EventEmitter} = require('events') const {webFrame, WebFrame} = process.atomBinding('web_frame') // WebFrame is an EventEmitter. Object.setPrototypeOf(WebFrame.prototype, EventEmitter.prototype) EventEmitter.call(webFrame) // Lots of webview would subscribe to webFrame's events. webFrame.setMaxListeners(0) module.exports = webFrame const {ipcRenderer} = require('electron') const Event = require('./extensions/event') const url = require('url') let nextId = 0 class Tab { constructor (tabId) { this.id = tabId } } class MessageSender { constructor (tabId, extensionId) { this.tab = tabId ? new Tab(tabId) : null this.id = extensionId this.url = `chrome-extension://${extensionId}` } } class Port { constructor (tabId, portId, extensionId, name) { this.tabId = tabId this.portId = portId this.disconnected = false this.name = name this.onDisconnect = new Event() this.onMessage = new Event() this.sender = new MessageSender(tabId, extensionId) ipcRenderer.once(`CHROME_PORT_DISCONNECT_${portId}`, () => { this._onDisconnect() }) ipcRenderer.on(`CHROME_PORT_POSTMESSAGE_${portId}`, (event, message) => { const sendResponse = function () { console.error('sendResponse is not implemented') } this.onMessage.emit(message, this.sender, sendResponse) }) } disconnect () { if (this.disconnected) return ipcRenderer.sendToAll(this.tabId, `CHROME_PORT_DISCONNECT_${this.portId}`) this._onDisconnect() } postMessage (message) { ipcRenderer.sendToAll(this.tabId, `CHROME_PORT_POSTMESSAGE_${this.portId}`, message) } _onDisconnect () { this.disconnected = true ipcRenderer.removeAllListeners(`CHROME_PORT_POSTMESSAGE_${this.portId}`) this.onDisconnect.emit() } } // Inject chrome API to the |context| exports.injectTo = function (extensionId, isBackgroundPage, context) { const chrome = context.chrome = context.chrome || {} let originResultID = 1 ipcRenderer.on(`CHROME_RUNTIME_ONCONNECT_${extensionId}`, (event, tabId, portId, connectInfo) => { chrome.runtime.onConnect.emit(new Port(tabId, portId, extensionId, connectInfo.name)) }) ipcRenderer.on(`CHROME_RUNTIME_ONMESSAGE_${extensionId}`, (event, tabId, message, resultID) => { chrome.runtime.onMessage.emit(message, new MessageSender(tabId, extensionId), (messageResult) => { ipcRenderer.send(`CHROME_RUNTIME_ONMESSAGE_RESULT_${resultID}`, messageResult) }) }) ipcRenderer.on('CHROME_TABS_ONCREATED', (event, tabId) => { chrome.tabs.onCreated.emit(new Tab(tabId)) }) ipcRenderer.on('CHROME_TABS_ONREMOVED', (event, tabId) => { chrome.tabs.onRemoved.emit(tabId) }) chrome.runtime = { id: extensionId, getURL: function (path) { return url.format({ protocol: 'chrome-extension', slashes: true, hostname: extensionId, pathname: path }) }, connect (...args) { if (isBackgroundPage) { console.error('chrome.runtime.connect is not supported in background page') return } // Parse the optional args. let targetExtensionId = extensionId let connectInfo = {name: ''} if (args.length === 1) { connectInfo = args[0] } else if (args.length === 2) { [targetExtensionId, connectInfo] = args } const {tabId, portId} = ipcRenderer.sendSync('CHROME_RUNTIME_CONNECT', targetExtensionId, connectInfo) return new Port(tabId, portId, extensionId, connectInfo.name) }, sendMessage (...args) { if (isBackgroundPage) { console.error('chrome.runtime.sendMessage is not supported in background page') return } // Parse the optional args. let targetExtensionId = extensionId let message if (args.length === 1) { message = args[0] } else if (args.length === 2) { // A case of not provide extension-id: (message, responseCallback) if (typeof args[1] === 'function') { ipcRenderer.on(`CHROME_RUNTIME_SENDMESSAGE_RESULT_${originResultID}`, (event, result) => args[1](result)) message = args[0] } else { [targetExtensionId, message] = args } } else { console.error('options is not supported') ipcRenderer.on(`CHROME_RUNTIME_SENDMESSAGE_RESULT_${originResultID}`, (event, result) => args[2](result)) } ipcRenderer.send('CHROME_RUNTIME_SENDMESSAGE', targetExtensionId, message, originResultID) originResultID++ }, onConnect: new Event(), onMessage: new Event(), onInstalled: new Event() } chrome.tabs = { executeScript (tabId, details, callback) { const requestId = ++nextId ipcRenderer.once(`CHROME_TABS_EXECUTESCRIPT_RESULT_${requestId}`, (event, result) => { callback([event.result]) }) ipcRenderer.send('CHROME_TABS_EXECUTESCRIPT', requestId, tabId, extensionId, details) }, sendMessage (tabId, message, options, responseCallback) { if (responseCallback) { ipcRenderer.on(`CHROME_TABS_SEND_MESSAGE_RESULT_${originResultID}`, (event, result) => responseCallback(result)) } ipcRenderer.send('CHROME_TABS_SEND_MESSAGE', tabId, extensionId, isBackgroundPage, message, originResultID) originResultID++ }, onUpdated: new Event(), onCreated: new Event(), onRemoved: new Event() } chrome.extension = { getURL: chrome.runtime.getURL, connect: chrome.runtime.connect, onConnect: chrome.runtime.onConnect, sendMessage: chrome.runtime.sendMessage, onMessage: chrome.runtime.onMessage } chrome.storage = require('./extensions/storage').setup(extensionId) chrome.pageAction = { show () {}, hide () {}, setTitle () {}, getTitle () {}, setIcon () {}, setPopup () {}, getPopup () {} } chrome.i18n = require('./extensions/i18n').setup(extensionId) chrome.webNavigation = require('./extensions/web-navigation').setup() } const {ipcRenderer} = require('electron') const {runInThisContext} = require('vm') // Check whether pattern matches. // https://developer.chrome.com/extensions/match_patterns const matchesPattern = function (pattern) { if (pattern === '') return true const regexp = new RegExp(`^${pattern.replace(/\*/g, '.*')}$`) const url = `${location.protocol}//${location.host}${location.pathname}` return url.match(regexp) } // Run the code with chrome API integrated. const runContentScript = function (extensionId, url, code) { const context = {} require('./chrome-api').injectTo(extensionId, false, context) const wrapper = `((chrome) => {\n ${code}\n })` const compiledWrapper = runInThisContext(wrapper, { filename: url, lineOffset: 1, displayErrors: true }) return compiledWrapper.call(this, context.chrome) } const runStylesheet = function (url, code) { const wrapper = `((code) => { function init() { const styleElement = document.createElement('style'); styleElement.textContent = code; document.head.append(styleElement); } document.addEventListener('DOMContentLoaded', init); })` const compiledWrapper = runInThisContext(wrapper, { filename: url, lineOffset: 1, displayErrors: true }) return compiledWrapper.call(this, code) } // Run injected scripts. // https://developer.chrome.com/extensions/content_scripts const injectContentScript = function (extensionId, script) { if (!script.matches.some(matchesPattern)) return if (script.js) { for (const {url, code} of script.js) { const fire = runContentScript.bind(window, extensionId, url, code) if (script.runAt === 'document_start') { process.once('document-start', fire) } else if (script.runAt === 'document_end') { process.once('document-end', fire) } else { document.addEventListener('DOMContentLoaded', fire) } } } if (script.css) { for (const {url, code} of script.css) { const fire = runStylesheet.bind(window, url, code) if (script.runAt === 'document_start') { process.once('document-start', fire) } else if (script.runAt === 'document_end') { process.once('document-end', fire) } else { document.addEventListener('DOMContentLoaded', fire) } } } } // Handle the request of chrome.tabs.executeJavaScript. ipcRenderer.on('CHROME_TABS_EXECUTESCRIPT', function (event, senderWebContentsId, requestId, extensionId, url, code) { const result = runContentScript.call(window, extensionId, url, code) ipcRenderer.sendToAll(senderWebContentsId, `CHROME_TABS_EXECUTESCRIPT_RESULT_${requestId}`, result) }) // Read the renderer process preferences. const preferences = process.getRenderProcessPreferences() if (preferences) { for (const pref of preferences) { if (pref.contentScripts) { for (const script of pref.contentScripts) { injectContentScript(pref.extensionId, script) } } } } class Event { constructor () { this.listeners = [] } addListener (callback) { this.listeners.push(callback) } removeListener (callback) { const index = this.listeners.indexOf(callback) if (index !== -1) { this.listeners.splice(index, 1) } } emit (...args) { for (const listener of this.listeners) { listener(...args) } } } module.exports = Event // Implementation of chrome.i18n.getMessage // https://developer.chrome.com/extensions/i18n#method-getMessage // // Does not implement predefined messages: // https://developer.chrome.com/extensions/i18n#overview-predefined const {ipcRenderer} = require('electron') const fs = require('fs') const path = require('path') let metadata const getExtensionMetadata = (extensionId) => { if (!metadata) { metadata = ipcRenderer.sendSync('CHROME_I18N_MANIFEST', extensionId) } return metadata } const getMessagesPath = (extensionId, language) => { const metadata = getExtensionMetadata(extensionId) const defaultLocale = metadata.default_locale || 'en' const localesDirectory = path.join(metadata.srcDirectory, '_locales') let messagesPath = path.join(localesDirectory, language, 'messages.json') if (!fs.statSyncNoException(messagesPath)) { messagesPath = path.join(localesDirectory, defaultLocale, 'messages.json') } return messagesPath } const getMessages = (extensionId, language) => { try { const messagesPath = getMessagesPath(extensionId, language) return JSON.parse(fs.readFileSync(messagesPath)) || {} } catch (error) { return {} } } const getLanguage = () => { return navigator.language.replace(/-.*$/, '').toLowerCase() } const replaceNumberedSubstitutions = (message, substitutions) => { return message.replace(/\$(\d+)/, (_, number) => { const index = parseInt(number, 10) - 1 return substitutions[index] || '' }) } const replacePlaceholders = (message, placeholders, substitutions) => { if (typeof substitutions === 'string') { substitutions = [substitutions] } if (!Array.isArray(substitutions)) { substitutions = [] } if (placeholders) { Object.keys(placeholders).forEach((name) => { let {content} = placeholders[name] content = replaceNumberedSubstitutions(content, substitutions) message = message.replace(new RegExp(`\\$${name}\\$`, 'gi'), content) }) } return replaceNumberedSubstitutions(message, substitutions) } const getMessage = (extensionId, messageName, substitutions) => { const messages = getMessages(extensionId, getLanguage()) if (messages.hasOwnProperty(messageName)) { const {message, placeholders} = messages[messageName] return replacePlaceholders(message, placeholders, substitutions) } } exports.setup = (extensionId) => { return { getMessage (messageName, substitutions) { return getMessage(extensionId, messageName, substitutions) } } } const fs = require('fs') const path = require('path') const { remote } = require('electron') const { app } = remote const getChromeStoragePath = (storageType, extensionId) => { return path.join( app.getPath('userData'), `/Chrome Storage/${extensionId}-${storageType}.json`) } const mkdirp = (dir, callback) => { fs.mkdir(dir, (error) => { if (error && error.code === 'ENOENT') { mkdirp(path.dirname(dir), (error) => { if (!error) { mkdirp(dir, callback) } }) } else if (error && error.code === 'EEXIST') { callback(null) } else { callback(error) } }) } const readChromeStorageFile = (storageType, extensionId, cb) => { const filePath = getChromeStoragePath(storageType, extensionId) fs.readFile(filePath, 'utf8', (err, data) => { if (err && err.code === 'ENOENT') { return cb(null, null) } cb(err, data) }) } const writeChromeStorageFile = (storageType, extensionId, data, cb) => { const filePath = getChromeStoragePath(storageType, extensionId) mkdirp(path.dirname(filePath), err => { if (err) { /* we just ignore the errors of mkdir or mkdirp */ } fs.writeFile(filePath, data, cb) }) } const getStorage = (storageType, extensionId, cb) => { readChromeStorageFile(storageType, extensionId, (err, data) => { if (err) throw err if (!cb) throw new TypeError('No callback provided') if (data !== null) { cb(JSON.parse(data)) } else { cb({}) } }) } const setStorage = (storageType, extensionId, storage, cb) => { const json = JSON.stringify(storage) writeChromeStorageFile(storageType, extensionId, json, err => { if (err) throw err if (cb) cb() }) } const getStorageManager = (storageType, extensionId) => { return { get (keys, callback) { getStorage(storageType, extensionId, storage => { if (keys == null) return callback(storage) let defaults = {} switch (typeof keys) { case 'string': keys = [keys] break case 'object': if (!Array.isArray(keys)) { defaults = keys keys = Object.keys(keys) } break } if (keys.length === 0) return callback({}) let items = {} keys.forEach(function (key) { var value = storage[key] if (value == null) value = defaults[key] items[key] = value }) callback(items) }) }, set (items, callback) { getStorage(storageType, extensionId, storage => { Object.keys(items).forEach(function (name) { storage[name] = items[name] }) setStorage(storageType, extensionId, storage, callback) }) }, remove (keys, callback) { getStorage(storageType, extensionId, storage => { if (!Array.isArray(keys)) { keys = [keys] } keys.forEach(function (key) { delete storage[key] }) setStorage(storageType, extensionId, storage, callback) }) }, clear (callback) { setStorage(storageType, extensionId, {}, callback) } } } module.exports = { setup: extensionId => ({ sync: getStorageManager('sync', extensionId), local: getStorageManager('local', extensionId) }) } const Event = require('./event') const {ipcRenderer} = require('electron') class WebNavigation { constructor () { this.onBeforeNavigate = new Event() this.onCompleted = new Event() ipcRenderer.on('CHROME_WEBNAVIGATION_ONBEFORENAVIGATE', (event, details) => { this.onBeforeNavigate.emit(details) }) ipcRenderer.on('CHROME_WEBNAVIGATION_ONCOMPLETED', (event, details) => { this.onCompleted.emit(details) }) } } exports.setup = () => { return new WebNavigation() } 'use strict' const events = require('events') const path = require('path') const Module = require('module') const resolvePromise = Promise.resolve.bind(Promise) // We modified the original process.argv to let node.js load the // init.js, we need to restore it here. process.argv.splice(1, 1) // Clear search paths. require('../common/reset-search-paths') // Import common settings. require('../common/init') var globalPaths = Module.globalPaths // Expose public APIs. globalPaths.push(path.join(__dirname, 'api', 'exports')) // The global variable will be used by ipc for event dispatching var v8Util = process.atomBinding('v8_util') v8Util.setHiddenValue(global, 'ipc', new events.EventEmitter()) // Use electron module after everything is ready. const electron = require('electron') // Call webFrame method. electron.ipcRenderer.on('ELECTRON_INTERNAL_RENDERER_WEB_FRAME_METHOD', (event, method, args) => { electron.webFrame[method](...args) }) electron.ipcRenderer.on('ELECTRON_INTERNAL_RENDERER_SYNC_WEB_FRAME_METHOD', (event, requestId, method, args) => { const result = electron.webFrame[method](...args) event.sender.send(`ELECTRON_INTERNAL_BROWSER_SYNC_WEB_FRAME_RESPONSE_${requestId}`, result) }) electron.ipcRenderer.on('ELECTRON_INTERNAL_RENDERER_ASYNC_WEB_FRAME_METHOD', (event, requestId, method, args) => { const responseCallback = function (result) { resolvePromise(result) .then((resolvedResult) => { event.sender.send(`ELECTRON_INTERNAL_BROWSER_ASYNC_WEB_FRAME_RESPONSE_${requestId}`, null, resolvedResult) }) .catch((resolvedError) => { if (resolvedError instanceof Error) { // Errors get lost, because: JSON.stringify(new Error('Message')) === {} // Take the serializable properties and construct a generic object resolvedError = { message: resolvedError.message, stack: resolvedError.stack, name: resolvedError.name, __ELECTRON_SERIALIZED_ERROR__: true } } event.sender.send(`ELECTRON_INTERNAL_BROWSER_ASYNC_WEB_FRAME_RESPONSE_${requestId}`, resolvedError) }) } args.push(responseCallback) electron.webFrame[method](...args) }) // Process command line arguments. let nodeIntegration = 'false' let webviewTag = 'false' let preloadScript = null let isBackgroundPage = false let appPath = null for (let 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) } 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) } } if (window.location.protocol === 'chrome-devtools:') { // Override some inspector APIs. require('./inspector') nodeIntegration = 'false' } else if (window.location.protocol === 'chrome-extension:') { // Add implementations of chrome API. require('./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('./override') // Inject content scripts. require('./content-scripts-injector') // Load webview tag implementation. if (webviewTag === 'true' && process.guestInstanceId == null) { require('./web-view/web-view') require('./web-view/web-view-attributes') } } if (nodeIntegration === 'true') { // Export node bindings to global. global.require = require global.module = module // Set the __filename to the path of html file if it is file: protocol. if (window.location.protocol === 'file:') { const location = window.location let pathname = location.pathname if (process.platform === 'win32') { if (pathname[0] === '/') pathname = pathname.substr(1) const isWindowsNetworkSharePath = location.hostname.length > 0 && globalPaths[0].startsWith('\\') if (isWindowsNetworkSharePath) { pathname = `//${location.host}/${pathname}` } } global.__filename = path.normalize(decodeURIComponent(pathname)) global.__dirname = path.dirname(global.__filename) // Set module's filename so relative require can work as expected. module.filename = global.__filename // Also search for module under the html file. module.paths = module.paths.concat(Module._nodeModulePaths(global.__dirname)) } else { global.__filename = __filename global.__dirname = __dirname if (appPath) { // Search for module under the app directory module.paths = module.paths.concat(Module._nodeModulePaths(appPath)) } } if (window.location.protocol === 'https:' || window.location.protocol === 'http:' || window.location.protocol === 'ftp:') { let warning = 'This renderer process has Node.js integration enabled ' warning += 'and attempted to load remote content. This exposes users of this app to severe ' warning += 'security risks.\n' warning += 'For more information and help, consult https://electronjs.org/docs/tutorial/security' console.warn('%cElectron Security Warning', 'font-weight: bold;', warning) } // Redirect window.onerror to uncaughtException. window.onerror = function (message, filename, lineno, colno, error) { if (global.process.listeners('uncaughtException').length > 0) { global.process.emit('uncaughtException', error) return true } else { return false } } } else { // Delete Node's symbols after the Environment has been loaded. process.once('loaded', function () { delete global.process delete global.Buffer delete global.setImmediate delete global.clearImmediate delete global.global }) } // Load the script specfied by the "preload" attribute. if (preloadScript) { try { require(preloadScript) } catch (error) { console.error('Unable to load preload script: ' + preloadScript) console.error(error.stack || error.message) } } window.onload = function () { // Use menu API to show context menu. window.InspectorFrontendHost.showContextMenuAtPoint = createMenu // Use dialog API to override file chooser dialog. // Note: It will be moved to UI after Chrome 57. window.Bindings.createFileSelectorElement = createFileSelectorElement } window.confirm = function (message, title) { const {dialog} = require('electron').remote if (title == null) { title = '' } return !dialog.showMessageBox({ message: message, title: title, buttons: ['OK', 'Cancel'], cancelId: 1 }) } const convertToMenuTemplate = function (items) { return items.map(function (item) { const transformed = item.type === 'subMenu' ? { type: 'submenu', label: item.label, enabled: item.enabled, submenu: convertToMenuTemplate(item.subItems) } : item.type === 'separator' ? { type: 'separator' } : item.type === 'checkbox' ? { type: 'checkbox', label: item.label, enabled: item.enabled, checked: item.checked } : { type: 'normal', label: item.label, enabled: item.enabled } if (item.id != null) { transformed.click = function () { window.DevToolsAPI.contextMenuItemSelected(item.id) return window.DevToolsAPI.contextMenuCleared() } } return transformed }) } const createMenu = function (x, y, items) { const {remote} = require('electron') const {Menu} = remote let template = convertToMenuTemplate(items) if (useEditMenuItems(x, y, template)) { template = getEditMenuItems() } const menu = Menu.buildFromTemplate(template) // The menu is expected to show asynchronously. setTimeout(function () { menu.popup(remote.getCurrentWindow()) }) } const useEditMenuItems = function (x, y, items) { return items.length === 0 && document.elementsFromPoint(x, y).some(function (element) { return element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA' || element.isContentEditable }) } const getEditMenuItems = function () { return [ { role: 'undo' }, { role: 'redo' }, { type: 'separator' }, { role: 'cut' }, { role: 'copy' }, { role: 'paste' }, { role: 'pasteandmatchstyle' }, { role: 'delete' }, { role: 'selectall' } ] } const showFileChooserDialog = function (callback) { const {dialog} = require('electron').remote const files = dialog.showOpenDialog({}) if (files != null) { callback(pathToHtml5FileObject(files[0])) } } const pathToHtml5FileObject = function (path) { const fs = require('fs') const blob = new Blob([fs.readFileSync(path)]) blob.name = path return blob } const createFileSelectorElement = function (callback) { const fileSelectorElement = document.createElement('span') fileSelectorElement.style.display = 'none' fileSelectorElement.click = showFileChooserDialog.bind(this, callback) return fileSelectorElement } 'use strict' const {ipcRenderer} = require('electron') const {guestInstanceId, openerId} = process const hiddenPage = process.argv.includes('--hidden-page') const usesNativeWindowOpen = process.argv.includes('--native-window-open') require('./window-setup')(ipcRenderer, guestInstanceId, openerId, hiddenPage, usesNativeWindowOpen) 'use strict' const {ipcRenderer, webFrame} = require('electron') let requestId = 0 const WEB_VIEW_EVENTS = { 'load-commit': ['url', 'isMainFrame'], 'did-attach': [], 'did-finish-load': [], 'did-fail-load': ['errorCode', 'errorDescription', 'validatedURL', 'isMainFrame'], 'did-frame-finish-load': ['isMainFrame'], 'did-start-loading': [], 'did-stop-loading': [], 'did-get-response-details': ['status', 'newURL', 'originalURL', 'httpResponseCode', 'requestMethod', 'referrer', 'headers', 'resourceType'], 'did-get-redirect-request': ['oldURL', 'newURL', 'isMainFrame'], 'dom-ready': [], 'console-message': ['level', 'message', 'line', 'sourceId'], 'context-menu': ['params'], 'devtools-opened': [], 'devtools-closed': [], 'devtools-focused': [], 'new-window': ['url', 'frameName', 'disposition', 'options'], 'will-navigate': ['url'], 'did-navigate': ['url'], 'did-navigate-in-page': ['url', 'isMainFrame'], 'close': [], 'crashed': [], 'gpu-crashed': [], 'plugin-crashed': ['name', 'version'], 'destroyed': [], 'page-title-updated': ['title', 'explicitSet'], 'page-favicon-updated': ['favicons'], 'enter-html-full-screen': [], 'leave-html-full-screen': [], 'media-started-playing': [], 'media-paused': [], 'found-in-page': ['result'], 'did-change-theme-color': ['themeColor'], 'update-target-url': ['url'] } const DEPRECATED_EVENTS = { 'page-title-updated': 'page-title-set' } const dispatchEvent = function (webView, eventName, eventKey, ...args) { if (DEPRECATED_EVENTS[eventName] != null) { dispatchEvent(webView, DEPRECATED_EVENTS[eventName], eventKey, ...args) } const domEvent = new Event(eventName) WEB_VIEW_EVENTS[eventKey].forEach((prop, index) => { domEvent[prop] = args[index] }) webView.dispatchEvent(domEvent) if (eventName === 'load-commit') { webView.onLoadCommit(domEvent) } } module.exports = { registerEvents: function (webView, viewInstanceId) { ipcRenderer.on(`ELECTRON_GUEST_VIEW_INTERNAL_DESTROY_GUEST-${viewInstanceId}`, function () { webFrame.detachGuest(webView.internalInstanceId) webView.guestInstanceId = undefined webView.reset() const domEvent = new Event('destroyed') webView.dispatchEvent(domEvent) }) ipcRenderer.on(`ELECTRON_GUEST_VIEW_INTERNAL_DISPATCH_EVENT-${viewInstanceId}`, function (event, eventName, ...args) { dispatchEvent(webView, eventName, eventName, ...args) }) ipcRenderer.on(`ELECTRON_GUEST_VIEW_INTERNAL_IPC_MESSAGE-${viewInstanceId}`, function (event, channel, ...args) { const domEvent = new Event('ipc-message') domEvent.channel = channel domEvent.args = args webView.dispatchEvent(domEvent) }) ipcRenderer.on(`ELECTRON_GUEST_VIEW_INTERNAL_SIZE_CHANGED-${viewInstanceId}`, function (event, ...args) { const domEvent = new Event('size-changed') const props = ['oldWidth', 'oldHeight', 'newWidth', 'newHeight'] for (let i = 0; i < props.length; i++) { const prop = props[i] domEvent[prop] = args[i] } webView.onSizeChanged(domEvent) }) }, deregisterEvents: function (viewInstanceId) { ipcRenderer.removeAllListeners(`ELECTRON_GUEST_VIEW_INTERNAL_DESTROY_GUEST-${viewInstanceId}`) ipcRenderer.removeAllListeners(`ELECTRON_GUEST_VIEW_INTERNAL_DISPATCH_EVENT-${viewInstanceId}`) ipcRenderer.removeAllListeners(`ELECTRON_GUEST_VIEW_INTERNAL_IPC_MESSAGE-${viewInstanceId}`) ipcRenderer.removeAllListeners(`ELECTRON_GUEST_VIEW_INTERNAL_SIZE_CHANGED-${viewInstanceId}`) }, createGuest: function (params, callback) { requestId++ ipcRenderer.send('ELECTRON_GUEST_VIEW_MANAGER_CREATE_GUEST', params, requestId) ipcRenderer.once(`ELECTRON_RESPONSE_${requestId}`, callback) }, attachGuest: function (elementInstanceId, guestInstanceId, params) { ipcRenderer.send('ELECTRON_GUEST_VIEW_MANAGER_ATTACH_GUEST', elementInstanceId, guestInstanceId, params) webFrame.attachGuest(elementInstanceId) }, destroyGuest: function (guestInstanceId) { ipcRenderer.send('ELECTRON_GUEST_VIEW_MANAGER_DESTROY_GUEST', guestInstanceId) }, setSize: function (guestInstanceId, params) { ipcRenderer.send('ELECTRON_GUEST_VIEW_MANAGER_SET_SIZE', guestInstanceId, params) } } 'use strict' const WebViewImpl = require('./web-view') const guestViewInternal = require('./guest-view-internal') const webViewConstants = require('./web-view-constants') const {remote} = require('electron') // Helper function to resolve url set in attribute. const a = document.createElement('a') const resolveURL = function (url) { if (url === '') return '' a.href = url return a.href } // Attribute objects. // Default implementation of a WebView attribute. class WebViewAttribute { constructor (name, webViewImpl) { this.name = name this.value = webViewImpl.webviewNode[name] || '' this.webViewImpl = webViewImpl this.ignoreMutation = false this.defineProperty() } // Retrieves and returns the attribute's value. getValue () { return this.webViewImpl.webviewNode.getAttribute(this.name) || this.value } // Sets the attribute's value. setValue (value) { this.webViewImpl.webviewNode.setAttribute(this.name, value || '') } // Changes the attribute's value without triggering its mutation handler. setValueIgnoreMutation (value) { this.ignoreMutation = true this.setValue(value) this.ignoreMutation = false } // Defines this attribute as a property on the webview node. defineProperty () { return Object.defineProperty(this.webViewImpl.webviewNode, this.name, { get: () => { return this.getValue() }, set: (value) => { return this.setValue(value) }, enumerable: true }) } // Called when the attribute's value changes. handleMutation () {} } // An attribute that is treated as a Boolean. class BooleanAttribute extends WebViewAttribute { getValue () { return this.webViewImpl.webviewNode.hasAttribute(this.name) } setValue (value) { if (value) { this.webViewImpl.webviewNode.setAttribute(this.name, '') } else { this.webViewImpl.webviewNode.removeAttribute(this.name) } } } // Attribute used to define the demension limits of autosizing. class AutosizeDimensionAttribute extends WebViewAttribute { getValue () { return parseInt(this.webViewImpl.webviewNode.getAttribute(this.name)) || 0 } handleMutation () { if (!this.webViewImpl.guestInstanceId) { return } guestViewInternal.setSize(this.webViewImpl.guestInstanceId, { enableAutoSize: this.webViewImpl.attributes[webViewConstants.ATTRIBUTE_AUTOSIZE].getValue(), min: { width: parseInt(this.webViewImpl.attributes[webViewConstants.ATTRIBUTE_MINWIDTH].getValue() || 0), height: parseInt(this.webViewImpl.attributes[webViewConstants.ATTRIBUTE_MINHEIGHT].getValue() || 0) }, max: { width: parseInt(this.webViewImpl.attributes[webViewConstants.ATTRIBUTE_MAXWIDTH].getValue() || 0), height: parseInt(this.webViewImpl.attributes[webViewConstants.ATTRIBUTE_MAXHEIGHT].getValue() || 0) } }) } } // Attribute that specifies whether the webview should be autosized. class AutosizeAttribute extends BooleanAttribute { constructor (webViewImpl) { super(webViewConstants.ATTRIBUTE_AUTOSIZE, webViewImpl) } } AutosizeAttribute.prototype.handleMutation = AutosizeDimensionAttribute.prototype.handleMutation // Attribute representing the state of the storage partition. class PartitionAttribute extends WebViewAttribute { constructor (webViewImpl) { super(webViewConstants.ATTRIBUTE_PARTITION, webViewImpl) this.validPartitionId = true } handleMutation (oldValue, newValue) { newValue = newValue || '' // The partition cannot change if the webview has already navigated. if (!this.webViewImpl.beforeFirstNavigation) { window.console.error(webViewConstants.ERROR_MSG_ALREADY_NAVIGATED) this.setValueIgnoreMutation(oldValue) return } if (newValue === 'persist:') { this.validPartitionId = false window.console.error(webViewConstants.ERROR_MSG_INVALID_PARTITION_ATTRIBUTE) } } } // An attribute that controls the guest instance this webview is connected to class GuestInstanceAttribute extends WebViewAttribute { constructor (webViewImpl) { super(webViewConstants.ATTRIBUTE_GUESTINSTANCE, webViewImpl) } // Retrieves and returns the attribute's value. getValue () { if (this.webViewImpl.webviewNode.hasAttribute(this.name)) { return parseInt(this.webViewImpl.webviewNode.getAttribute(this.name)) } } // Sets the attribute's value. setValue (value) { if (!value) { this.webViewImpl.webviewNode.removeAttribute(this.name) } else if (!isNaN(value)) { this.webViewImpl.webviewNode.setAttribute(this.name, value) } } handleMutation (oldValue, newValue) { if (!newValue) { this.webViewImpl.reset() return } const intVal = parseInt(newValue) if (!isNaN(newValue) && remote.getGuestWebContents(intVal)) { this.webViewImpl.attachGuestInstance(intVal) } else { this.setValueIgnoreMutation(oldValue) } } } // Attribute that handles the location and navigation of the webview. class SrcAttribute extends WebViewAttribute { constructor (webViewImpl) { super(webViewConstants.ATTRIBUTE_SRC, webViewImpl) this.setupMutationObserver() } getValue () { if (this.webViewImpl.webviewNode.hasAttribute(this.name)) { return resolveURL(this.webViewImpl.webviewNode.getAttribute(this.name)) } else { return this.value } } setValueIgnoreMutation (value) { super.setValueIgnoreMutation(value) // takeRecords() is needed to clear queued up src mutations. Without it, it // is possible for this change to get picked up asyncronously by src's // mutation observer |observer|, and then get handled even though we do not // want to handle this mutation. this.observer.takeRecords() } handleMutation (oldValue, newValue) { // Once we have navigated, we don't allow clearing the src attribute. // Once enters a navigated state, it cannot return to a // placeholder state. if (!newValue && oldValue) { // src attribute changes normally initiate a navigation. We suppress // the next src attribute handler call to avoid reloading the page // on every guest-initiated navigation. this.setValueIgnoreMutation(oldValue) return } this.parse() } // The purpose of this mutation observer is to catch assignment to the src // attribute without any changes to its value. This is useful in the case // where the webview guest has crashed and navigating to the same address // spawns off a new process. setupMutationObserver () { this.observer = new MutationObserver((mutations) => { for (const mutation of mutations) { const {oldValue} = mutation const newValue = this.getValue() if (oldValue !== newValue) { return } this.handleMutation(oldValue, newValue) } }) const params = { attributes: true, attributeOldValue: true, attributeFilter: [this.name] } this.observer.observe(this.webViewImpl.webviewNode, params) } parse () { if (!this.webViewImpl.elementAttached || !this.webViewImpl.attributes[webViewConstants.ATTRIBUTE_PARTITION].validPartitionId || !this.getValue()) { return } if (this.webViewImpl.guestInstanceId == null) { if (this.webViewImpl.beforeFirstNavigation) { this.webViewImpl.beforeFirstNavigation = false this.webViewImpl.createGuest() } return } // Navigate to |this.src|. const opts = {} const httpreferrer = this.webViewImpl.attributes[webViewConstants.ATTRIBUTE_HTTPREFERRER].getValue() if (httpreferrer) { opts.httpReferrer = httpreferrer } const useragent = this.webViewImpl.attributes[webViewConstants.ATTRIBUTE_USERAGENT].getValue() if (useragent) { opts.userAgent = useragent } const guestContents = remote.getGuestWebContents(this.webViewImpl.guestInstanceId) guestContents.loadURL(this.getValue(), opts) } } // Attribute specifies HTTP referrer. class HttpReferrerAttribute extends WebViewAttribute { constructor (webViewImpl) { super(webViewConstants.ATTRIBUTE_HTTPREFERRER, webViewImpl) } } // Attribute specifies user agent class UserAgentAttribute extends WebViewAttribute { constructor (webViewImpl) { super(webViewConstants.ATTRIBUTE_USERAGENT, webViewImpl) } } // Attribute that set preload script. class PreloadAttribute extends WebViewAttribute { constructor (webViewImpl) { super(webViewConstants.ATTRIBUTE_PRELOAD, webViewImpl) } getValue () { if (!this.webViewImpl.webviewNode.hasAttribute(this.name)) { return this.value } let preload = resolveURL(this.webViewImpl.webviewNode.getAttribute(this.name)) const protocol = preload.substr(0, 5) if (protocol !== 'file:') { console.error(webViewConstants.ERROR_MSG_INVALID_PRELOAD_ATTRIBUTE) preload = '' } return preload } } // Attribute that specifies the blink features to be enabled. class BlinkFeaturesAttribute extends WebViewAttribute { constructor (webViewImpl) { super(webViewConstants.ATTRIBUTE_BLINKFEATURES, webViewImpl) } } // Attribute that specifies the blink features to be disabled. class DisableBlinkFeaturesAttribute extends WebViewAttribute { constructor (webViewImpl) { super(webViewConstants.ATTRIBUTE_DISABLEBLINKFEATURES, webViewImpl) } } // Attribute that specifies the web preferences to be enabled. class WebPreferencesAttribute extends WebViewAttribute { constructor (webViewImpl) { super(webViewConstants.ATTRIBUTE_WEBPREFERENCES, webViewImpl) } } // Sets up all of the webview attributes. WebViewImpl.prototype.setupWebViewAttributes = function () { this.attributes = {} this.attributes[webViewConstants.ATTRIBUTE_AUTOSIZE] = new AutosizeAttribute(this) this.attributes[webViewConstants.ATTRIBUTE_PARTITION] = new PartitionAttribute(this) this.attributes[webViewConstants.ATTRIBUTE_SRC] = new SrcAttribute(this) this.attributes[webViewConstants.ATTRIBUTE_HTTPREFERRER] = new HttpReferrerAttribute(this) this.attributes[webViewConstants.ATTRIBUTE_USERAGENT] = new UserAgentAttribute(this) this.attributes[webViewConstants.ATTRIBUTE_NODEINTEGRATION] = new BooleanAttribute(webViewConstants.ATTRIBUTE_NODEINTEGRATION, this) this.attributes[webViewConstants.ATTRIBUTE_PLUGINS] = new BooleanAttribute(webViewConstants.ATTRIBUTE_PLUGINS, this) this.attributes[webViewConstants.ATTRIBUTE_DISABLEWEBSECURITY] = new BooleanAttribute(webViewConstants.ATTRIBUTE_DISABLEWEBSECURITY, this) this.attributes[webViewConstants.ATTRIBUTE_ALLOWPOPUPS] = new BooleanAttribute(webViewConstants.ATTRIBUTE_ALLOWPOPUPS, this) this.attributes[webViewConstants.ATTRIBUTE_PRELOAD] = new PreloadAttribute(this) this.attributes[webViewConstants.ATTRIBUTE_BLINKFEATURES] = new BlinkFeaturesAttribute(this) this.attributes[webViewConstants.ATTRIBUTE_DISABLEBLINKFEATURES] = new DisableBlinkFeaturesAttribute(this) this.attributes[webViewConstants.ATTRIBUTE_GUESTINSTANCE] = new GuestInstanceAttribute(this) this.attributes[webViewConstants.ATTRIBUTE_DISABLEGUESTRESIZE] = new BooleanAttribute(webViewConstants.ATTRIBUTE_DISABLEGUESTRESIZE, this) this.attributes[webViewConstants.ATTRIBUTE_WEBPREFERENCES] = new WebPreferencesAttribute(this) const autosizeAttributes = [webViewConstants.ATTRIBUTE_MAXHEIGHT, webViewConstants.ATTRIBUTE_MAXWIDTH, webViewConstants.ATTRIBUTE_MINHEIGHT, webViewConstants.ATTRIBUTE_MINWIDTH] autosizeAttributes.forEach((attribute) => { this.attributes[attribute] = new AutosizeDimensionAttribute(attribute, this) }) } module.exports = { // Attributes. ATTRIBUTE_AUTOSIZE: 'autosize', ATTRIBUTE_MAXHEIGHT: 'maxheight', ATTRIBUTE_MAXWIDTH: 'maxwidth', ATTRIBUTE_MINHEIGHT: 'minheight', ATTRIBUTE_MINWIDTH: 'minwidth', ATTRIBUTE_NAME: 'name', ATTRIBUTE_PARTITION: 'partition', ATTRIBUTE_SRC: 'src', ATTRIBUTE_HTTPREFERRER: 'httpreferrer', ATTRIBUTE_NODEINTEGRATION: 'nodeintegration', ATTRIBUTE_PLUGINS: 'plugins', ATTRIBUTE_DISABLEWEBSECURITY: 'disablewebsecurity', ATTRIBUTE_ALLOWPOPUPS: 'allowpopups', ATTRIBUTE_PRELOAD: 'preload', ATTRIBUTE_USERAGENT: 'useragent', ATTRIBUTE_BLINKFEATURES: 'blinkfeatures', ATTRIBUTE_DISABLEBLINKFEATURES: 'disableblinkfeatures', ATTRIBUTE_GUESTINSTANCE: 'guestinstance', ATTRIBUTE_DISABLEGUESTRESIZE: 'disableguestresize', ATTRIBUTE_WEBPREFERENCES: 'webpreferences', // Internal attribute. ATTRIBUTE_INTERNALINSTANCEID: 'internalinstanceid', // Error messages. ERROR_MSG_ALREADY_NAVIGATED: 'The object has already navigated, so its partition cannot be changed.', ERROR_MSG_CANNOT_INJECT_SCRIPT: ': ' + 'Script cannot be injected into content until the page has loaded.', ERROR_MSG_INVALID_PARTITION_ATTRIBUTE: 'Invalid partition attribute.', ERROR_MSG_INVALID_PRELOAD_ATTRIBUTE: 'Only "file:" protocol is supported in "preload" attribute.' } 'use strict' const {ipcRenderer, remote, webFrame} = require('electron') const v8Util = process.atomBinding('v8_util') const guestViewInternal = require('./guest-view-internal') const webViewConstants = require('./web-view-constants') const hasProp = {}.hasOwnProperty // ID generator. let nextId = 0 const getNextId = function () { return ++nextId } // Represents the internal state of the WebView node. class WebViewImpl { constructor (webviewNode) { this.webviewNode = webviewNode v8Util.setHiddenValue(this.webviewNode, 'internal', this) this.attached = false this.elementAttached = false this.beforeFirstNavigation = true // on* Event handlers. this.on = {} this.browserPluginNode = this.createBrowserPluginNode() const shadowRoot = this.webviewNode.createShadowRoot() shadowRoot.innerHTML = '' this.setupWebViewAttributes() this.setupFocusPropagation() this.viewInstanceId = getNextId() shadowRoot.appendChild(this.browserPluginNode) } createBrowserPluginNode () { // We create BrowserPlugin as a custom element in order to observe changes // to attributes synchronously. const browserPluginNode = new WebViewImpl.BrowserPlugin() v8Util.setHiddenValue(browserPluginNode, 'internal', this) return browserPluginNode } // Resets some state upon reattaching element to the DOM. reset () { // If guestInstanceId is defined then the has navigated and has // already picked up a partition ID. Thus, we need to reset the initialization // state. However, it may be the case that beforeFirstNavigation is false BUT // guestInstanceId has yet to be initialized. This means that we have not // heard back from createGuest yet. We will not reset the flag in this case so // that we don't end up allocating a second guest. if (this.guestInstanceId) { guestViewInternal.destroyGuest(this.guestInstanceId) this.guestInstanceId = void 0 } this.webContents = null this.beforeFirstNavigation = true this.attributes[webViewConstants.ATTRIBUTE_PARTITION].validPartitionId = true // Set guestinstance last since this can trigger the attachedCallback to fire // when moving the webview using element.replaceChild this.attributes[webViewConstants.ATTRIBUTE_GUESTINSTANCE].setValueIgnoreMutation(undefined) } // Sets the .request property. setRequestPropertyOnWebViewNode (request) { Object.defineProperty(this.webviewNode, 'request', { value: request, enumerable: true }) } setupFocusPropagation () { if (!this.webviewNode.hasAttribute('tabIndex')) { // needs a tabIndex in order to be focusable. // TODO(fsamuel): It would be nice to avoid exposing a tabIndex attribute // to allow to be focusable. // See http://crbug.com/231664. this.webviewNode.setAttribute('tabIndex', -1) } // Focus the BrowserPlugin when the takes focus. this.webviewNode.addEventListener('focus', () => { this.browserPluginNode.focus() }) // Blur the BrowserPlugin when the loses focus. this.webviewNode.addEventListener('blur', () => { this.browserPluginNode.blur() }) } // This observer monitors mutations to attributes of the and // updates the BrowserPlugin properties accordingly. In turn, updating // a BrowserPlugin property will update the corresponding BrowserPlugin // attribute, if necessary. See BrowserPlugin::UpdateDOMAttribute for more // details. handleWebviewAttributeMutation (attributeName, oldValue, newValue) { if (!this.attributes[attributeName] || this.attributes[attributeName].ignoreMutation) { return } // Let the changed attribute handle its own mutation this.attributes[attributeName].handleMutation(oldValue, newValue) } handleBrowserPluginAttributeMutation (attributeName, oldValue, newValue) { if (attributeName === webViewConstants.ATTRIBUTE_INTERNALINSTANCEID && !oldValue && !!newValue) { this.browserPluginNode.removeAttribute(webViewConstants.ATTRIBUTE_INTERNALINSTANCEID) this.internalInstanceId = parseInt(newValue) // Track when the element resizes using the element resize callback. webFrame.registerElementResizeCallback(this.internalInstanceId, this.onElementResize.bind(this)) if (this.guestInstanceId) { guestViewInternal.attachGuest(this.internalInstanceId, this.guestInstanceId, this.buildParams()) } } } onSizeChanged (webViewEvent) { const {newHeight, newWidth} = webViewEvent const node = this.webviewNode const width = node.offsetWidth // Check the current bounds to make sure we do not resize // outside of current constraints. const maxWidth = this.attributes[webViewConstants.ATTRIBUTE_MAXWIDTH].getValue() | width const maxHeight = this.attributes[webViewConstants.ATTRIBUTE_MAXHEIGHT].getValue() | width let minWidth = this.attributes[webViewConstants.ATTRIBUTE_MINWIDTH].getValue() | width let minHeight = this.attributes[webViewConstants.ATTRIBUTE_MINHEIGHT].getValue() | width minWidth = Math.min(minWidth, maxWidth) minHeight = Math.min(minHeight, maxHeight) if (!this.attributes[webViewConstants.ATTRIBUTE_AUTOSIZE].getValue() || (newWidth >= minWidth && newWidth <= maxWidth && newHeight >= minHeight && newHeight <= maxHeight)) { node.style.width = `${newWidth}px` node.style.height = `${newHeight}px` // Only fire the DOM event if the size of the has actually // changed. this.dispatchEvent(webViewEvent) } } onElementResize (newSize) { // Dispatch the 'resize' event. const resizeEvent = new Event('resize', { bubbles: true }) // Using client size values, because when a webview is transformed `newSize` // is incorrect newSize.width = this.webviewNode.clientWidth newSize.height = this.webviewNode.clientHeight resizeEvent.newWidth = newSize.width resizeEvent.newHeight = newSize.height this.dispatchEvent(resizeEvent) if (this.guestInstanceId && !this.attributes[webViewConstants.ATTRIBUTE_DISABLEGUESTRESIZE].getValue()) { guestViewInternal.setSize(this.guestInstanceId, { normal: newSize }) } } createGuest () { return guestViewInternal.createGuest(this.buildParams(), (event, guestInstanceId) => { this.attachGuestInstance(guestInstanceId) }) } dispatchEvent (webViewEvent) { this.webviewNode.dispatchEvent(webViewEvent) } // Adds an 'on' property on the webview, which can be used to set/unset // an event handler. setupEventProperty (eventName) { const propertyName = `on${eventName.toLowerCase()}` return Object.defineProperty(this.webviewNode, propertyName, { get: () => { return this.on[propertyName] }, set: (value) => { if (this.on[propertyName]) { this.webviewNode.removeEventListener(eventName, this.on[propertyName]) } this.on[propertyName] = value if (value) { return this.webviewNode.addEventListener(eventName, value) } }, enumerable: true }) } // Updates state upon loadcommit. onLoadCommit (webViewEvent) { const oldValue = this.webviewNode.getAttribute(webViewConstants.ATTRIBUTE_SRC) const newValue = webViewEvent.url if (webViewEvent.isMainFrame && (oldValue !== newValue)) { // Touching the src attribute triggers a navigation. To avoid // triggering a page reload on every guest-initiated navigation, // we do not handle this mutation. this.attributes[webViewConstants.ATTRIBUTE_SRC].setValueIgnoreMutation(newValue) } } onAttach (storagePartitionId) { return this.attributes[webViewConstants.ATTRIBUTE_PARTITION].setValue(storagePartitionId) } buildParams () { const params = { instanceId: this.viewInstanceId, userAgentOverride: this.userAgentOverride } for (const attributeName in this.attributes) { if (hasProp.call(this.attributes, attributeName)) { params[attributeName] = this.attributes[attributeName].getValue() } } // When the WebView is not participating in layout (display:none) // then getBoundingClientRect() would report a width and height of 0. // However, in the case where the WebView has a fixed size we can // use that value to initially size the guest so as to avoid a relayout of // the on display:block. const css = window.getComputedStyle(this.webviewNode, null) const elementRect = this.webviewNode.getBoundingClientRect() params.elementWidth = parseInt(elementRect.width) || parseInt(css.getPropertyValue('width')) params.elementHeight = parseInt(elementRect.height) || parseInt(css.getPropertyValue('height')) return params } attachGuestInstance (guestInstanceId) { this.guestInstanceId = guestInstanceId this.attributes[webViewConstants.ATTRIBUTE_GUESTINSTANCE].setValueIgnoreMutation(guestInstanceId) this.webContents = remote.getGuestWebContents(this.guestInstanceId) if (!this.internalInstanceId) { return true } return guestViewInternal.attachGuest(this.internalInstanceId, this.guestInstanceId, this.buildParams()) } } // Registers browser plugin custom element. const registerBrowserPluginElement = function () { const proto = Object.create(HTMLObjectElement.prototype) proto.createdCallback = function () { this.setAttribute('type', 'application/browser-plugin') this.setAttribute('id', `browser-plugin-${getNextId()}`) // The node fills in the container. this.style.flex = '1 1 auto' } proto.attributeChangedCallback = function (name, oldValue, newValue) { const internal = v8Util.getHiddenValue(this, 'internal') if (internal) { internal.handleBrowserPluginAttributeMutation(name, oldValue, newValue) } } proto.attachedCallback = function () { // Load the plugin immediately. return this.nonExistentAttribute } WebViewImpl.BrowserPlugin = webFrame.registerEmbedderCustomElement('browserplugin', { 'extends': 'object', prototype: proto }) delete proto.createdCallback delete proto.attachedCallback delete proto.detachedCallback delete proto.attributeChangedCallback } // Registers custom element. const registerWebViewElement = function () { const proto = Object.create(HTMLObjectElement.prototype) proto.createdCallback = function () { return new WebViewImpl(this) } proto.attributeChangedCallback = function (name, oldValue, newValue) { 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 const instance = internal.attributes[webViewConstants.ATTRIBUTE_GUESTINSTANCE].getValue() if (instance) { internal.attachGuestInstance(instance) } else { internal.attributes[webViewConstants.ATTRIBUTE_SRC].parse() } } } // 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', 'undo', 'redo', 'cut', 'copy', 'paste', 'pasteAndMatchStyle', 'delete', 'selectAll', 'unselect', 'replace', 'replaceMisspelling', 'findInPage', 'stopFindInPage', 'getId', 'downloadURL', 'inspectServiceWorker', 'print', 'printToPDF', 'showDefinitionForSelection', 'capturePage', 'setZoomFactor', 'setZoomLevel', 'getZoomLevel', 'getZoomFactor' ] const nonblockMethods = [ 'insertCSS', 'insertText', 'send', 'sendInputEvent', 'setLayoutZoomLevelLimits', 'setVisualZoomLevelLimits', // TODO(kevinsawicki): Remove in 2.0, deprecate before then with warnings 'setZoomLevelLimits' ] // Forward proto.foo* method calls to WebViewImpl.foo*. const createBlockHandler = function (m) { return function (...args) { const internal = v8Util.getHiddenValue(this, 'internal') if (internal.webContents) { return internal.webContents[m](...args) } else { throw new Error(`Cannot call ${m} because the webContents is unavailable. The WebView must be attached to the DOM and the dom-ready event emitted before this method can be called.`) } } } for (const method of methods) { proto[method] = createBlockHandler(method) } const createNonBlockHandler = function (m) { return function (...args) { const internal = v8Util.getHiddenValue(this, 'internal') ipcRenderer.send('ELECTRON_BROWSER_ASYNC_CALL_TO_GUEST_VIEW', null, internal.guestInstanceId, m, ...args) } } for (const method of nonblockMethods) { proto[method] = createNonBlockHandler(method) } proto.executeJavaScript = function (code, hasUserGesture, callback) { const internal = v8Util.getHiddenValue(this, 'internal') if (typeof hasUserGesture === 'function') { callback = hasUserGesture hasUserGesture = false } const requestId = getNextId() ipcRenderer.send('ELECTRON_BROWSER_ASYNC_CALL_TO_GUEST_VIEW', requestId, internal.guestInstanceId, 'executeJavaScript', code, hasUserGesture) ipcRenderer.once(`ELECTRON_RENDERER_ASYNC_CALL_TO_GUEST_VIEW_RESPONSE_${requestId}`, function (event, result) { if (callback) callback(result) }) } // WebContents associated with this webview. proto.getWebContents = function () { return v8Util.getHiddenValue(this, 'internal').webContents } window.WebView = webFrame.registerEmbedderCustomElement('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 useCapture = true const listener = function (event) { if (document.readyState === 'loading') { return } registerBrowserPluginElement() registerWebViewElement() window.removeEventListener(event.type, listener, useCapture) } window.addEventListener('readystatechange', listener, true) module.exports = WebViewImpl // This file should have no requires since it is used by the isolated context // preload bundle. Instead arguments should be passed in for everything it // needs. // This file implements the following APIs: // - window.alert() // - window.confirm() // - window.history.back() // - window.history.forward() // - window.history.go() // - window.history.length // - window.open() // - window.opener.blur() // - window.opener.close() // - window.opener.eval() // - window.opener.focus() // - window.opener.location // - window.opener.print() // - window.opener.postMessage() // - window.prompt() // - document.hidden // - document.visibilityState 'use strict' const {defineProperty} = Object // Helper function to resolve relative url. const a = window.top.document.createElement('a') const resolveURL = function (url) { a.href = url return a.href } // Use this method to ensure values expected as strings in the main process // are convertible to strings in the renderer process. This ensures exceptions // converting values to strings are thrown in this process. const toString = (value) => { return value != null ? `${value}` : value } const windowProxies = {} const getOrCreateProxy = (ipcRenderer, guestId) => { let proxy = windowProxies[guestId] if (proxy == null) { proxy = new BrowserWindowProxy(ipcRenderer, guestId) windowProxies[guestId] = proxy } return proxy } const removeProxy = (guestId) => { delete windowProxies[guestId] } function BrowserWindowProxy (ipcRenderer, guestId) { this.closed = false defineProperty(this, 'location', { get: function () { return ipcRenderer.sendSync('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD_SYNC', guestId, 'getURL') }, set: function (url) { url = resolveURL(url) return ipcRenderer.sendSync('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD_SYNC', guestId, 'loadURL', url) } }) ipcRenderer.once(`ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_CLOSED_${guestId}`, () => { removeProxy(guestId) this.closed = true }) this.close = () => { ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_CLOSE', guestId) } this.focus = () => { ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_METHOD', guestId, 'focus') } this.blur = () => { ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_METHOD', guestId, 'blur') } this.print = () => { ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD', guestId, 'print') } this.postMessage = (message, targetOrigin) => { ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_POSTMESSAGE', guestId, message, toString(targetOrigin), window.location.origin) } this.eval = (...args) => { ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD', guestId, 'executeJavaScript', ...args) } } // 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. window.close = function () { ipcRenderer.sendSync('ELECTRON_BROWSER_WINDOW_CLOSE') } } if (!usesNativeWindowOpen) { // Make the browser window or guest view emit "new-window" event. window.open = function (url, frameName, features) { if (url != null && url !== '') { url = resolveURL(url) } const guestId = ipcRenderer.sendSync('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN', url, toString(frameName), toString(features)) if (guestId != null) { return getOrCreateProxy(ipcRenderer, guestId) } else { return null } } if (openerId != null) { window.opener = getOrCreateProxy(ipcRenderer, openerId) } } window.alert = function (message, title) { ipcRenderer.sendSync('ELECTRON_BROWSER_WINDOW_ALERT', toString(message), toString(title)) } window.confirm = function (message, title) { return ipcRenderer.sendSync('ELECTRON_BROWSER_WINDOW_CONFIRM', toString(message), toString(title)) } // But we do not support prompt(). window.prompt = function () { throw new Error('prompt() is and will not be supported.') } ipcRenderer.on('ELECTRON_GUEST_WINDOW_POSTMESSAGE', function (event, sourceId, message, sourceOrigin) { // Manually dispatch event instead of using postMessage because we also need to // set event.source. event = document.createEvent('Event') event.initEvent('message', false, false) event.data = message event.origin = sourceOrigin event.source = getOrCreateProxy(ipcRenderer, sourceId) window.dispatchEvent(event) }) window.history.back = function () { sendHistoryOperation(ipcRenderer, 'goBack') } window.history.forward = function () { sendHistoryOperation(ipcRenderer, 'goForward') } window.history.go = function (offset) { sendHistoryOperation(ipcRenderer, 'goToOffset', +offset) } defineProperty(window.history, 'length', { get: function () { return getHistoryOperation(ipcRenderer, 'length') } }) if (guestInstanceId != null) { // Webview `document.visibilityState` tracks window visibility (and ignores // the actual element visibility) for backwards compatibility. // See discussion in #9178. // // Note that this results in duplicate visibilitychange events (since // Chromium also fires them) and potentially incorrect visibility change. // We should reconsider this decision for Electron 2.0. let cachedVisibilityState = hiddenPage ? 'hidden' : 'visible' // Subscribe to visibilityState changes. ipcRenderer.on('ELECTRON_GUEST_INSTANCE_VISIBILITY_CHANGE', function (event, visibilityState) { if (cachedVisibilityState !== visibilityState) { cachedVisibilityState = visibilityState document.dispatchEvent(new Event('visibilitychange')) } }) // Make document.hidden and document.visibilityState return the correct value. defineProperty(document, 'hidden', { get: function () { return cachedVisibilityState !== 'visible' } }) defineProperty(document, 'visibilityState', { get: function () { return cachedVisibilityState } }) } } 'use strict' const path = require('path') const Module = require('module') // We modified the original process.argv to let node.js load the // init.js, we need to restore it here. process.argv.splice(1, 1) // Clear search paths. require('../common/reset-search-paths') // Import common settings. require('../common/init') // Expose public APIs. Module.globalPaths.push(path.join(__dirname, 'api', 'exports')) // Export node bindings to global. global.require = require global.module = module // Set the __filename to the path of html file if it is file: protocol. if (self.location.protocol === 'file:') { let pathname = process.platform === 'win32' && self.location.pathname[0] === '/' ? self.location.pathname.substr(1) : self.location.pathname global.__filename = path.normalize(decodeURIComponent(pathname)) global.__dirname = path.dirname(global.__filename) // Set module's filename so relative require can work as expected. module.filename = global.__filename // Also search for module under the html file. module.paths = module.paths.concat(Module._nodeModulePaths(global.__dirname)) } else { global.__filename = __filename global.__dirname = __dirname }