Repository URL to install this package:
Version:
1.16.0 ▾
|
| x q {"files":{"browser":{"files":{"api":{"files":{"app.js":{"size":2287,"offset":"0"},"auto-updater":{"files":{"auto-updater-native.js":{"size":223,"offset":"2461"},"auto-updater-win.js":{"size":1684,"offset":"2684"},"squirrel-update-win.js":{"size":3432,"offset":"4368"}}},"auto-updater.js":{"size":174,"offset":"2287"},"browser-window.js":{"size":4887,"offset":"7800"},"content-tracing.js":{"size":56,"offset":"12687"},"dialog.js":{"size":6151,"offset":"12743"},"exports":{"files":{"electron.js":{"size":2260,"offset":"18894"}}},"global-shortcut.js":{"size":71,"offset":"21154"},"ipc-main.js":{"size":183,"offset":"21225"},"menu-item-roles.js":{"size":4188,"offset":"21408"},"menu-item.js":{"size":2690,"offset":"25596"},"menu.js":{"size":8866,"offset":"28286"},"navigation-controller.js":{"size":5401,"offset":"37152"},"power-monitor.js":{"size":217,"offset":"42553"},"power-save-blocker.js":{"size":76,"offset":"42770"},"protocol.js":{"size":775,"offset":"42846"},"screen.js":{"size":186,"offset":"43621"},"session.js":{"size":495,"offset":"43807"},"system-preferences.js":{"size":242,"offset":"44302"},"tray.js":{"size":170,"offset":"44544"},"web-contents.js":{"size":8162,"offset":"44714"}}},"chrome-extension.js":{"size":12144,"offset":"52876"},"desktop-capturer.js":{"size":2630,"offset":"65020"},"guest-view-manager.js":{"size":6775,"offset":"67650"},"guest-window-manager.js":{"size":6814,"offset":"74425"},"init.js":{"size":4811,"offset":"81239"},"objects-registry.js":{"size":2743,"offset":"86050"},"rpc-server.js":{"size":13077,"offset":"88793"}}},"common":{"files":{"api":{"files":{"callbacks-registry.js":{"size":1491,"offset":"101870"},"clipboard.js":{"size":251,"offset":"103361"},"crash-reporter.js":{"size":2636,"offset":"103612"},"deprecate.js":{"size":2733,"offset":"106248"},"deprecations.js":{"size":231,"offset":"108981"},"exports":{"files":{"electron.js":{"size":1234,"offset":"109212"}}},"is-promise.js":{"size":320,"offset":"110446"},"native-image.js":{"size":53,"offset":"110766"},"shell.js":{"size":46,"offset":"110819"}}},"init.js":{"size":2026,"offset":"110865"},"reset-search-paths.js":{"size":1285,"offset":"112891"}}},"renderer":{"files":{"api":{"files":{"desktop-capturer.js":{"size":1438,"offset":"114176"},"exports":{"files":{"electron.js":{"size":767,"offset":"115614"}}},"ipc-renderer.js":{"size":1050,"offset":"116381"},"remote.js":{"size":10470,"offset":"117431"},"screen.js":{"size":51,"offset":"127901"},"web-frame.js":{"size":329,"offset":"127952"}}},"chrome-api.js":{"size":5241,"offset":"128281"},"content-scripts-injector.js":{"size":2103,"offset":"133522"},"extensions":{"files":{"event.js":{"size":406,"offset":"135625"},"i18n.js":{"size":2520,"offset":"136031"},"storage.js":{"size":1575,"offset":"138551"},"web-navigation.js":{"size":508,"offset":"140126"}}},"init.js":{"size":4555,"offset":"140634"},"inspector.js":{"size":2736,"offset":"145189"},"override.js":{"size":6972,"offset":"147925"},"web-view":{"files":{"guest-view-internal.js":{"size":4031,"offset":"154897"},"web-view-attributes.js":{"size":10334,"offset":"158928"},"web-view-constants.js":{"size":1182,"offset":"169262"},"web-view.js":{"size":16665,"offset":"170444"}}}}}}} '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') Object.setPrototypeOf(App.prototype, EventEmitter.prototype) let appPath = null Object.assign(app, { getAppPath () { return appPath }, setAppPath (path) { appPath = path }, setApplicationMenu (menu) { return Menu.setApplicationMenu(menu) }, getApplicationMenu () { return Menu.getApplicationMenu() }, commandLine: { appendSwitch: bindings.appendSwitch, appendArgument: bindings.appendArgument } }) 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') Object.setPrototypeOf(AutoUpdater.prototype, EventEmitter.prototype) 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.download(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.download = function (updateURL, callback) { return spawnUpdate(['--download', 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 {ipcMain} = require('electron') const {EventEmitter} = require('events') const {BrowserWindow} = process.atomBinding('window') 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) => { const options = { show: true, width: 800, height: 600 } ipcMain.emit('ELECTRON_GUEST_WINDOW_MANAGER_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) => { // The page-title-updated event is not emitted immediately (see #3645), so // when the callback is called the BrowserWindow might have been closed. if (this.isDestroyed()) return // Route the event to BrowserWindow. this.emit('page-title-updated', event, title) if (!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 this.webContents.send('ELECTRON_RENDERER_WINDOW_VISIBILITY_CHANGE', isVisible ? 'visible' : 'hidden') } } 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 (let window of BrowserWindow.getAllWindows()) { if (window.webContents.equal(webContents)) return window } } BrowserWindow.fromDevToolsWebContents = (webContents) => { for (let window of BrowserWindow.getAllWindows()) { if (window.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) } }) 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') var includes = [].includes const fileDialogProperties = { openFile: 1 << 0, openDirectory: 1 << 1, multiSelections: 1 << 2, createDirectory: 1 << 3, showHiddenFiles: 1 << 4 } var messageBoxTypes = ['none', 'info', 'warning', 'error', 'question'] var messageBoxOptions = { noLink: 1 << 0 } var 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 var lastArgument = args[args.length - 1] if ((callback == null) && typeof lastArgument === 'function') { callback = lastArgument } return [window, options, callback] } var checkAppInitialized = function () { if (!app.isReady()) { throw new Error('dialog module can only be used after app is ready') } } module.exports = { showOpenDialog: function (...args) { var prop, properties, value, wrappedCallback checkAppInitialized() let [window, options, callback] = parseArgs(...args) if (options == null) { options = { title: 'Open', properties: ['openFile'] } } if (options.properties == null) { options.properties = ['openFile'] } if (!Array.isArray(options.properties)) { throw new TypeError('Properties must be an array') } properties = 0 for (prop in fileDialogProperties) { value = fileDialogProperties[prop] if (includes.call(options.properties, prop)) { properties |= value } } if (options.title == null) { options.title = '' } else if (typeof options.title !== 'string') { throw new TypeError('Title must be a string') } if (options.buttonLabel == null) { options.buttonLabel = '' } else if (typeof options.buttonLabel !== 'string') { throw new TypeError('buttonLabel must be a string') } if (options.defaultPath == null) { options.defaultPath = '' } else if (typeof options.defaultPath !== 'string') { throw new TypeError('Default path must be a string') } if (options.filters == null) { options.filters = [] } wrappedCallback = typeof callback === 'function' ? function (success, result) { return callback(success ? result : void 0) } : null return binding.showOpenDialog(options.title, options.buttonLabel, options.defaultPath, options.filters, properties, window, wrappedCallback) }, showSaveDialog: function (...args) { var wrappedCallback checkAppInitialized() let [window, options, callback] = parseArgs(...args) if (options == null) { options = { title: 'Save' } } if (options.title == null) { options.title = '' } else if (typeof options.title !== 'string') { throw new TypeError('Title must be a string') } if (options.buttonLabel == null) { options.buttonLabel = '' } else if (typeof options.buttonLabel !== 'string') { throw new TypeError('buttonLabel must be a string') } if (options.defaultPath == null) { options.defaultPath = '' } else if (typeof options.defaultPath !== 'string') { throw new TypeError('Default path must be a string') } if (options.filters == null) { options.filters = [] } wrappedCallback = typeof callback === 'function' ? function (success, result) { return callback(success ? result : void 0) } : null return binding.showSaveDialog(options.title, options.buttonLabel, options.defaultPath, options.filters, window, wrappedCallback) }, showMessageBox: function (...args) { var flags, i, j, len, messageBoxType, ref2, ref3, text checkAppInitialized() let [window, options, callback] = parseArgs(...args) if (options == null) { options = { type: 'none' } } if (options.type == null) { options.type = 'none' } messageBoxType = messageBoxTypes.indexOf(options.type) if (!(messageBoxType > -1)) { throw new TypeError('Invalid message box type') } if (!Array.isArray(options.buttons)) { throw new TypeError('Buttons must be an array') } if (options.title == null) { options.title = '' } else if (typeof options.title !== 'string') { throw new TypeError('Title must be a string') } if (options.message == null) { options.message = '' } else if (typeof options.message !== 'string') { throw new TypeError('Message must be a string') } if (options.detail == null) { options.detail = '' } else if (typeof options.detail !== 'string') { throw new TypeError('Detail must be a string') } if (options.icon == null) { options.icon = null } if (options.defaultId == null) { options.defaultId = -1 } // Choose a default button to get selected when dialog is cancelled. if (options.cancelId == null) { options.cancelId = 0 ref2 = options.buttons for (i = j = 0, len = ref2.length; j < len; i = ++j) { text = ref2[i] if ((ref3 = text.toLowerCase()) === 'cancel' || ref3 === 'no') { options.cancelId = i break } } } flags = options.noLink ? messageBoxOptions.noLink : 0 return binding.showMessageBox(messageBoxType, options.buttons, options.defaultId, options.cancelId, flags, options.title, options.message, options.detail, options.icon, window, callback) }, showErrorBox: function (...args) { return binding.showErrorBox(...args) } } // Mark standard asynchronous functions. var ref1 = ['showMessageBox', 'showOpenDialog', 'showSaveDialog'] var j, len, api for (j = 0, len = ref1.length; j < len; j++) { api = ref1[j] v8Util.setHiddenValue(module.exports[api], 'asynchronous', true) } const common = require('../../../common/api/exports/electron') // Import common modules. common.defineProperties(exports) Object.defineProperties(exports, { // Browser side modules, please sort with alphabet order. app: { enumerable: true, get: function () { return require('../app') } }, autoUpdater: { enumerable: true, get: function () { return require('../auto-updater') } }, BrowserWindow: { enumerable: true, get: function () { return require('../browser-window') } }, contentTracing: { enumerable: true, get: function () { return require('../content-tracing') } }, dialog: { enumerable: true, get: function () { return require('../dialog') } }, ipcMain: { enumerable: true, get: function () { return require('../ipc-main') } }, globalShortcut: { enumerable: true, get: function () { return require('../global-shortcut') } }, Menu: { enumerable: true, get: function () { return require('../menu') } }, MenuItem: { enumerable: true, get: function () { return require('../menu-item') } }, powerMonitor: { enumerable: true, get: function () { return require('../power-monitor') } }, powerSaveBlocker: { enumerable: true, get: function () { return require('../power-save-blocker') } }, protocol: { enumerable: true, get: function () { return require('../protocol') } }, screen: { enumerable: true, get: function () { return require('../screen') } }, session: { enumerable: true, get: function () { return require('../session') } }, systemPreferences: { enumerable: true, get: function () { return require('../system-preferences') } }, Tray: { enumerable: true, get: function () { return require('../tray') } }, webContents: { enumerable: true, get: function () { return require('../web-contents') } }, // The internal modules, invisible unless you know their names. NavigationController: { get: function () { return require('../navigation-controller') } } }) module.exports = process.atomBinding('global_shortcut').globalShortcut const EventEmitter = require('events').EventEmitter module.exports = new EventEmitter() // Do not throw exception when channel name is "error". module.exports.on('error', () => {}) 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' }, 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: 'Shift+CommandOrControl+Z', webContentsMethod: 'redo' }, resetzoom: { label: 'Actual Size', accelerator: 'CommandOrControl+0', 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' }, 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', webContentsMethod: (webContents) => { webContents.getZoomLevel((zoomLevel) => { webContents.setZoomLevel(zoomLevel + 0.5) }) } }, zoomout: { label: 'Zoom Out', accelerator: 'CommandOrControl+-', webContentsMethod: (webContents) => { webContents.getZoomLevel((zoomLevel) => { webContents.setZoomLevel(zoomLevel - 0.5) }) } } } const canExecuteRole = (role) => { if (!roles.hasOwnProperty(role)) return false if (process.platform !== 'darwin') return true // macOS handles all roles natively except the ones listed below return ['zoomin', 'zoomout', 'resetzoom'].includes(role) } 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.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') this.selector = options.selector this.type = options.type this.role = options.role this.label = options.label this.sublabel = options.sublabel this.accelerator = options.accelerator this.icon = options.icon this.enabled = options.enabled this.visible = options.visible this.checked = options.checked this.submenu = options.submenu 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) { if (defaultValue == null) { 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') // Automatically generated radio menu item's group id. var nextGroupId = 0 // Search between separators to find a radio menu item and return its group id, // otherwise generate a group id. var generateGroupId = function (items, pos) { var i, item, j, k, ref1, ref2, ref3 if (pos > 0) { for (i = j = ref1 = pos - 1; ref1 <= 0 ? j <= 0 : j >= 0; i = ref1 <= 0 ? ++j : --j) { item = items[i] if (item.type === 'radio') { return item.groupId } if (item.type === 'separator') { break } } } else if (pos < items.length) { for (i = k = ref2 = pos, ref3 = items.length - 1; ref2 <= ref3 ? k <= ref3 : k >= ref3; i = ref2 <= ref3 ? ++k : --k) { item = items[i] if (item.type === 'radio') { return item.groupId } if (item.type === 'separator') { break } } } return ++nextGroupId } // Returns the index of item according to |id|. var indexOfItemById = function (items, id) { var i, item, j, len for (i = j = 0, len = items.length; j < len; i = ++j) { item = items[i] if (item.id === id) { return i } } return -1 } // Returns the index of where to insert the item according to |position|. var indexToInsertByPosition = function (items, position) { var insertIndex if (!position) { return items.length } const [query, id] = position.split('=') insertIndex = indexOfItemById(items, id) if (insertIndex === -1 && query !== 'endof') { console.warn("Item with id '" + id + "' is not found") return items.length } switch (query) { case 'after': insertIndex++ break case 'endof': // If the |id| doesn't exist, then create a new group with the |id|. if (insertIndex === -1) { items.push({ id: id, type: 'separator' }) insertIndex = items.length - 1 } // Find the end of the group. insertIndex++ while (insertIndex < items.length && items[insertIndex].type !== 'separator') { insertIndex++ } } return insertIndex } const Menu = bindings.Menu Object.setPrototypeOf(Menu.prototype, EventEmitter.prototype) Menu.prototype._init = function () { this.commandsMap = {} this.groupsMap = {} this.items = [] this.delegate = { isCommandIdChecked: (commandId) => { var command = this.commandsMap[commandId] return command != null ? command.checked : undefined }, isCommandIdEnabled: (commandId) => { var command = this.commandsMap[commandId] return command != null ? command.enabled : undefined }, isCommandIdVisible: (commandId) => { var command = this.commandsMap[commandId] return command != null ? command.visible : undefined }, getAcceleratorForCommandId: (commandId, useDefaultAccelerator) => { const command = this.commandsMap[commandId] if (command == null) return if (command.accelerator != null) return command.accelerator if (useDefaultAccelerator) return command.getDefaultRoleAccelerator() }, getIconForCommandId: (commandId) => { var command = this.commandsMap[commandId] return command != null ? command.icon : undefined }, executeCommand: (event, commandId) => { const command = this.commandsMap[commandId] if (command == null) return command.click(event, BrowserWindow.getFocusedWindow(), webContents.getFocusedWebContents()) }, menuWillShow: () => { // Make sure radio groups have at least one menu item seleted. var checked, group, id, j, len, radioItem, ref1 ref1 = this.groupsMap for (id in ref1) { group = ref1[id] checked = false for (j = 0, len = group.length; j < len; j++) { radioItem = group[j] if (!radioItem.checked) { continue } checked = true break } if (!checked) { v8Util.setHiddenValue(group[0], 'checked', true) } } } } } Menu.prototype.popup = function (window, x, y, positioningItem) { if (typeof window !== 'object' || window.constructor !== BrowserWindow) { // Shift. positioningItem = y y = x x = window window = BrowserWindow.getFocusedWindow() } // Default to showing under mouse location. if (typeof x !== 'number') x = -1 if (typeof y !== 'number') y = -1 // Default to not highlighting any item. if (typeof positioningItem !== 'number') positioningItem = -1 this.popupAt(window, x, y, positioningItem) } Menu.prototype.append = function (item) { return this.insert(this.getItemCount(), item) } Menu.prototype.insert = function (pos, item) { var base, name if ((item != null ? item.constructor : void 0) !== MenuItem) { throw new TypeError('Invalid item') } switch (item.type) { case 'normal': this.insertItem(pos, item.commandId, item.label) break case 'checkbox': this.insertCheckItem(pos, item.commandId, item.label) break case 'separator': this.insertSeparator(pos) break case 'submenu': this.insertSubMenu(pos, item.commandId, item.label, item.submenu) break case 'radio': // Grouping radio menu items. item.overrideReadOnlyProperty('groupId', generateGroupId(this.items, pos)) if ((base = this.groupsMap)[name = item.groupId] == null) { base[name] = [] } 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: function () { return v8Util.getHiddenValue(item, 'checked') }, set: () => { var j, len, otherItem, ref1 ref1 = this.groupsMap[item.groupId] for (j = 0, len = ref1.length; j < len; j++) { otherItem = ref1[j] if (otherItem !== item) { v8Util.setHiddenValue(otherItem, 'checked', false) } } return v8Util.setHiddenValue(item, 'checked', true) } }) this.insertRadioItem(pos, item.commandId, item.label, item.groupId) } if (item.sublabel != null) { this.setSublabel(pos, item.sublabel) } if (item.icon != null) { this.setIcon(pos, item.icon) } if (item.role != null) { 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 } // Force menuWillShow to be called Menu.prototype._callMenuWillShow = function () { if (this.delegate != null) { this.delegate.menuWillShow() } this.items.forEach(function (item) { if (item.submenu != null) { item.submenu._callMenuWillShow() } }) } var applicationMenu = null Menu.setApplicationMenu = function (menu) { if (!(menu === null || menu.constructor === Menu)) { throw new TypeError('Invalid menu') } // Keep a reference. applicationMenu = menu if (process.platform === 'darwin') { if (menu === null) { return } menu._callMenuWillShow() bindings.setApplicationMenu(menu) } else { BrowserWindow.getAllWindows().forEach(function (window) { window.setMenu(menu) }) } } Menu.getApplicationMenu = function () { return applicationMenu } Menu.sendActionToFirstResponder = bindings.sendActionToFirstResponder Menu.buildFromTemplate = function (template) { var insertIndex, item, j, k, key, len, len1, menu, menuItem, positionedTemplate if (!Array.isArray(template)) { throw new TypeError('Invalid template for Menu') } positionedTemplate = [] insertIndex = 0 for (j = 0, len = template.length; j < len; j++) { item = template[j] if (item.position) { insertIndex = indexToInsertByPosition(positionedTemplate, item.position) } else { // If no |position| is specified, insert after last item. insertIndex++ } positionedTemplate.splice(insertIndex, 0, item) } menu = new Menu() for (k = 0, len1 = positionedTemplate.length; k < len1; k++) { item = positionedTemplate[k] if (typeof item !== 'object') { throw new TypeError('Invalid template for MenuItem') } menuItem = new MenuItem(item) for (key in item) { // Preserve extra fields specified by user if (!menuItem.hasOwnProperty(key)) { menuItem[key] = item[key] } } menu.append(menuItem) } return menu } module.exports = Menu '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) => { var currentEntry 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) currentEntry = this.history[this.currentIndex] if (currentEntry !== url) { 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 const {EventEmitter} = require('events') const {powerMonitor, PowerMonitor} = process.atomBinding('power_monitor') Object.setPrototypeOf(PowerMonitor.prototype, EventEmitter.prototype) 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') Object.setPrototypeOf(Screen.prototype, EventEmitter.prototype) module.exports = screen const {EventEmitter} = require('events') const {app} = require('electron') const {fromPartition, Session} = 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) Session.prototype._init = function () { app.emit('session-created', this) } const {EventEmitter} = require('events') const {systemPreferences, SystemPreferences} = process.atomBinding('system_preferences') Object.setPrototypeOf(SystemPreferences.prototype, EventEmitter.prototype) module.exports = systemPreferences 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, 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 = [ 'insertText', 'setLayoutZoomLevelLimits', 'setVisualZoomLevelLimits', 'setZoomFactor', 'setZoomLevel', // TODO(kevinsawicki): Remove in 2.0, deprecate before then with warnings 'setZoomLevelLimits' ] const webFrameMethodsWithResult = [ 'getZoomFactor', 'getZoomLevel' ] const asyncWebFrameMethods = function (requestId, method, callback, ...args) { this.send('ELECTRON_INTERNAL_RENDERER_ASYNC_WEB_FRAME_METHOD', requestId, method, args) ipcMain.once(`ELECTRON_INTERNAL_BROWSER_ASYNC_WEB_FRAME_RESPONSE_${requestId}`, function (event, result) { if (callback) callback(result) }) } 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') { callback = hasUserGesture hasUserGesture = false } if (this.getURL() && !this.isLoadingMainFrame()) { asyncWebFrameMethods.call(this, requestId, 'executeJavaScript', callback, code, hasUserGesture) } else { this.once('did-finish-load', () => { asyncWebFrameMethods.call(this, requestId, 'executeJavaScript', callback, code, hasUserGesture) }) } } // 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: pageSize.height, width_microns: 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) } // 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(params.x, params.y) }) // The devtools requests the webContents to reload. this.on('devtools-reload-page', function () { this.reload() }) // Delays the page-title-updated event to next tick. this.on('-page-title-updated', function (...args) { setImmediate(() => { this.emit('page-title-updated', ...args) }) }) 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 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.`) } } // 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 `<script src="${name}"></script>` }).join('') html = new Buffer(`<html><body>${scripts}</body></html>`) } 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.getId(), tabId: tabId, timeStamp: Date.now(), url: url }) }) webContents.on('did-navigate', (event, url) => { sendToBackgroundPages('CHROME_WEBNAVIGATION_ONCOMPLETED', { frameId: 0, parentFrameId: -1, processId: webContents.getId(), 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] }) ipcMain.on('CHROME_RUNTIME_SENDMESSAGE', function (event, extensionId, message) { 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) }) ipcMain.on('CHROME_TABS_SEND_MESSAGE', function (event, tabId, extensionId, isBackgroundPage, message) { 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) }) 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.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) 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 loadedExtensionsPath = null app.on('will-quit', function () { try { const loadedExtensions = objectValues(manifestMap).map(function (manifest) { return manifest.srcDirectory }) if (loadedExtensions.length > 0) { try { fs.mkdirSync(path.dirname(loadedExtensionsPath)) } catch (error) { // Ignore error } fs.writeFileSync(loadedExtensionsPath, JSON.stringify(loadedExtensions)) } else { fs.unlinkSync(loadedExtensionsPath) } } catch (error) { // Ignore error } }) // We can not use protocol or BrowserWindow until app is ready. app.once('ready', function () { // Load persisted extensions. loadedExtensionsPath = path.join(app.getPath('userData'), 'DevTools Extensions') try { const loadedExtensions = JSON.parse(fs.readFileSync(loadedExtensionsPath)) if (Array.isArray(loadedExtensions)) { for (const srcDirectory of loadedExtensions) { // Start background pages and set content scripts. const manifest = getManifestFromPath(srcDirectory) loadExtension(manifest) } } } catch (error) { // Ignore error } // The public API to add/remove extensions. BrowserWindow.addDevToolsExtension = 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.removeDevToolsExtension = function (name) { const manifest = manifestNameMap[name] if (!manifest) return removeBackgroundPages(manifest) removeContentScripts(manifest) delete manifestMap[manifest.extensionId] delete manifestNameMap[name] } BrowserWindow.getDevToolsExtensions = function () { const extensions = {} Object.keys(manifestNameMap).forEach(function (name) { const manifest = manifestNameMap[name] extensions[name] = {name: manifest.name, version: manifest.version} }) return extensions } }) 'use strict' const ipcMain = require('electron').ipcMain const desktopCapturer = process.atomBinding('desktop_capturer').desktopCapturer var deepEqual = function (opt1, opt2) { return JSON.stringify(opt1) === JSON.stringify(opt2) } // A queue for holding all requests from renderer process. var requestsQueue = [] ipcMain.on('ELECTRON_BROWSER_DESKTOP_CAPTURER_GET_SOURCES', function (event, captureWindow, captureScreen, thumbnailSize, id) { var request request = { id: id, options: { captureWindow: captureWindow, captureScreen: captureScreen, thumbnailSize: 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', function () { request.webContents = null }) }) desktopCapturer.emit = function (event, name, sources) { // Receiving sources result from main process, now send them back to renderer. var handledRequest, i, len, ref, ref1, request, result, source, unhandledRequestsQueue handledRequest = requestsQueue.shift(0) result = (function () { var i, len, results results = [] for (i = 0, len = sources.length; i < len; i++) { source = sources[i] results.push({ id: source.id, name: source.name, thumbnail: source.thumbnail.toDataURL() }) } return results })() if ((ref = handledRequest.webContents) != null) { ref.send('ELECTRON_RENDERER_DESKTOP_CAPTURER_RESULT_' + handledRequest.id, result) } // Check the queue to see whether there is other same request. If has, handle // it for reducing redunplicated `desktopCaptuer.startHandling` calls. unhandledRequestsQueue = [] for (i = 0, len = requestsQueue.length; i < len; i++) { request = requestsQueue[i] if (deepEqual(handledRequest.options, request.options)) { if ((ref1 = request.webContents) != null) { ref1.send('ELECTRON_RENDERER_DESKTOP_CAPTURER_RESULT_' + 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 = require('electron').ipcMain const webContents = require('electron').webContents // Doesn't exist in early initialization. let webViewManager = null const supportedWebViewEvents = [ 'load-commit', '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', '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 nextInstanceId = 0 const guestInstances = {} const embedderElementsMap = {} const reverseEmbedderElementsMap = {} // Moves the last element of array to the first one. const moveLastToFirst = function (list) { return list.unshift(list.pop()) } // Generate guestInstanceId. const getNextInstanceId = function () { return ++nextInstanceId } // Create a new guest instance. const createGuest = function (embedder, params) { if (webViewManager == null) { webViewManager = process.atomBinding('web_view_manager') } const id = getNextInstanceId(embedder) const guest = webContents.create({ isGuest: true, partition: params.partition, embedder: embedder }) guestInstances[id] = { guest: guest, embedder: embedder } // Destroy guest when the embedder is gone or navigated. const destroyEvents = ['will-destroy', 'crashed', 'did-navigate'] const destroy = function () { if (guestInstances[id] != null) { destroyGuest(embedder, id) } } 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) } } guest.once('destroyed', function () { for (const event of destroyEvents) { embedder.removeListener(event, destroy) } }) // Init guest web view after attached. guest.once('did-attach', function () { let opts params = this.attachParams delete this.attachParams this.viewInstanceId = params.instanceId 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) { opts = {} if (params.httpreferrer) { opts.httpReferrer = params.httpreferrer } if (params.useragent) { opts.userAgent = params.useragent } this.loadURL(params.src, opts) } guest.allowPopups = params.allowpopups }) // Dispatch events to embedder. const fn = function (event) { guest.on(event, function (_, ...args) { embedder.send.apply(embedder, ['ELECTRON_GUEST_VIEW_INTERNAL_DISPATCH_EVENT-' + guest.viewInstanceId, event].concat(args)) }) } for (const event of supportedWebViewEvents) { fn(event) } // Dispatch guest's IPC messages to embedder. guest.on('ipc-message-host', function (_, [channel, ...args]) { embedder.send.apply(embedder, ['ELECTRON_GUEST_VIEW_INTERNAL_IPC_MESSAGE-' + guest.viewInstanceId, channel].concat(args)) }) // Autosize. guest.on('size-changed', function (_, ...args) { embedder.send.apply(embedder, ['ELECTRON_GUEST_VIEW_INTERNAL_SIZE_CHANGED-' + guest.viewInstanceId].concat(args)) }) return id } // Attach the guest to an element of embedder. const attachGuest = function (embedder, elementInstanceId, guestInstanceId, params) { let guest, key, oldGuestInstanceId, ref1, webPreferences guest = guestInstances[guestInstanceId].guest // Destroy the old guest when attaching. key = (embedder.getId()) + '-' + elementInstanceId oldGuestInstanceId = embedderElementsMap[key] if (oldGuestInstanceId != null) { // Reattachment to the same guest is not currently supported. if (oldGuestInstanceId === guestInstanceId) { return } if (guestInstances[oldGuestInstanceId] == null) { return } destroyGuest(embedder, oldGuestInstanceId) } webPreferences = { guestInstanceId: guestInstanceId, nodeIntegration: (ref1 = params.nodeintegration) != null ? ref1 : false, plugins: params.plugins, zoomFactor: params.zoomFactor, webSecurity: !params.disablewebsecurity, blinkFeatures: params.blinkfeatures, disableBlinkFeatures: params.disableblinkfeatures } if (params.preload) { webPreferences.preloadURL = params.preload } webViewManager.addGuest(guestInstanceId, elementInstanceId, embedder, guest, webPreferences) guest.attachParams = params embedderElementsMap[key] = guestInstanceId reverseEmbedderElementsMap[guestInstanceId] = key } // Destroy an existing guest instance. const destroyGuest = function (embedder, id) { webViewManager.removeGuest(embedder, id) guestInstances[id].guest.destroy() delete guestInstances[id] const key = reverseEmbedderElementsMap[id] if (key != null) { delete reverseEmbedderElementsMap[id] return delete embedderElementsMap[key] } } 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.sender, elementInstanceId, guestInstanceId, params) }) ipcMain.on('ELECTRON_GUEST_VIEW_MANAGER_DESTROY_GUEST', function (event, id) { destroyGuest(event.sender, id) }) ipcMain.on('ELECTRON_GUEST_VIEW_MANAGER_SET_SIZE', function (event, id, params) { const guestInstance = guestInstances[id] return guestInstance != null ? guestInstance.guest.setSize(params) : void 0 }) // Returns WebContents from its guest id. exports.getGuest = function (id) { const guestInstance = guestInstances[id] return guestInstance != null ? guestInstance.guest : void 0 } // Returns the embedder of the guest. exports.getEmbedder = function (id) { const guestInstance = guestInstances[id] return guestInstance != null ? guestInstance.embedder : void 0 } 'use strict' const {BrowserWindow, ipcMain, webContents} = require('electron') const {isSameOrigin} = process.atomBinding('v8_util') const hasProp = {}.hasOwnProperty const frameToGuest = {} // Copy attribute of |parent| to |child| if it is not defined in |child|. const mergeOptions = function (child, parent) { let key, value for (key in parent) { if (!hasProp.call(parent, key)) continue value = parent[key] if (!(key in child)) { if (typeof value === 'object') { child[key] = mergeOptions({}, value) } else { child[key] = value } } } return child } // Merge |options| with the |embedder|'s window's options. const mergeBrowserWindowOptions = function (embedder, options) { if (embedder.browserWindowOptions != null) { // Inherit the original options if it is a BrowserWindow. mergeOptions(options, embedder.browserWindowOptions) } else { // Or only inherit web-preferences if it is a webview. if (options.webPreferences == null) { options.webPreferences = {} } mergeOptions(options.webPreferences, embedder.getWebPreferences()) } // Disable node integration on child window if disabled on parent window if (embedder.getWebPreferences().nodeIntegration === false) { options.webPreferences.nodeIntegration = false } return options } // Create a new guest created by |embedder| with |options|. const createGuest = function (embedder, url, frameName, options) { let guest = frameToGuest[frameName] if (frameName && (guest != null)) { guest.loadURL(url) return guest.id } // Remember the embedder window's id. if (options.webPreferences == null) { options.webPreferences = {} } options.webPreferences.openerId = embedder.id guest = new BrowserWindow(options) guest.loadURL(url) // 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[frameName] = guest guest.frameName = frameName guest.once('closed', function () { delete frameToGuest[frameName] }) } return guestId } 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.getWebPreferences().openerId === sender.id) || (sender.getWebPreferences().nodeIntegration === true) || isSameOrigin(sender.getURL(), target.getURL()) } // Routed window.open messages. ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN', function (event, url, frameName, disposition, options) { options = mergeBrowserWindowOptions(event.sender, options) event.sender.emit('new-window', event, url, frameName, disposition, options) if ((event.sender.isGuest() && !event.sender.allowPopups) || event.defaultPrevented) { event.returnValue = null } else { event.returnValue = createGuest(event.sender, url, frameName, options) } }) 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) { 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 (guestContents.getURL().indexOf(targetOrigin) === 0 || 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' // 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) { return this.storage[id].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 listener = (event, deletedProcessId) => { if (deletedProcessId === webContentsId) { 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 {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 } } // 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 { throw new Error(`Attempting to call a function in a renderer window that has been closed or released. Function provided here: ${meta.location}.`) } } 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) // 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) 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 constructor = objectsRegistry.get(id)[method] // Call new with array of arguments. 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) callFunction(event, obj[method], obj, args) } catch (error) { event.returnValue = exceptionToMeta(error) } }) ipcMain.on('ELECTRON_BROWSER_MEMBER_SET', function (event, id, name, value) { try { let obj = objectsRegistry.get(id) obj[name] = value 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) 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_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 = '' event.returnValue = electron.dialog.showMessageBox(event.sender.getOwnerBrowserWindow(), { message: `${message}`, title: `${title}`, buttons: ['OK'] }) }) // Implements window.confirm(message, title) ipcMain.on('ELECTRON_BROWSER_WINDOW_CONFIRM', function (event, message, title) { if (message == null) message = '' if (title == null) title = '' event.returnValue = !electron.dialog.showMessageBox(event.sender.getOwnerBrowserWindow(), { message: `${message}`, title: `${title}`, buttons: ['OK', 'Cancel'], cancelId: 1 }) }) // Implements window.close() ipcMain.on('ELECTRON_BROWSER_WINDOW_CLOSE', function (event) { event.sender.getOwnerBrowserWindow().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. var filenameAndLine, id, location, match, ref, regexp, stackString id = v8Util.getHiddenValue(callback, 'callbackId') if (id != null) { return id } id = ++this.nextId // Capture the location of the function and put it in the ID string, // so that release errors can be tracked down easily. regexp = /at (.*)/gi stackString = (new Error()).stack while ((match = regexp.exec(stackString)) !== null) { location = match[1] if (location.indexOf('(native)') !== -1) { continue } if (location.indexOf('electron.asar') !== -1) { continue } 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) { var ref return (ref = this.callbacks[id]) != null ? ref : 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 { module.exports = process.atomBinding('clipboard') } 'use strict' const os = require('os') const path = require('path') const spawn = require('child_process').spawn const electron = require('electron') const binding = process.atomBinding('crash_reporter') var CrashReporter = (function () { function CrashReporter () {} CrashReporter.prototype.start = function (options) { var app, args, autoSubmit, companyName, env, extra, ignoreSystemCrashHandler, start, submitURL if (options == null) { options = {} } this.productName = options.productName companyName = options.companyName submitURL = options.submitURL autoSubmit = options.autoSubmit ignoreSystemCrashHandler = options.ignoreSystemCrashHandler extra = options.extra app = (process.type === 'browser' ? electron : electron.remote).app if (this.productName == null) { this.productName = app.getName() } if (autoSubmit == null) { autoSubmit = true } if (ignoreSystemCrashHandler == null) { ignoreSystemCrashHandler = false } if (extra == null) { extra = {} } if (extra._productName == null) { extra._productName = this.productName } 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') } start = () => { binding.start(this.productName, companyName, submitURL, autoSubmit, ignoreSystemCrashHandler, extra) } if (process.platform === 'win32') { args = ['--reporter-url=' + submitURL, '--application-name=' + this.productName, '--v=1'] env = { ELECTRON_INTERNAL_CRASH_SERVICE: 1 } spawn(process.execPath, args, { env: env, detached: true }) } return start() } CrashReporter.prototype.getLastCrashReport = function () { var reports reports = this.getUploadedReports() if (reports.length > 0) { return reports[0] } else { return null } } CrashReporter.prototype.getUploadedReports = function () { var log, tmpdir tmpdir = process.platform === 'win32' ? os.tmpdir() : '/tmp' log = process.platform === 'darwin' ? path.join(tmpdir, this.productName + ' Crashes') : path.join(tmpdir, this.productName + ' Crashes', 'uploads.log') return binding._getUploadedReports(log) } return CrashReporter })() module.exports = new CrashReporter() // Deprecate a method. const deprecate = function (oldName, newName, fn) { var warned warned = false return function () { if (!(warned || process.noDeprecation)) { warned = true deprecate.warn(oldName, newName) } return fn.apply(this, arguments) } } // The method is renamed. deprecate.rename = function (object, oldName, newName) { var newMethod, warned warned = false newMethod = function () { if (!(warned || process.noDeprecation)) { warned = true deprecate.warn(oldName, newName) } return this[newName].apply(this, arguments) } if (typeof object === 'function') { object.prototype[oldName] = newMethod } else { object[oldName] = newMethod } } // Forward the method to member. deprecate.member = function (object, method, member) { var warned 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 = function (object, property, method) { return Object.defineProperty(object, property, { get: function () { var warned warned = false if (!(warned || process.noDeprecation)) { warned = true deprecate.warn(property + ' property', method + ' method') } return this[method]() } }) } // Deprecate an event. deprecate.event = function (emitter, oldName, newName, fn) { var warned = false return emitter.on(newName, function (...args) { // there is listeners for old API. 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)) } } }) } // Print deprecation warning. deprecate.warn = function (oldName, newName) { return deprecate.log(oldName + ' is deprecated. Use ' + newName + ' instead.') } var deprecationHandler = null // Print deprecation message. deprecate.log = function (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 = function (handler) { deprecationHandler = handler } deprecate.getHandler = function () { return deprecationHandler } module.exports = deprecate 'use strict' const deprecate = require('electron').deprecate exports.setHandler = function (deprecationHandler) { deprecate.setHandler(deprecationHandler) } exports.getHandler = function () { return deprecate.getHandler() } // Attaches properties to |exports|. exports.defineProperties = function (exports) { return Object.defineProperties(exports, { // Common modules, please sort with alphabet order. clipboard: { // Must be enumerable, otherwise it woulde be invisible to remote module. enumerable: true, get: function () { return require('../clipboard') } }, crashReporter: { enumerable: true, get: function () { return require('../crash-reporter') } }, nativeImage: { enumerable: true, get: function () { return require('../native-image') } }, shell: { enumerable: true, get: function () { return require('../shell') } }, // The internal modules, invisible unless you know their names. CallbacksRegistry: { get: function () { return require('../callbacks-registry') } }, deprecate: { get: function () { return require('../deprecate') } }, deprecations: { get: function () { return require('../deprecations') } }, isPromise: { get: function () { return require('../is-promise') } } }) } '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 ) } module.exports = process.atomBinding('native_image') module.exports = process.atomBinding('shell') const timers = require('timers') const {binding} = process process.atomBinding = function (name) { try { return binding('atom_' + process.type + '_' + name) } catch (error) { if (/No such module/.test(error.message)) { return binding('atom_common_' + name) } } } // 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. var wrapWithActivateUvLoop = function (func) { return function () { process.activateUvLoop() return func.apply(this, arguments) } } 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.indexOf('\\Program Files\\WindowsApps\\') === 2) { process.windowsStore = true } } 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 = require('electron').ipcRenderer const nativeImage = require('electron').nativeImage var nextId = 0 var includes = [].includes var getNextId = function () { return ++nextId } // |options.type| can not be empty and has to include 'window' or 'screen'. var isValid = function (options) { return ((options != null ? options.types : void 0) != null) && Array.isArray(options.types) } exports.getSources = function (options, callback) { var captureScreen, captureWindow, id if (!isValid(options)) { return callback(new Error('Invalid options')) } captureWindow = includes.call(options.types, 'window') captureScreen = includes.call(options.types, 'screen') if (options.thumbnailSize == null) { options.thumbnailSize = { width: 150, height: 150 } } id = getNextId() ipcRenderer.send('ELECTRON_BROWSER_DESKTOP_CAPTURER_GET_SOURCES', captureWindow, captureScreen, options.thumbnailSize, id) return ipcRenderer.once('ELECTRON_RENDERER_DESKTOP_CAPTURER_RESULT_' + id, function (event, sources) { var source callback(null, (function () { var i, len, results results = [] for (i = 0, len = sources.length; i < len; i++) { source = sources[i] results.push({ id: source.id, name: source.name, thumbnail: nativeImage.createFromDataURL(source.thumbnail) }) } return results })()) }) } const common = require('../../../common/api/exports/electron') // Import common modules. common.defineProperties(exports) Object.defineProperties(exports, { // Renderer side modules, please sort with alphabet order. desktopCapturer: { enumerable: true, get: function () { return require('../desktop-capturer') } }, ipcRenderer: { enumerable: true, get: function () { return require('../ipc-renderer') } }, remote: { enumerable: true, get: function () { return require('../remote') } }, screen: { enumerable: true, get: function () { return require('../screen') } }, webFrame: { enumerable: true, get: function () { return require('../web-frame') } } }) '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) } module.exports = ipcRenderer 'use strict' const {Buffer} = require('buffer') const v8Util = process.atomBinding('v8_util') const {ipcRenderer, isPromise, CallbacksRegistry} = require('electron') const callbacksRegistry = new CallbacksRegistry() const remoteObjectCache = v8Util.createIDWeakMap() // Convert the arguments object into an array of meta data. const wrapArgs = function (args, visited) { if (visited == null) { visited = new Set() } const valueToMeta = function (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 != null ? 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') } } 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. const setObjectMembers = function (ref, object, metaId, members) { for (let member of members) { if (object.hasOwnProperty(member.name)) continue let descriptor = { enumerable: member.enumerable } if (member.type === 'method') { const remoteMemberFunction = function (...args) { if (this && this.constructor === remoteMemberFunction) { // Constructor call. let ret = ipcRenderer.sendSync('ELECTRON_BROWSER_MEMBER_CONSTRUCTOR', metaId, member.name, wrapArgs(args)) return metaToValue(ret) } else { // Call member function. let ret = ipcRenderer.sendSync('ELECTRON_BROWSER_MEMBER_CALL', metaId, member.name, wrapArgs(args)) return metaToValue(ret) } } let descriptorFunction = proxyFunctionProperties(remoteMemberFunction, metaId, member.name) descriptor.get = function () { descriptorFunction.ref = ref // The member should reference its object. return descriptorFunction } // Enable monkey-patch the method descriptor.set = function (value) { descriptorFunction = value return value } descriptor.configurable = true } else if (member.type === 'get') { descriptor.get = function () { return metaToValue(ipcRenderer.sendSync('ELECTRON_BROWSER_MEMBER_GET', metaId, member.name)) } // Only set setter when it is writable. if (member.writable) { descriptor.set = function (value) { ipcRenderer.sendSync('ELECTRON_BROWSER_MEMBER_SET', metaId, member.name, value) return value } } } Object.defineProperty(object, member.name, descriptor) } } // Populate object's prototype from descriptor. // This matches |getObjectPrototype| in rpc-server. const setObjectPrototype = function (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 const proxyFunctionProperties = function (remoteMemberFunction, metaId, name) { let loaded = false // Lazily load function properties const loadRemoteProperties = () => { if (loaded) return loaded = true const meta = ipcRenderer.sendSync('ELECTRON_BROWSER_MEMBER_GET', metaId, name) setObjectMembers(remoteMemberFunction, remoteMemberFunction, meta.id, meta.members) } return new Proxy(remoteMemberFunction, { get: (target, property, receiver) => { if (!target.hasOwnProperty(property)) loadRemoteProperties() return target[property] }, ownKeys: (target) => { loadRemoteProperties() return Object.getOwnPropertyNames(target) }, getOwnPropertyDescriptor: (target, property) => { let descriptor = Object.getOwnPropertyDescriptor(target, property) if (descriptor != null) return descriptor loadRemoteProperties() return Object.getOwnPropertyDescriptor(target, property) } }) } // Convert meta data from browser into real value. const metaToValue = function (meta) { var el, i, len, ref1, results, ret switch (meta.type) { case 'value': return meta.value case 'array': ref1 = meta.members results = [] for (i = 0, len = ref1.length; i < len; i++) { el = ref1[i] results.push(metaToValue(el)) } return results case 'buffer': return Buffer.from(meta.value) case 'promise': return Promise.resolve({ then: metaToValue(meta.then) }) case 'error': return metaToPlainObject(meta) case 'date': return new Date(meta.value) case 'exception': throw new Error(meta.message + '\n' + meta.stack) default: if (remoteObjectCache.has(meta.id)) return remoteObjectCache.get(meta.id) if (meta.type === 'function') { // A shadow class to represent the remote function object. let remoteFunction = function (...args) { if (this && this.constructor === remoteFunction) { // Constructor call. let obj = ipcRenderer.sendSync('ELECTRON_BROWSER_CONSTRUCTOR', meta.id, wrapArgs(args)) // Returning object in constructor will replace constructed object // with the returned object. // http://stackoverflow.com/questions/1978049/what-values-can-a-constructor-return-to-avoid-returning-this return metaToValue(obj) } else { // Function call. let obj = ipcRenderer.sendSync('ELECTRON_BROWSER_FUNCTION_CALL', meta.id, wrapArgs(args)) return metaToValue(obj) } } ret = remoteFunction } else { ret = {} } // Populate delegate members. setObjectMembers(ret, ret, meta.id, meta.members) // Populate delegate prototype. setObjectPrototype(ret, ret, meta.id, meta.proto) // Set constructor.name to object's name. Object.defineProperty(ret.constructor, 'name', { value: meta.name }) // Track delegate object's life time, and tell the browser to clean up // when the object is GCed. v8Util.setRemoteObjectFreer(ret, meta.id) // Remember object's id. v8Util.setHiddenValue(ret, 'atomId', meta.id) remoteObjectCache.set(meta.id, ret) return ret } } // Construct a plain object from the meta. const metaToPlainObject = function (meta) { var i, len, obj, ref1 obj = (function () { switch (meta.type) { case 'error': return new Error() default: return {} } })() ref1 = meta.members for (i = 0, len = ref1.length; i < len; i++) { let {name, value} = ref1[i] obj[name] = value } return obj } // Browser calls a callback in renderer. ipcRenderer.on('ELECTRON_RENDERER_CALLBACK', function (event, id, args) { callbacksRegistry.apply(id, metaToValue(args)) }) // A callback in browser is released. ipcRenderer.on('ELECTRON_RENDERER_RELEASE_CALLBACK', function (event, id) { callbacksRegistry.remove(id) }) // List all built-in modules in browser process. const browserModules = require('../../browser/api/exports/electron') // And add a helper receiver for each one. for (let name of Object.getOwnPropertyNames(browserModules)) { Object.defineProperty(exports, name, { get: function () { return exports.getBuiltin(name) } }) } // Get remote module. exports.require = function (module) { return metaToValue(ipcRenderer.sendSync('ELECTRON_BROWSER_REQUIRE', module)) } // Alias to remote.require('electron').xxx. exports.getBuiltin = function (module) { return metaToValue(ipcRenderer.sendSync('ELECTRON_BROWSER_GET_BUILTIN', module)) } // Get current BrowserWindow. exports.getCurrentWindow = function () { return metaToValue(ipcRenderer.sendSync('ELECTRON_BROWSER_CURRENT_WINDOW')) } // Get current WebContents object. exports.getCurrentWebContents = function () { return metaToValue(ipcRenderer.sendSync('ELECTRON_BROWSER_CURRENT_WEB_CONTENTS')) } // Get a global object in browser. exports.getGlobal = function (name) { return metaToValue(ipcRenderer.sendSync('ELECTRON_BROWSER_GLOBAL', name)) } // Get the process object in browser. exports.__defineGetter__('process', function () { return exports.getGlobal('process') }) // Create a funtion that will return the specifed value when called in browser. exports.createFunctionWithReturnValue = function (returnValue) { const func = function () { return returnValue } v8Util.setHiddenValue(func, 'returnValue', true) return func } // Get the guest WebContents from guestInstanceId. exports.getGuestWebContents = function (guestInstanceId) { const meta = ipcRenderer.sendSync('ELECTRON_BROWSER_GUEST_WEB_CONTENTS', guestInstanceId) return metaToValue(meta) } 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) // 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 || {} 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) => { chrome.runtime.onMessage.emit(message, new MessageSender(tabId, extensionId)) }) 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') { console.error('responseCallback is not supported') message = args[0] } else { [targetExtensionId, message] = args } } else { console.error('options and responseCallback are not supported') } ipcRenderer.send('CHROME_RUNTIME_SENDMESSAGE', targetExtensionId, message) }, 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) { console.error('responseCallback is not supported') } ipcRenderer.send('CHROME_TABS_SEND_MESSAGE', tabId, extensionId, isBackgroundPage, message) }, 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') 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 === '<all_urls>') return true const regexp = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$') return location.href.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 = `(function (chrome) {\n ${code}\n })` const compiledWrapper = runInThisContext(wrapper, { filename: url, lineOffset: 1, displayErrors: true }) return compiledWrapper.call(this, context.chrome) } // Run injected scripts. // https://developer.chrome.com/extensions/content_scripts const injectContentScript = function (extensionId, script) { for (const match of script.matches) { if (!matchesPattern(match)) return } 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 if (script.runAt === 'document_idle') { 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 getStorage = (storageType) => { const data = window.localStorage.getItem(`__chrome.storage.${storageType}__`) if (data != null) { return JSON.parse(data) } else { return {} } } const setStorage = (storageType, storage) => { const json = JSON.stringify(storage) window.localStorage.setItem(`__chrome.storage.${storageType}__`, json) } const scheduleCallback = (items, callback) => { setTimeout(function () { callback(items) }) } const getStorageManager = (storageType) => { return { get (keys, callback) { const storage = getStorage(storageType) if (keys == null) return scheduleCallback(storage, callback) 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 scheduleCallback({}, callback) let items = {} keys.forEach(function (key) { var value = storage[key] if (value == null) value = defaults[key] items[key] = value }) scheduleCallback(items, callback) }, set (items, callback) { const storage = getStorage(storageType) Object.keys(items).forEach(function (name) { storage[name] = items[name] }) setStorage(storageType, storage) setTimeout(callback) } } } module.exports = { sync: getStorageManager('sync'), local: getStorageManager('local') } 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') // We modified the original process.argv to let node.js load the // atom-renderer.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) => { electron.webFrame[method](...args, function (result) { event.sender.send(`ELECTRON_INTERNAL_BROWSER_ASYNC_WEB_FRAME_RESPONSE_${requestId}`, result) }) }) // Process command line arguments. let nodeIntegration = 'false' let preloadScript = null let isBackgroundPage = false 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 } } if (window.location.protocol === 'chrome-devtools:') { // Override some inspector APIs. require('./inspector') nodeIntegration = 'true' } else if (window.location.protocol === 'chrome-extension:') { // Add implementations of chrome API. require('./chrome-api').injectTo(window.location.hostname, isBackgroundPage, window) nodeIntegration = 'false' } else { // Override default web functions. require('./override') // Inject content scripts. require('./content-scripts-injector') // Load webview tag implementation. if (nodeIntegration === '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:') { var pathname = process.platform === 'win32' && window.location.pathname[0] === '/' ? window.location.pathname.substr(1) : window.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 } // 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.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. window.WebInspector.createFileSelectorElement = createFileSelectorElement } 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 {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 } // Window object returned by "window.open". const BrowserWindowProxy = (function () { BrowserWindowProxy.proxies = {} BrowserWindowProxy.getOrCreate = function (guestId) { let proxy = this.proxies[guestId] if (proxy == null) { proxy = new BrowserWindowProxy(guestId) this.proxies[guestId] = proxy } return proxy } BrowserWindowProxy.remove = function (guestId) { delete this.proxies[guestId] } function BrowserWindowProxy (guestId1) { defineProperty(this, 'guestId', { configurable: false, enumerable: true, writeable: false, value: guestId1 }) this.closed = false ipcRenderer.once('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_CLOSED_' + this.guestId, () => { BrowserWindowProxy.remove(this.guestId) this.closed = true }) } BrowserWindowProxy.prototype.close = function () { ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_CLOSE', this.guestId) } BrowserWindowProxy.prototype.focus = function () { ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_METHOD', this.guestId, 'focus') } BrowserWindowProxy.prototype.blur = function () { ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_METHOD', this.guestId, 'blur') } BrowserWindowProxy.prototype.print = function () { ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD', this.guestId, 'print') } defineProperty(BrowserWindowProxy.prototype, 'location', { get: function () { return ipcRenderer.sendSync('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD_SYNC', this.guestId, 'getURL') }, set: function (url) { url = resolveURL(url) return ipcRenderer.sendSync('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD_SYNC', this.guestId, 'loadURL', url) } }) BrowserWindowProxy.prototype.postMessage = function (message, targetOrigin) { if (targetOrigin == null) { targetOrigin = '*' } ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_POSTMESSAGE', this.guestId, message, targetOrigin, window.location.origin) } BrowserWindowProxy.prototype['eval'] = function (...args) { ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD', this.guestId, 'executeJavaScript', ...args) } return BrowserWindowProxy })() if (process.guestInstanceId == null) { // Override default window.close. window.close = function () { ipcRenderer.sendSync('ELECTRON_BROWSER_WINDOW_CLOSE') } } // Make the browser window or guest view emit "new-window" event. window.open = function (url, frameName, features) { var feature, guestId, i, j, len, len1, name, options, ref1, ref2, value if (frameName == null) { frameName = '' } if (features == null) { features = '' } options = {} const ints = ['x', 'y', 'width', 'height', 'minWidth', 'maxWidth', 'minHeight', 'maxHeight', 'zoomFactor'] const webPreferences = ['zoomFactor', 'nodeIntegration', 'preload'] const disposition = 'new-window' // Make sure to get rid of excessive whitespace in the property name ref1 = features.split(/,\s*/) for (i = 0, len = ref1.length; i < len; i++) { feature = ref1[i] ref2 = feature.split(/\s*=/) name = ref2[0] value = ref2[1] value = value === 'yes' || value === '1' ? true : value === 'no' || value === '0' ? false : value if (webPreferences.includes(name)) { if (options.webPreferences == null) { options.webPreferences = {} } options.webPreferences[name] = value } else { options[name] = 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 } // Resolve relative urls. if (url == null || url === '') { url = 'about:blank' } else { url = resolveURL(url) } for (j = 0, len1 = ints.length; j < len1; j++) { name = ints[j] if (options[name] != null) { options[name] = parseInt(options[name], 10) } } guestId = ipcRenderer.sendSync('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN', url, frameName, disposition, options) if (guestId) { return BrowserWindowProxy.getOrCreate(guestId) } else { return null } } window.alert = function (message, title) { ipcRenderer.sendSync('ELECTRON_BROWSER_WINDOW_ALERT', message, title) } window.confirm = function (message, title) { return ipcRenderer.sendSync('ELECTRON_BROWSER_WINDOW_CONFIRM', message, title) } // But we do not support prompt(). window.prompt = function () { throw new Error('prompt() is and will not be supported.') } if (process.openerId != null) { window.opener = BrowserWindowProxy.getOrCreate(process.openerId) } 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 = BrowserWindowProxy.getOrCreate(sourceId) window.dispatchEvent(event) }) // Forward history operations to browser. var sendHistoryOperation = function (...args) { ipcRenderer.send('ELECTRON_NAVIGATION_CONTROLLER', ...args) } var getHistoryOperation = function (...args) { return ipcRenderer.sendSync('ELECTRON_SYNC_NAVIGATION_CONTROLLER', ...args) } window.history.back = function () { sendHistoryOperation('goBack') } window.history.forward = function () { sendHistoryOperation('goForward') } window.history.go = function (offset) { sendHistoryOperation('goToOffset', offset) } defineProperty(window.history, 'length', { get: function () { return getHistoryOperation('length') } }) // The initial visibilityState. let cachedVisibilityState = process.argv.includes('--hidden-page') ? 'hidden' : 'visible' // Subscribe to visibilityState changes. ipcRenderer.on('ELECTRON_RENDERER_WINDOW_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 ipcRenderer = require('electron').ipcRenderer const webFrame = require('electron').webFrame var requestId = 0 var WEB_VIEW_EVENTS = { 'load-commit': ['url', 'isMainFrame'], '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'], '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'] } var DEPRECATED_EVENTS = { 'page-title-updated': 'page-title-set' } var dispatchEvent = function (webView, eventName, eventKey, ...args) { var domEvent, f, i, j, len, ref1 if (DEPRECATED_EVENTS[eventName] != null) { dispatchEvent(webView, DEPRECATED_EVENTS[eventName], eventKey, ...args) } domEvent = new Event(eventName) ref1 = WEB_VIEW_EVENTS[eventKey] for (i = j = 0, len = ref1.length; j < len; i = ++j) { f = ref1[i] domEvent[f] = args[i] } webView.dispatchEvent(domEvent) if (eventName === 'load-commit') { return webView.onLoadCommit(domEvent) } } module.exports = { registerEvents: function (webView, viewInstanceId) { 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) { var domEvent = new Event('ipc-message') domEvent.channel = channel domEvent.args = args webView.dispatchEvent(domEvent) }) return ipcRenderer.on('ELECTRON_GUEST_VIEW_INTERNAL_SIZE_CHANGED-' + viewInstanceId, function (event, ...args) { var domEvent, f, i, j, len, ref1 domEvent = new Event('size-changed') ref1 = ['oldWidth', 'oldHeight', 'newWidth', 'newHeight'] for (i = j = 0, len = ref1.length; j < len; i = ++j) { f = ref1[i] domEvent[f] = args[i] } webView.onSizeChanged(domEvent) }) }, deregisterEvents: function (viewInstanceId) { ipcRenderer.removeAllListeners('ELECTRON_GUEST_VIEW_INTERNAL_DISPATCH_EVENT-' + viewInstanceId) ipcRenderer.removeAllListeners('ELECTRON_GUEST_VIEW_INTERNAL_IPC_MESSAGE-' + viewInstanceId) return ipcRenderer.removeAllListeners('ELECTRON_GUEST_VIEW_INTERNAL_SIZE_CHANGED-' + viewInstanceId) }, createGuest: function (params, callback) { requestId++ ipcRenderer.send('ELECTRON_GUEST_VIEW_MANAGER_CREATE_GUEST', params, requestId) return ipcRenderer.once('ELECTRON_RESPONSE_' + requestId, callback) }, attachGuest: function (elementInstanceId, guestInstanceId, params) { ipcRenderer.send('ELECTRON_GUEST_VIEW_MANAGER_ATTACH_GUEST', elementInstanceId, guestInstanceId, params) return webFrame.attachGuest(elementInstanceId) }, destroyGuest: function (guestInstanceId) { return ipcRenderer.send('ELECTRON_GUEST_VIEW_MANAGER_DESTROY_GUEST', guestInstanceId) }, setSize: function (guestInstanceId, params) { return 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').remote // Helper function to resolve url set in attribute. var a = document.createElement('a') var 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) { return 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) { return this.webViewImpl.webviewNode.removeAttribute(this.name) } else { return this.webViewImpl.webviewNode.setAttribute(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 } 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 return window.console.error(webViewConstants.ERROR_MSG_INVALID_PARTITION_ATTRIBUTE) } } } // 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. return this.observer.takeRecords() } handleMutation (oldValue, newValue) { // Once we have navigated, we don't allow clearing the src attribute. // Once <webview> 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 } 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 () { var params this.observer = new MutationObserver((mutations) => { var i, len, mutation, newValue, oldValue for (i = 0, len = mutations.length; i < len; i++) { mutation = mutations[i] oldValue = mutation.oldValue newValue = this.getValue() if (oldValue !== newValue) { return } this.handleMutation(oldValue, newValue) } }) params = { attributes: true, attributeOldValue: true, attributeFilter: [this.name] } return this.observer.observe(this.webViewImpl.webviewNode, params) } parse () { var guestContents, httpreferrer, opts, useragent 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|. opts = {} httpreferrer = this.webViewImpl.attributes[webViewConstants.ATTRIBUTE_HTTPREFERRER].getValue() if (httpreferrer) { opts.httpReferrer = httpreferrer } useragent = this.webViewImpl.attributes[webViewConstants.ATTRIBUTE_USERAGENT].getValue() if (useragent) { opts.userAgent = useragent } guestContents = remote.getGuestWebContents(this.webViewImpl.guestInstanceId) return 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 () { var preload, protocol if (!this.webViewImpl.webviewNode.hasAttribute(this.name)) { return this.value } preload = resolveURL(this.webViewImpl.webviewNode.getAttribute(this.name)) 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) } } // 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) 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', // 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: '<webview>: ' + '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 webFrame = require('electron').webFrame const remote = require('electron').remote const ipcRenderer = require('electron').ipcRenderer const v8Util = process.atomBinding('v8_util') const guestViewInternal = require('./guest-view-internal') const webViewConstants = require('./web-view-constants') var hasProp = {}.hasOwnProperty // ID generator. var nextId = 0 var getNextId = function () { return ++nextId } // Represents the internal state of the WebView node. var WebViewImpl = (function () { function WebViewImpl (webviewNode) { var shadowRoot 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() shadowRoot = this.webviewNode.createShadowRoot() shadowRoot.innerHTML = '<style>:host { display: flex; }</style>' this.setupWebViewAttributes() this.setupFocusPropagation() this.viewInstanceId = getNextId() shadowRoot.appendChild(this.browserPluginNode) // Subscribe to host's zoom level changes. this.onZoomLevelChanged = (zoomLevel) => { this.webviewNode.setZoomLevel(zoomLevel) } webFrame.on('zoom-level-changed', this.onZoomLevelChanged) this.onVisibilityChanged = (event, visibilityState) => { this.webviewNode.send('ELECTRON_RENDERER_WINDOW_VISIBILITY_CHANGE', visibilityState) } ipcRenderer.on('ELECTRON_RENDERER_WINDOW_VISIBILITY_CHANGE', this.onVisibilityChanged) } WebViewImpl.prototype.createBrowserPluginNode = function () { // We create BrowserPlugin as a custom element in order to observe changes // to attributes synchronously. var browserPluginNode = new WebViewImpl.BrowserPlugin() v8Util.setHiddenValue(browserPluginNode, 'internal', this) return browserPluginNode } // Resets some state upon reattaching <webview> element to the DOM. WebViewImpl.prototype.reset = function () { // Unlisten the zoom-level-changed event. webFrame.removeListener('zoom-level-changed', this.onZoomLevelChanged) ipcRenderer.removeListener('ELECTRON_RENDERER_WINDOW_VISIBILITY_CHANGE', this.onVisibilityChanged) // If guestInstanceId is defined then the <webview> 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.webContents = null this.guestInstanceId = void 0 this.beforeFirstNavigation = true this.attributes[webViewConstants.ATTRIBUTE_PARTITION].validPartitionId = true } this.internalInstanceId = 0 } // Sets the <webview>.request property. WebViewImpl.prototype.setRequestPropertyOnWebViewNode = function (request) { return Object.defineProperty(this.webviewNode, 'request', { value: request, enumerable: true }) } WebViewImpl.prototype.setupFocusPropagation = function () { if (!this.webviewNode.hasAttribute('tabIndex')) { // <webview> needs a tabIndex in order to be focusable. // TODO(fsamuel): It would be nice to avoid exposing a tabIndex attribute // to allow <webview> to be focusable. // See http://crbug.com/231664. this.webviewNode.setAttribute('tabIndex', -1) } // Focus the BrowserPlugin when the <webview> takes focus. this.webviewNode.addEventListener('focus', () => { this.browserPluginNode.focus() }) // Blur the BrowserPlugin when the <webview> loses focus. this.webviewNode.addEventListener('blur', () => { this.browserPluginNode.blur() }) } // This observer monitors mutations to attributes of the <webview> 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. WebViewImpl.prototype.handleWebviewAttributeMutation = function (attributeName, oldValue, newValue) { if (!this.attributes[attributeName] || this.attributes[attributeName].ignoreMutation) { return } // Let the changed attribute handle its own mutation return this.attributes[attributeName].handleMutation(oldValue, newValue) } WebViewImpl.prototype.handleBrowserPluginAttributeMutation = function (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) { return } return guestViewInternal.attachGuest(this.internalInstanceId, this.guestInstanceId, this.buildParams()) } } WebViewImpl.prototype.onSizeChanged = function (webViewEvent) { var maxHeight, maxWidth, minHeight, minWidth, newHeight, newWidth, node, width newWidth = webViewEvent.newWidth newHeight = webViewEvent.newHeight node = this.webviewNode width = node.offsetWidth // Check the current bounds to make sure we do not resize <webview> // outside of current constraints. maxWidth = this.attributes[webViewConstants.ATTRIBUTE_MAXWIDTH].getValue() | width maxHeight = this.attributes[webViewConstants.ATTRIBUTE_MAXHEIGHT].getValue() | width minWidth = this.attributes[webViewConstants.ATTRIBUTE_MINWIDTH].getValue() | width 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 <webview> has actually // changed. return this.dispatchEvent(webViewEvent) } } WebViewImpl.prototype.onElementResize = function (newSize) { // Dispatch the 'resize' event. var resizeEvent 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) { return guestViewInternal.setSize(this.guestInstanceId, { normal: newSize }) } } WebViewImpl.prototype.createGuest = function () { return guestViewInternal.createGuest(this.buildParams(), (event, guestInstanceId) => { this.attachWindow(guestInstanceId) }) } WebViewImpl.prototype.dispatchEvent = function (webViewEvent) { return this.webviewNode.dispatchEvent(webViewEvent) } // Adds an 'on<event>' property on the webview, which can be used to set/unset // an event handler. WebViewImpl.prototype.setupEventProperty = function (eventName) { var propertyName 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. WebViewImpl.prototype.onLoadCommit = function (webViewEvent) { var newValue, oldValue oldValue = this.webviewNode.getAttribute(webViewConstants.ATTRIBUTE_SRC) 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. return this.attributes[webViewConstants.ATTRIBUTE_SRC].setValueIgnoreMutation(newValue) } } WebViewImpl.prototype.onAttach = function (storagePartitionId) { return this.attributes[webViewConstants.ATTRIBUTE_PARTITION].setValue(storagePartitionId) } WebViewImpl.prototype.buildParams = function () { var attribute, attributeName, css, elementRect, params, ref1 params = { instanceId: this.viewInstanceId, userAgentOverride: this.userAgentOverride, zoomFactor: webFrame.getZoomFactor() } ref1 = this.attributes for (attributeName in ref1) { if (!hasProp.call(ref1, attributeName)) continue attribute = ref1[attributeName] params[attributeName] = attribute.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. css = window.getComputedStyle(this.webviewNode, null) elementRect = this.webviewNode.getBoundingClientRect() params.elementWidth = parseInt(elementRect.width) || parseInt(css.getPropertyValue('width')) params.elementHeight = parseInt(elementRect.height) || parseInt(css.getPropertyValue('height')) return params } WebViewImpl.prototype.attachWindow = function (guestInstanceId) { this.guestInstanceId = guestInstanceId this.webContents = remote.getGuestWebContents(this.guestInstanceId) if (!this.internalInstanceId) { return true } return guestViewInternal.attachGuest(this.internalInstanceId, this.guestInstanceId, this.buildParams()) } return WebViewImpl })() // Registers browser plugin <object> custom element. var registerBrowserPluginElement = function () { var proto = Object.create(HTMLObjectElement.prototype) proto.createdCallback = function () { this.setAttribute('type', 'application/browser-plugin') this.setAttribute('id', 'browser-plugin-' + getNextId()) // The <object> node fills in the <webview> container. this.style.flex = '1 1 auto' } proto.attributeChangedCallback = function (name, oldValue, newValue) { var internal internal = v8Util.getHiddenValue(this, 'internal') if (!internal) { return } return 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 return delete proto.attributeChangedCallback } // Registers <webview> custom element. var registerWebViewElement = function () { var createBlockHandler, createNonBlockHandler, i, j, len, len1, m, methods, nonblockMethods, proto proto = Object.create(HTMLObjectElement.prototype) proto.createdCallback = function () { return new WebViewImpl(this) } proto.attributeChangedCallback = function (name, oldValue, newValue) { var internal internal = v8Util.getHiddenValue(this, 'internal') if (!internal) { return } return internal.handleWebviewAttributeMutation(name, oldValue, newValue) } proto.detachedCallback = function () { var internal internal = v8Util.getHiddenValue(this, 'internal') if (!internal) { return } guestViewInternal.deregisterEvents(internal.viewInstanceId) internal.elementAttached = false return internal.reset() } proto.attachedCallback = function () { var internal internal = v8Util.getHiddenValue(this, 'internal') if (!internal) { return } if (!internal.elementAttached) { guestViewInternal.registerEvents(internal, internal.viewInstanceId) internal.elementAttached = true return internal.attributes[webViewConstants.ATTRIBUTE_SRC].parse() } } // Public-facing API methods. 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' ] nonblockMethods = [ 'insertCSS', 'insertText', 'send', 'sendInputEvent', 'setLayoutZoomLevelLimits', 'setVisualZoomLevelLimits', 'setZoomFactor', 'setZoomLevel', // TODO(kevinsawicki): Remove in 2.0, deprecate before then with warnings 'setZoomLevelLimits' ] // Forward proto.foo* method calls to WebViewImpl.foo*. 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 (i = 0, len = methods.length; i < len; i++) { m = methods[i] proto[m] = createBlockHandler(m) } createNonBlockHandler = function (m) { return function (...args) { const internal = v8Util.getHiddenValue(this, 'internal') return ipcRenderer.send('ELECTRON_BROWSER_ASYNC_CALL_TO_GUEST_VIEW', null, internal.guestInstanceId, m, ...args) } } for (j = 0, len1 = nonblockMethods.length; j < len1; j++) { m = nonblockMethods[j] proto[m] = createNonBlockHandler(m) } proto.executeJavaScript = function (code, hasUserGesture, callback) { var internal = v8Util.getHiddenValue(this, 'internal') if (typeof hasUserGesture === 'function') { callback = hasUserGesture hasUserGesture = false } let 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 () { var internal = v8Util.getHiddenValue(this, 'internal') return 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 return delete proto.attributeChangedCallback } var useCapture = true var listener = function (event) { if (document.readyState === 'loading') { return } registerBrowserPluginElement() registerWebViewElement() return window.removeEventListener(event.type, listener, useCapture) } window.addEventListener('readystatechange', listener, true) module.exports = WebViewImpl