From 336db33d180b6028240f76675167a6c7ef831d52 Mon Sep 17 00:00:00 2001 From: Milan Burda Date: Tue, 26 Mar 2019 03:38:35 +0100 Subject: [PATCH] refactor: use ipcMainUtils.invokeInWebContents / ipcRendererUtils.handle helpers for Chrome APIs (#17417) --- lib/browser/api/web-contents.js | 4 +- lib/browser/chrome-extension.js | 69 +++++++++++---------- lib/browser/ipc-main-internal-utils.ts | 9 ++- lib/content_script/init.js | 2 +- lib/renderer/chrome-api.ts | 79 +++++++++--------------- lib/renderer/content-scripts-injector.ts | 11 ++-- lib/renderer/init.ts | 3 +- lib/sandboxed_renderer/init.js | 3 +- typings/internal-electron.d.ts | 1 + 9 files changed, 82 insertions(+), 99 deletions(-) diff --git a/lib/browser/api/web-contents.js b/lib/browser/api/web-contents.js index 4a435a98b011..3f4d2e1294cf 100644 --- a/lib/browser/api/web-contents.js +++ b/lib/browser/api/web-contents.js @@ -180,12 +180,12 @@ const webFrameMethods = [ for (const method of webFrameMethods) { WebContents.prototype[method] = function (...args) { - ipcMainUtils.invokeInWebContents(this, 'ELECTRON_INTERNAL_RENDERER_WEB_FRAME_METHOD', method, ...args) + ipcMainUtils.invokeInWebContents(this, false, 'ELECTRON_INTERNAL_RENDERER_WEB_FRAME_METHOD', method, ...args) } } const executeJavaScript = (contents, code, hasUserGesture) => { - return ipcMainUtils.invokeInWebContents(contents, 'ELECTRON_INTERNAL_RENDERER_WEB_FRAME_METHOD', 'executeJavaScript', code, hasUserGesture) + return ipcMainUtils.invokeInWebContents(contents, false, 'ELECTRON_INTERNAL_RENDERER_WEB_FRAME_METHOD', 'executeJavaScript', code, hasUserGesture) } // Make sure WebContents::executeJavaScript would run the code only when the diff --git a/lib/browser/chrome-extension.js b/lib/browser/chrome-extension.js index a96a9353c6b4..531004f7c7e7 100644 --- a/lib/browser/chrome-extension.js +++ b/lib/browser/chrome-extension.js @@ -3,7 +3,6 @@ const { app, webContents, BrowserWindow } = require('electron') const { getAllWebContents } = process.electronBinding('web_contents') const renderProcessPreferences = process.electronBinding('render_process_preferences').forAllWebContents() -const { ipcMainInternal } = require('@electron/internal/browser/ipc-main-internal') const ipcMainUtils = require('@electron/internal/browser/ipc-main-internal-utils') const { Buffer } = require('buffer') @@ -29,6 +28,10 @@ const isWindowOrWebView = function (webContents) { return type === 'window' || type === 'webview' } +const isBackgroundPage = function (webContents) { + return webContents.getType() === 'backgroundPage' +} + // Create or get manifest object from |srcDirectory|. const getManifestFromPath = function (srcDirectory) { let manifest @@ -94,7 +97,6 @@ const startBackgroundPages = function (manifest) { const contents = webContents.create({ partition: 'persist:__chrome_extension', isBackgroundPage: true, - commandLineSwitches: ['--background-page'], sandbox: true, enableRemoteModule: false }) @@ -156,21 +158,26 @@ const hookWebContentsEvents = function (webContents) { // Handle the chrome.* API messages. let nextId = 0 -ipcMainInternal.on('CHROME_RUNTIME_CONNECT', function (event, extensionId, connectInfo) { - const page = backgroundPages[extensionId] - if (!page) { - console.error(`Connect to unknown extension ${extensionId}`) - return +ipcMainUtils.handle('CHROME_RUNTIME_CONNECT', function (event, extensionId, connectInfo) { + if (isBackgroundPage(event.sender)) { + throw new Error('chrome.runtime.connect is not supported in background page') } + const page = backgroundPages[extensionId] + if (!page) { + throw new Error(`Connect to unknown extension ${extensionId}`) + } + + const tabId = page.webContents.id const portId = ++nextId - event.returnValue = { tabId: page.webContents.id, portId: portId } event.sender.once('render-view-deleted', () => { if (page.webContents.isDestroyed()) return page.webContents._sendInternalToAll(`CHROME_PORT_DISCONNECT_${portId}`) }) page.webContents._sendInternalToAll(`CHROME_RUNTIME_ONCONNECT_${extensionId}`, event.sender.id, portId, connectInfo) + + return { tabId, portId } }) ipcMainUtils.handle('CHROME_EXTENSION_MANIFEST', function (event, extensionId) { @@ -181,35 +188,28 @@ ipcMainUtils.handle('CHROME_EXTENSION_MANIFEST', function (event, extensionId) { return manifest }) -let resultID = 1 -ipcMainInternal.on('CHROME_RUNTIME_SENDMESSAGE', function (event, extensionId, message, originResultID) { +ipcMainUtils.handle('CHROME_RUNTIME_SEND_MESSAGE', async function (event, extensionId, message) { + if (isBackgroundPage(event.sender)) { + throw new Error('chrome.runtime.sendMessage is not supported in background page') + } + const page = backgroundPages[extensionId] if (!page) { - console.error(`Connect to unknown extension ${extensionId}`) - return + throw new Error(`Connect to unknown extension ${extensionId}`) } - page.webContents._sendInternalToAll(`CHROME_RUNTIME_ONMESSAGE_${extensionId}`, event.sender.id, message, resultID) - ipcMainInternal.once(`CHROME_RUNTIME_ONMESSAGE_RESULT_${resultID}`, (resultEvent, result) => { - event._replyInternal(`CHROME_RUNTIME_SENDMESSAGE_RESULT_${originResultID}`, result) - }) - resultID++ + return ipcMainUtils.invokeInWebContents(page.webContents, true, `CHROME_RUNTIME_ONMESSAGE_${extensionId}`, event.sender.id, message) }) -ipcMainInternal.on('CHROME_TABS_SEND_MESSAGE', function (event, tabId, extensionId, isBackgroundPage, message, originResultID) { +ipcMainUtils.handle('CHROME_TABS_SEND_MESSAGE', async function (event, tabId, extensionId, message) { const contents = webContents.fromId(tabId) if (!contents) { - console.error(`Sending message to unknown tab ${tabId}`) - return + throw new Error(`Sending message to unknown tab ${tabId}`) } - const senderTabId = isBackgroundPage ? null : event.sender.id + const senderTabId = isBackgroundPage(event.sender) ? null : event.sender.id - contents._sendInternalToAll(`CHROME_RUNTIME_ONMESSAGE_${extensionId}`, senderTabId, message, resultID) - ipcMainInternal.once(`CHROME_RUNTIME_ONMESSAGE_RESULT_${resultID}`, (resultEvent, result) => { - event._replyInternal(`CHROME_TABS_SEND_MESSAGE_RESULT_${originResultID}`, result) - }) - resultID++ + return ipcMainUtils.invokeInWebContents(contents, true, `CHROME_RUNTIME_ONMESSAGE_${extensionId}`, senderTabId, message) }) const getLanguage = () => { @@ -301,17 +301,20 @@ const isChromeExtension = function (pageURL) { return protocol === 'chrome-extension:' } -ipcMainInternal.on('CHROME_TABS_EXECUTESCRIPT', function (event, requestId, tabId, extensionId, details) { - const pageURL = event.sender._getURL() +const assertChromeExtension = function (contents, api) { + const pageURL = contents._getURL() if (!isChromeExtension(pageURL)) { - console.error(`Blocked ${pageURL} from calling chrome.tabs.executeScript()`) - return + console.error(`Blocked ${pageURL} from calling ${api}`) + throw new Error(`Blocked ${api}`) } +} + +ipcMainUtils.handle('CHROME_TABS_EXECUTE_SCRIPT', async function (event, tabId, extensionId, details) { + assertChromeExtension(event.sender, 'chrome.tabs.executeScript()') const contents = webContents.fromId(tabId) if (!contents) { - console.error(`Sending message to unknown tab ${tabId}`) - return + throw new Error(`Sending message to unknown tab ${tabId}`) } let code, url @@ -324,7 +327,7 @@ ipcMainInternal.on('CHROME_TABS_EXECUTESCRIPT', function (event, requestId, tabI url = `chrome-extension://${extensionId}/${String(Math.random()).substr(2, 8)}.js` } - contents._sendInternal('CHROME_TABS_EXECUTESCRIPT', event.sender.id, requestId, extensionId, url, code) + return ipcMainUtils.invokeInWebContents(contents, false, 'CHROME_TABS_EXECUTE_SCRIPT', extensionId, url, code) }) // Transfer the content scripts to renderer. diff --git a/lib/browser/ipc-main-internal-utils.ts b/lib/browser/ipc-main-internal-utils.ts index 6f7998674960..0b8eeee8d84f 100644 --- a/lib/browser/ipc-main-internal-utils.ts +++ b/lib/browser/ipc-main-internal-utils.ts @@ -26,7 +26,7 @@ export const handle = function (channel: string, handler: let nextId = 0 -export function invokeInWebContents (sender: Electron.WebContentsInternal, command: string, ...args: any[]) { +export function invokeInWebContents (sender: Electron.WebContentsInternal, sendToAll: boolean, command: string, ...args: any[]) { return new Promise((resolve, reject) => { const requestId = ++nextId const channel = `${command}_RESPONSE_${requestId}` @@ -46,6 +46,11 @@ export function invokeInWebContents (sender: Electron.WebContentsInternal, co resolve(result) } }) - sender._sendInternal(command, requestId, ...args) + + if (sendToAll) { + sender._sendInternalToAll(command, requestId, ...args) + } else { + sender._sendInternal(command, requestId, ...args) + } }) } diff --git a/lib/content_script/init.js b/lib/content_script/init.js index 1949b5d0e406..7300a718b497 100644 --- a/lib/content_script/init.js +++ b/lib/content_script/init.js @@ -33,5 +33,5 @@ const extensionId = v8Util.getHiddenValue(isolatedWorld, `extension-${worldId}`) if (extensionId) { const chromeAPI = require('@electron/internal/renderer/chrome-api') - chromeAPI.injectTo(extensionId, false, window) + chromeAPI.injectTo(extensionId, window) } diff --git a/lib/renderer/chrome-api.ts b/lib/renderer/chrome-api.ts index e5fa7dcda534..59c5a20818b6 100644 --- a/lib/renderer/chrome-api.ts +++ b/lib/renderer/chrome-api.ts @@ -67,22 +67,20 @@ class Port { } // Inject chrome API to the |context| -export function injectTo (extensionId: string, isBackgroundPage: boolean, context: any) { +export function injectTo (extensionId: string, context: any) { const chrome = context.chrome = context.chrome || {} - let originResultID = 1 ipcRendererInternal.on(`CHROME_RUNTIME_ONCONNECT_${extensionId}`, ( _event: Electron.Event, tabId: number, portId: number, connectInfo: { name: string } ) => { chrome.runtime.onConnect.emit(new Port(tabId, portId, extensionId, connectInfo.name)) - } - ) + }) - ipcRendererInternal.on(`CHROME_RUNTIME_ONMESSAGE_${extensionId}`, ( - _event: Electron.Event, tabId: number, message: string, resultID: number + ipcRendererUtils.handle(`CHROME_RUNTIME_ONMESSAGE_${extensionId}`, ( + _event: Electron.Event, tabId: number, message: string ) => { - chrome.runtime.onMessage.emit(message, new MessageSender(tabId, extensionId), (messageResult: any) => { - ipcRendererInternal.send(`CHROME_RUNTIME_ONMESSAGE_RESULT_${resultID}`, messageResult) + return new Promise(resolve => { + chrome.runtime.onMessage.emit(message, new MessageSender(tabId, extensionId), resolve) }) }) @@ -115,11 +113,6 @@ export function injectTo (extensionId: string, isBackgroundPage: boolean, contex // https://developer.chrome.com/extensions/runtime#method-connect connect (...args: Array) { - if (isBackgroundPage) { - console.error('chrome.runtime.connect is not supported in background page') - return - } - // Parse the optional args. let targetExtensionId = extensionId let connectInfo = { name: '' } @@ -135,36 +128,33 @@ export function injectTo (extensionId: string, isBackgroundPage: boolean, contex // https://developer.chrome.com/extensions/runtime#method-sendMessage sendMessage (...args: Array) { - if (isBackgroundPage) { - console.error('chrome.runtime.sendMessage is not supported in background page') - return + // Parse the optional args. + const targetExtensionId = extensionId + let message: string + let options: Object | undefined + let responseCallback: Chrome.Tabs.SendMessageCallback = () => {} + + if (typeof args[args.length - 1] === 'function') { + responseCallback = args.pop() } - // Parse the optional args. - let targetExtensionId = extensionId - let message if (args.length === 1) { - message = args[0] + [message] = args } else if (args.length === 2) { - // A case of not provide extension-id: (message, responseCallback) - if (typeof args[1] === 'function') { - ipcRendererInternal.once(`CHROME_RUNTIME_SENDMESSAGE_RESULT_${originResultID}`, - (_event: Electron.Event, result: any) => args[1](result) - ) - - message = args[0] + if (typeof args[0] === 'string') { + [extensionId, message] = args } else { - [targetExtensionId, message] = args + [message, options] = args } } else { - console.error('options is not supported') - ipcRendererInternal.once(`CHROME_RUNTIME_SENDMESSAGE_RESULT_${originResultID}`, - (event: Electron.Event, result: any) => args[2](result) - ) + [extensionId, message, options] = args } - ipcRendererInternal.send('CHROME_RUNTIME_SENDMESSAGE', targetExtensionId, message, originResultID) - originResultID++ + if (options) { + console.error('options are not supported') + } + + ipcRendererUtils.invoke('CHROME_RUNTIME_SEND_MESSAGE', targetExtensionId, message).then(responseCallback) }, onConnect: new Event(), @@ -177,15 +167,10 @@ export function injectTo (extensionId: string, isBackgroundPage: boolean, contex executeScript ( tabId: number, details: Chrome.Tabs.ExecuteScriptDetails, - resultCallback: Chrome.Tabs.ExecuteScriptCallback + resultCallback: Chrome.Tabs.ExecuteScriptCallback = () => {} ) { - if (resultCallback) { - ipcRendererInternal.once(`CHROME_TABS_EXECUTESCRIPT_RESULT_${originResultID}`, - (_event: Electron.Event, result: any) => resultCallback([result]) - ) - } - ipcRendererInternal.send('CHROME_TABS_EXECUTESCRIPT', originResultID, tabId, extensionId, details) - originResultID++ + ipcRendererUtils.invoke('CHROME_TABS_EXECUTE_SCRIPT', tabId, extensionId, details) + .then((result: any) => resultCallback([result])) }, // https://developer.chrome.com/extensions/tabs#method-sendMessage @@ -193,15 +178,9 @@ export function injectTo (extensionId: string, isBackgroundPage: boolean, contex tabId: number, message: any, _options: Chrome.Tabs.SendMessageDetails, - responseCallback: Chrome.Tabs.SendMessageCallback + responseCallback: Chrome.Tabs.SendMessageCallback = () => {} ) { - if (responseCallback) { - ipcRendererInternal.once(`CHROME_TABS_SEND_MESSAGE_RESULT_${originResultID}`, - (_event: Electron.Event, result: any) => responseCallback(result) - ) - } - ipcRendererInternal.send('CHROME_TABS_SEND_MESSAGE', tabId, extensionId, isBackgroundPage, message, originResultID) - originResultID++ + ipcRendererUtils.invoke('CHROME_TABS_SEND_MESSAGE', tabId, extensionId, message).then(responseCallback) }, onUpdated: new Event(), diff --git a/lib/renderer/content-scripts-injector.ts b/lib/renderer/content-scripts-injector.ts index 106688285524..f1aa98f99c10 100644 --- a/lib/renderer/content-scripts-injector.ts +++ b/lib/renderer/content-scripts-injector.ts @@ -1,6 +1,7 @@ -import { ipcRendererInternal } from '@electron/internal/renderer/ipc-renderer-internal' import { webFrame } from 'electron' +import * as ipcRendererUtils from '@electron/internal/renderer/ipc-renderer-internal-utils' + const v8Util = process.electronBinding('v8_util') const IsolatedWorldIDs = { @@ -94,17 +95,13 @@ const injectContentScript = function (extensionId: string, script: Electron.Cont } // Handle the request of chrome.tabs.executeJavaScript. -ipcRendererInternal.on('CHROME_TABS_EXECUTESCRIPT', function ( +ipcRendererUtils.handle('CHROME_TABS_EXECUTE_SCRIPT', function ( event: Electron.Event, - senderWebContentsId: number, - requestId: number, extensionId: string, url: string, code: string ) { - runContentScript.call(window, extensionId, url, code).then(result => { - ipcRendererInternal.sendToAll(senderWebContentsId, `CHROME_TABS_EXECUTESCRIPT_RESULT_${requestId}`, result) - }) + return runContentScript.call(window, extensionId, url, code) }) module.exports = (getRenderProcessPreferences: typeof process.getRenderProcessPreferences) => { diff --git a/lib/renderer/init.ts b/lib/renderer/init.ts index 04487f80d3ea..8f0e1af51764 100644 --- a/lib/renderer/init.ts +++ b/lib/renderer/init.ts @@ -49,7 +49,6 @@ const contextIsolation = hasSwitch('context-isolation') const nodeIntegration = hasSwitch('node-integration') const webviewTag = hasSwitch('webview-tag') const isHiddenPage = hasSwitch('hidden-page') -const isBackgroundPage = hasSwitch('background-page') const usesNativeWindowOpen = hasSwitch('native-window-open') const preloadScript = parseOption('preload', null) @@ -74,7 +73,7 @@ switch (window.location.protocol) { } case 'chrome-extension:': { // Inject the chrome.* APIs that chrome extensions require - require('@electron/internal/renderer/chrome-api').injectTo(window.location.hostname, isBackgroundPage, window) + require('@electron/internal/renderer/chrome-api').injectTo(window.location.hostname, window) break } case 'chrome:': diff --git a/lib/sandboxed_renderer/init.js b/lib/sandboxed_renderer/init.js index 6e37445d6a4b..1eff5fb7b3e3 100644 --- a/lib/sandboxed_renderer/init.js +++ b/lib/sandboxed_renderer/init.js @@ -105,7 +105,6 @@ function preloadRequire (module) { // Process command line arguments. const { hasSwitch } = process.electronBinding('command_line') -const isBackgroundPage = hasSwitch('background-page') const contextIsolation = hasSwitch('context-isolation') switch (window.location.protocol) { @@ -116,7 +115,7 @@ switch (window.location.protocol) { } case 'chrome-extension:': { // Inject the chrome.* APIs that chrome extensions require - require('@electron/internal/renderer/chrome-api').injectTo(window.location.hostname, isBackgroundPage, window) + require('@electron/internal/renderer/chrome-api').injectTo(window.location.hostname, window) break } case 'chrome': { diff --git a/typings/internal-electron.d.ts b/typings/internal-electron.d.ts index 471534bb1527..e41ac13df9b2 100644 --- a/typings/internal-electron.d.ts +++ b/typings/internal-electron.d.ts @@ -61,6 +61,7 @@ declare namespace Electron { interface WebContentsInternal extends Electron.WebContents { _sendInternal(channel: string, ...args: any[]): void; + _sendInternalToAll(channel: string, ...args: any[]): void; } const deprecate: ElectronInternal.DeprecationUtil;