From f29598d9078957477f7b153ae9a0c0094fd1536e Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Thu, 26 May 2016 16:10:05 +0900 Subject: [PATCH 01/38] getHostForPath => generateHostForPath The original name implies no side effect, but is is not true. --- lib/browser/chrome-extension.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/browser/chrome-extension.js b/lib/browser/chrome-extension.js index 6924f9a7d509..7a1d267cd045 100644 --- a/lib/browser/chrome-extension.js +++ b/lib/browser/chrome-extension.js @@ -7,7 +7,7 @@ const url = require('url') var hostPathMap = {} var hostPathMapNextKey = 0 -var getHostForPath = function (path) { +var generateHostForPath = function (path) { var key key = 'extension-' + (++hostPathMapNextKey) hostPathMap[key] = path @@ -30,7 +30,7 @@ var getExtensionInfoFromPath = function (srcDirectory) { page = url.format({ protocol: 'chrome-extension', slashes: true, - hostname: getHostForPath(srcDirectory), + hostname: generateHostForPath(srcDirectory), pathname: manifest.devtools_page }) extensionInfoMap[manifest.name] = { From 513b66f2079b4871028d391305ee9de13c54c12a Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Thu, 26 May 2016 16:34:57 +0900 Subject: [PATCH 02/38] Cleanup chrome-extension.js after the Coffe2ES transfer --- lib/browser/chrome-extension.js | 106 +++++++++++++++----------------- 1 file changed, 48 insertions(+), 58 deletions(-) diff --git a/lib/browser/chrome-extension.js b/lib/browser/chrome-extension.js index 7a1d267cd045..26734ca809e8 100644 --- a/lib/browser/chrome-extension.js +++ b/lib/browser/chrome-extension.js @@ -3,31 +3,34 @@ const fs = require('fs') const path = require('path') const url = require('url') -// Mapping between hostname and file path. -var hostPathMap = {} -var hostPathMapNextKey = 0 +// Remove this when we have Object.values(). +const objectValues = function (object) { + return Object.keys(object).map(function (key) { return object[key] }) +} -var generateHostForPath = function (path) { - var key - key = 'extension-' + (++hostPathMapNextKey) +// Mapping between hostname and file path. +let hostPathMap = {} +let hostPathMapNextKey = 0 + +const generateHostForPath = function (path) { + let key = `extension-${++hostPathMapNextKey}` hostPathMap[key] = path return key } -var getPathForHost = function (host) { +const getPathForHost = function (host) { return hostPathMap[host] } // Cache extensionInfo. -var extensionInfoMap = {} +let extensionInfoMap = {} -var getExtensionInfoFromPath = function (srcDirectory) { - var manifest, page - manifest = JSON.parse(fs.readFileSync(path.join(srcDirectory, 'manifest.json'))) +const getExtensionInfoFromPath = function (srcDirectory) { + let manifest = JSON.parse(fs.readFileSync(path.join(srcDirectory, 'manifest.json'))) if (extensionInfoMap[manifest.name] == null) { // We can not use 'file://' directly because all resources in the extension // will be treated as relative to the root in Chrome. - page = url.format({ + let page = url.format({ protocol: 'chrome-extension', slashes: true, hostname: generateHostForPath(srcDirectory), @@ -43,13 +46,18 @@ var getExtensionInfoFromPath = function (srcDirectory) { } } -// The loaded extensions cache and its persistent path. -var loadedExtensions = null -var loadedExtensionsPath = null +// Load the extensions for the window. +const loadDevToolsExtensions = function (win, extensionInfoArray) { + if (!win.devToolsWebContents) return + win.devToolsWebContents.executeJavaScript(`DevToolsAPI.addExtensions(${JSON.stringify(extensionInfoArray)})`) +} + +// The persistent path of "DevTools Extensions" preference file. +let loadedExtensionsPath = null app.on('will-quit', function () { try { - loadedExtensions = Object.keys(extensionInfoMap).map(function (key) { + let loadedExtensions = Object.keys(extensionInfoMap).map(function (key) { return extensionInfoMap[key].srcDirectory }) if (loadedExtensions.length > 0) { @@ -69,74 +77,56 @@ app.on('will-quit', function () { // We can not use protocol or BrowserWindow until app is ready. app.once('ready', function () { - var chromeExtensionHandler, i, init, len, srcDirectory - // Load persisted extensions. loadedExtensionsPath = path.join(app.getPath('userData'), 'DevTools Extensions') try { - loadedExtensions = JSON.parse(fs.readFileSync(loadedExtensionsPath)) - if (!Array.isArray(loadedExtensions)) { - loadedExtensions = [] - } - - // Preheat the extensionInfo cache. - for (i = 0, len = loadedExtensions.length; i < len; i++) { - srcDirectory = loadedExtensions[i] - getExtensionInfoFromPath(srcDirectory) + let loadedExtensions = JSON.parse(fs.readFileSync(loadedExtensionsPath)) + if (Array.isArray(loadedExtensions)) { + // Preheat the extensionInfo cache. + for (let srcDirectory of loadedExtensions) { + getExtensionInfoFromPath(srcDirectory) + } } } catch (error) { // Ignore error } // The chrome-extension: can map a extension URL request to real file path. - chromeExtensionHandler = function (request, callback) { - var directory, parsed - parsed = url.parse(request.url) - if (!(parsed.hostname && (parsed.path != null))) { - return callback() - } - if (!/extension-\d+/.test(parsed.hostname)) { - return callback() - } - directory = getPathForHost(parsed.hostname) - if (directory == null) { - return callback() - } - return callback(path.join(directory, parsed.path)) + const chromeExtensionHandler = function (request, callback) { + let parsed = url.parse(request.url) + if (!parsed.hostname || !parsed.path) return callback() + if (!/extension-\d+/.test(parsed.hostname)) return callback() + + let directory = getPathForHost(parsed.hostname) + if (!directory) return callback() + + callback(path.join(directory, parsed.path)) } protocol.registerFileProtocol('chrome-extension', chromeExtensionHandler, function (error) { if (error) { - return console.error('Unable to register chrome-extension protocol') + console.error(`Unable to register chrome-extension protocol: ${error}`) } }) - BrowserWindow.prototype._loadDevToolsExtensions = function (extensionInfoArray) { - var ref - return (ref = this.devToolsWebContents) != null ? ref.executeJavaScript('DevToolsAPI.addExtensions(' + (JSON.stringify(extensionInfoArray)) + ');') : void 0 - } + BrowserWindow.addDevToolsExtension = function (srcDirectory) { - var extensionInfo, j, len1, ref, window - extensionInfo = getExtensionInfoFromPath(srcDirectory) + let extensionInfo = getExtensionInfoFromPath(srcDirectory) if (extensionInfo) { - ref = BrowserWindow.getAllWindows() - for (j = 0, len1 = ref.length; j < len1; j++) { - window = ref[j] - window._loadDevToolsExtensions([extensionInfo]) + for (let win of BrowserWindow.getAllWindows()) { + loadDevToolsExtensions(win, [extensionInfo]) } return extensionInfo.name } } BrowserWindow.removeDevToolsExtension = function (name) { - return delete extensionInfoMap[name] + delete extensionInfoMap[name] } // Load persisted extensions when devtools is opened. - init = BrowserWindow.prototype._init + let init = BrowserWindow.prototype._init BrowserWindow.prototype._init = function () { init.call(this) - return this.webContents.on('devtools-opened', () => { - return this._loadDevToolsExtensions(Object.keys(extensionInfoMap).map(function (key) { - return extensionInfoMap[key] - })) + this.webContents.on('devtools-opened', () => { + loadDevToolsExtensions(this, objectValues(extensionInfoMap)) }) } }) From 99c1434051026399a125706a8e113d67bf15da83 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Thu, 26 May 2016 16:57:23 +0900 Subject: [PATCH 03/38] Store the original manifest file --- lib/browser/chrome-extension.js | 52 +++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/lib/browser/chrome-extension.js b/lib/browser/chrome-extension.js index 26734ca809e8..e882b1862d4e 100644 --- a/lib/browser/chrome-extension.js +++ b/lib/browser/chrome-extension.js @@ -3,7 +3,7 @@ const fs = require('fs') const path = require('path') const url = require('url') -// Remove this when we have Object.values(). +// TODO(zcbenz): Remove this when we have Object.values(). const objectValues = function (object) { return Object.keys(object).map(function (key) { return object[key] }) } @@ -22,27 +22,23 @@ const getPathForHost = function (host) { return hostPathMap[host] } -// Cache extensionInfo. -let extensionInfoMap = {} +// Cache manifests. +let manifestMap = {} -const getExtensionInfoFromPath = function (srcDirectory) { +const getManifestFromPath = function (srcDirectory) { let manifest = JSON.parse(fs.readFileSync(path.join(srcDirectory, 'manifest.json'))) - if (extensionInfoMap[manifest.name] == null) { + if (!manifestMap[manifest.name]) { + manifestMap[manifest.name] = manifest // We can not use 'file://' directly because all resources in the extension // will be treated as relative to the root in Chrome. - let page = url.format({ + manifest.startPage = url.format({ protocol: 'chrome-extension', slashes: true, hostname: generateHostForPath(srcDirectory), pathname: manifest.devtools_page }) - extensionInfoMap[manifest.name] = { - startPage: page, - name: manifest.name, - srcDirectory: srcDirectory, - exposeExperimentalAPIs: true - } - return extensionInfoMap[manifest.name] + manifest.srcDirectory = srcDirectory + return manifest } } @@ -52,13 +48,24 @@ const loadDevToolsExtensions = function (win, extensionInfoArray) { win.devToolsWebContents.executeJavaScript(`DevToolsAPI.addExtensions(${JSON.stringify(extensionInfoArray)})`) } +// 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 + } +} + // The persistent path of "DevTools Extensions" preference file. let loadedExtensionsPath = null app.on('will-quit', function () { try { - let loadedExtensions = Object.keys(extensionInfoMap).map(function (key) { - return extensionInfoMap[key].srcDirectory + let loadedExtensions = objectValues(manifestMap).map(function (manifest) { + return manifest.srcDirectory }) if (loadedExtensions.length > 0) { try { @@ -82,9 +89,9 @@ app.once('ready', function () { try { let loadedExtensions = JSON.parse(fs.readFileSync(loadedExtensionsPath)) if (Array.isArray(loadedExtensions)) { - // Preheat the extensionInfo cache. + // Preheat the manifest cache. for (let srcDirectory of loadedExtensions) { - getExtensionInfoFromPath(srcDirectory) + getManifestFromPath(srcDirectory) } } } catch (error) { @@ -109,16 +116,17 @@ app.once('ready', function () { }) BrowserWindow.addDevToolsExtension = function (srcDirectory) { - let extensionInfo = getExtensionInfoFromPath(srcDirectory) - if (extensionInfo) { + const manifest = getManifestFromPath(srcDirectory) + if (manifest) { + const extensionInfo = manifestToExtensionInfo(manifest) for (let win of BrowserWindow.getAllWindows()) { loadDevToolsExtensions(win, [extensionInfo]) } - return extensionInfo.name + return manifest.name } } BrowserWindow.removeDevToolsExtension = function (name) { - delete extensionInfoMap[name] + delete manifestMap[name] } // Load persisted extensions when devtools is opened. @@ -126,7 +134,7 @@ app.once('ready', function () { BrowserWindow.prototype._init = function () { init.call(this) this.webContents.on('devtools-opened', () => { - loadDevToolsExtensions(this, objectValues(extensionInfoMap)) + loadDevToolsExtensions(this, objectValues(manifestMap).map(manifestToExtensionInfo)) }) } }) From edd8210ae5e38cfa023b0bbca50ce40e876a3454 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Thu, 26 May 2016 18:58:18 +0900 Subject: [PATCH 04/38] Add simple support for background page --- lib/browser/chrome-extension.js | 78 +++++++++++++++++++++++++-------- 1 file changed, 60 insertions(+), 18 deletions(-) diff --git a/lib/browser/chrome-extension.js b/lib/browser/chrome-extension.js index e882b1862d4e..4626270a8740 100644 --- a/lib/browser/chrome-extension.js +++ b/lib/browser/chrome-extension.js @@ -1,4 +1,4 @@ -const {app, protocol, BrowserWindow} = require('electron') +const {app, protocol, webContents, BrowserWindow} = require('electron') const fs = require('fs') const path = require('path') const url = require('url') @@ -28,24 +28,43 @@ let manifestMap = {} const getManifestFromPath = function (srcDirectory) { let manifest = JSON.parse(fs.readFileSync(path.join(srcDirectory, 'manifest.json'))) if (!manifestMap[manifest.name]) { + const hostname = generateHostForPath(srcDirectory) manifestMap[manifest.name] = manifest - // We can not use 'file://' directly because all resources in the extension - // will be treated as relative to the root in Chrome. - manifest.startPage = url.format({ - protocol: 'chrome-extension', - slashes: true, - hostname: generateHostForPath(srcDirectory), - pathname: manifest.devtools_page + Object.assign(manifest, { + srcDirectory: srcDirectory, + hostname: hostname, + // 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: hostname, + pathname: manifest.devtools_page + }) }) - manifest.srcDirectory = srcDirectory return manifest } } -// Load the extensions for the window. -const loadDevToolsExtensions = function (win, extensionInfoArray) { - if (!win.devToolsWebContents) return - win.devToolsWebContents.executeJavaScript(`DevToolsAPI.addExtensions(${JSON.stringify(extensionInfoArray)})`) +// Manage the background pages. +let backgroundPages = {} + +const startBackgroundPages = function (manifest) { + if (backgroundPages[manifest.hostname] || !manifest.background) return + + const scripts = manifest.background.scripts.map((name) => { + return `` + }).join('') + const html = new Buffer(`${scripts}`) + + const contents = webContents.create({}) + backgroundPages[manifest.hostname] = { html: html, contents: contents } + contents.loadURL(url.format({ + protocol: 'chrome-extension', + slashes: true, + hostname: manifest.hostname, + pathname: '_generated_background_page.html' + })) } // Transfer the |manifest| to a format that can be recognized by the @@ -59,6 +78,17 @@ const manifestToExtensionInfo = function (manifest) { } } +// Load the extensions for the window. +const loadDevToolsExtensions = function (win, manifests) { + if (!win.devToolsWebContents) return + + for (let manifest of manifests) { + startBackgroundPages(manifest) + } + const extensionInfoArray = manifests.map(manifestToExtensionInfo) + win.devToolsWebContents.executeJavaScript(`DevToolsAPI.addExtensions(${JSON.stringify(extensionInfoArray)})`) +} + // The persistent path of "DevTools Extensions" preference file. let loadedExtensionsPath = null @@ -107,9 +137,22 @@ app.once('ready', function () { let directory = getPathForHost(parsed.hostname) if (!directory) return callback() - callback(path.join(directory, parsed.path)) + if (parsed.path === '/_generated_background_page.html' && + backgroundPages[parsed.hostname]) { + return callback({ + mimeType: 'text/html', + data: backgroundPages[parsed.hostname].html + }) + } + + fs.readFile(path.join(directory, parsed.path), function (err, content) { + if (err) + callback(-6) // FILE_NOT_FOUND + else + return callback({mimeType: 'text/html', data: content}) + }) } - protocol.registerFileProtocol('chrome-extension', chromeExtensionHandler, function (error) { + protocol.registerBufferProtocol('chrome-extension', chromeExtensionHandler, function (error) { if (error) { console.error(`Unable to register chrome-extension protocol: ${error}`) } @@ -118,9 +161,8 @@ app.once('ready', function () { BrowserWindow.addDevToolsExtension = function (srcDirectory) { const manifest = getManifestFromPath(srcDirectory) if (manifest) { - const extensionInfo = manifestToExtensionInfo(manifest) for (let win of BrowserWindow.getAllWindows()) { - loadDevToolsExtensions(win, [extensionInfo]) + loadDevToolsExtensions(win, [manifest]) } return manifest.name } @@ -134,7 +176,7 @@ app.once('ready', function () { BrowserWindow.prototype._init = function () { init.call(this) this.webContents.on('devtools-opened', () => { - loadDevToolsExtensions(this, objectValues(manifestMap).map(manifestToExtensionInfo)) + loadDevToolsExtensions(this, objectValues(manifestMap)) }) } }) From cdfbe876a57f298cb1b2ce57cc41b942130ddf72 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Thu, 26 May 2016 19:29:39 +0900 Subject: [PATCH 05/38] Make sure chrome.devtools.inspectedWindow.tabId is set --- atom/browser/api/atom_api_web_contents.cc | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/atom/browser/api/atom_api_web_contents.cc b/atom/browser/api/atom_api_web_contents.cc index 72e87c30a8c7..df46eb883df8 100644 --- a/atom/browser/api/atom_api_web_contents.cc +++ b/atom/browser/api/atom_api_web_contents.cc @@ -655,6 +655,11 @@ void WebContents::DevToolsOpened() { isolate(), managed_web_contents()->GetDevToolsWebContents()); devtools_web_contents_.Reset(isolate(), handle.ToV8()); + // Set inspected tabID. + base::FundamentalValue tab_id(ID()); + managed_web_contents()->CallClientFunction( + "DevToolsAPI.setInspectedTabId", &tab_id, nullptr, nullptr); + // Inherit owner window in devtools. if (owner_window()) handle->SetOwnerWindow(managed_web_contents()->GetDevToolsWebContents(), From b646d7a55c60dc863b6409d512b9a9d1ea1a9115 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Fri, 27 May 2016 07:43:23 +0900 Subject: [PATCH 06/38] The consts --- lib/browser/chrome-extension.js | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/lib/browser/chrome-extension.js b/lib/browser/chrome-extension.js index 4626270a8740..aa72552d0f63 100644 --- a/lib/browser/chrome-extension.js +++ b/lib/browser/chrome-extension.js @@ -9,11 +9,11 @@ const objectValues = function (object) { } // Mapping between hostname and file path. -let hostPathMap = {} +const hostPathMap = {} let hostPathMapNextKey = 0 const generateHostForPath = function (path) { - let key = `extension-${++hostPathMapNextKey}` + const key = `extension-${++hostPathMapNextKey}` hostPathMap[key] = path return key } @@ -23,10 +23,10 @@ const getPathForHost = function (host) { } // Cache manifests. -let manifestMap = {} +const manifestMap = {} const getManifestFromPath = function (srcDirectory) { - let manifest = JSON.parse(fs.readFileSync(path.join(srcDirectory, 'manifest.json'))) + const manifest = JSON.parse(fs.readFileSync(path.join(srcDirectory, 'manifest.json'))) if (!manifestMap[manifest.name]) { const hostname = generateHostForPath(srcDirectory) manifestMap[manifest.name] = manifest @@ -47,7 +47,7 @@ const getManifestFromPath = function (srcDirectory) { } // Manage the background pages. -let backgroundPages = {} +const backgroundPages = {} const startBackgroundPages = function (manifest) { if (backgroundPages[manifest.hostname] || !manifest.background) return @@ -82,7 +82,7 @@ const manifestToExtensionInfo = function (manifest) { const loadDevToolsExtensions = function (win, manifests) { if (!win.devToolsWebContents) return - for (let manifest of manifests) { + for (const manifest of manifests) { startBackgroundPages(manifest) } const extensionInfoArray = manifests.map(manifestToExtensionInfo) @@ -94,7 +94,7 @@ let loadedExtensionsPath = null app.on('will-quit', function () { try { - let loadedExtensions = objectValues(manifestMap).map(function (manifest) { + const loadedExtensions = objectValues(manifestMap).map(function (manifest) { return manifest.srcDirectory }) if (loadedExtensions.length > 0) { @@ -117,10 +117,10 @@ app.once('ready', function () { // Load persisted extensions. loadedExtensionsPath = path.join(app.getPath('userData'), 'DevTools Extensions') try { - let loadedExtensions = JSON.parse(fs.readFileSync(loadedExtensionsPath)) + const loadedExtensions = JSON.parse(fs.readFileSync(loadedExtensionsPath)) if (Array.isArray(loadedExtensions)) { // Preheat the manifest cache. - for (let srcDirectory of loadedExtensions) { + for (const srcDirectory of loadedExtensions) { getManifestFromPath(srcDirectory) } } @@ -130,11 +130,11 @@ app.once('ready', function () { // The chrome-extension: can map a extension URL request to real file path. const chromeExtensionHandler = function (request, callback) { - let parsed = url.parse(request.url) + const parsed = url.parse(request.url) if (!parsed.hostname || !parsed.path) return callback() if (!/extension-\d+/.test(parsed.hostname)) return callback() - let directory = getPathForHost(parsed.hostname) + const directory = getPathForHost(parsed.hostname) if (!directory) return callback() if (parsed.path === '/_generated_background_page.html' && @@ -146,10 +146,11 @@ app.once('ready', function () { } fs.readFile(path.join(directory, parsed.path), function (err, content) { - if (err) - callback(-6) // FILE_NOT_FOUND - else + if (err) { + return callback(-6) // FILE_NOT_FOUND + } else { return callback({mimeType: 'text/html', data: content}) + } }) } protocol.registerBufferProtocol('chrome-extension', chromeExtensionHandler, function (error) { @@ -161,7 +162,7 @@ app.once('ready', function () { BrowserWindow.addDevToolsExtension = function (srcDirectory) { const manifest = getManifestFromPath(srcDirectory) if (manifest) { - for (let win of BrowserWindow.getAllWindows()) { + for (const win of BrowserWindow.getAllWindows()) { loadDevToolsExtensions(win, [manifest]) } return manifest.name @@ -172,7 +173,7 @@ app.once('ready', function () { } // Load persisted extensions when devtools is opened. - let init = BrowserWindow.prototype._init + const init = BrowserWindow.prototype._init BrowserWindow.prototype._init = function () { init.call(this) this.webContents.on('devtools-opened', () => { From 4fb9e20c336a07f5530714afbc375967339f4483 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Fri, 27 May 2016 08:51:02 +0900 Subject: [PATCH 07/38] Add API to set render process preferences --- .../atom_api_render_process_preferences.cc | 74 +++++++++++++++++++ .../api/atom_api_render_process_preferences.h | 44 +++++++++++ atom/browser/render_process_preferences.cc | 63 ++++++++++++++++ atom/browser/render_process_preferences.h | 61 +++++++++++++++ atom/common/api/api_messages.h | 3 + atom/common/node_bindings.cc | 1 + atom/renderer/atom_renderer_client.cc | 24 ++++++ atom/renderer/atom_renderer_client.h | 2 + atom/renderer/preferences_manager.cc | 34 +++++++++ atom/renderer/preferences_manager.h | 35 +++++++++ filenames.gypi | 6 ++ 11 files changed, 347 insertions(+) create mode 100644 atom/browser/api/atom_api_render_process_preferences.cc create mode 100644 atom/browser/api/atom_api_render_process_preferences.h create mode 100644 atom/browser/render_process_preferences.cc create mode 100644 atom/browser/render_process_preferences.h create mode 100644 atom/renderer/preferences_manager.cc create mode 100644 atom/renderer/preferences_manager.h diff --git a/atom/browser/api/atom_api_render_process_preferences.cc b/atom/browser/api/atom_api_render_process_preferences.cc new file mode 100644 index 000000000000..3bdbfaef18e4 --- /dev/null +++ b/atom/browser/api/atom_api_render_process_preferences.cc @@ -0,0 +1,74 @@ +// Copyright (c) 2016 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "atom/browser/api/atom_api_render_process_preferences.h" + +#include "atom/common/native_mate_converters/value_converter.h" +#include "atom/common/node_includes.h" +#include "native_mate/dictionary.h" +#include "native_mate/object_template_builder.h" + +namespace atom { + +namespace api { + +namespace { + +bool IsBrowserWindow(content::RenderProcessHost* process) { + return true; +} + +} // namespace + +RenderProcessPreferences::RenderProcessPreferences( + v8::Isolate* isolate, + const atom::RenderProcessPreferences::Predicate& predicate) + : preferences_(predicate) { + Init(isolate); +} + +RenderProcessPreferences::~RenderProcessPreferences() { +} + +int RenderProcessPreferences::AddEntry(const base::DictionaryValue& entry) { + return preferences_.AddEntry(entry); +} + +void RenderProcessPreferences::RemoveEntry(int id) { + preferences_.RemoveEntry(id); +} + +// static +void RenderProcessPreferences::BuildPrototype( + v8::Isolate* isolate, v8::Local prototype) { + mate::ObjectTemplateBuilder(isolate, prototype) + .SetMethod("addEntry", &RenderProcessPreferences::AddEntry) + .SetMethod("removeEntry", &RenderProcessPreferences::RemoveEntry); +} + +// static +mate::Handle +RenderProcessPreferences::ForAllBrowserWindow(v8::Isolate* isolate) { + return mate::CreateHandle( + isolate, + new RenderProcessPreferences(isolate, base::Bind(&IsBrowserWindow))); +} + +} // namespace api + +} // namespace atom + +namespace { + +void Initialize(v8::Local exports, v8::Local unused, + v8::Local context, void* priv) { + mate::Dictionary dict(context->GetIsolate(), exports); + dict.SetMethod("forAllBrowserWindow", + &atom::api::RenderProcessPreferences::ForAllBrowserWindow); +} + +} // namespace + +NODE_MODULE_CONTEXT_AWARE_BUILTIN(atom_browser_render_process_preferences, + Initialize) diff --git a/atom/browser/api/atom_api_render_process_preferences.h b/atom/browser/api/atom_api_render_process_preferences.h new file mode 100644 index 000000000000..a305f1361b2d --- /dev/null +++ b/atom/browser/api/atom_api_render_process_preferences.h @@ -0,0 +1,44 @@ +// Copyright (c) 2016 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ATOM_BROWSER_API_ATOM_API_RENDER_PROCESS_PREFERENCES_H_ +#define ATOM_BROWSER_API_ATOM_API_RENDER_PROCESS_PREFERENCES_H_ + +#include "atom/browser/render_process_preferences.h" +#include "native_mate/handle.h" +#include "native_mate/wrappable.h" + +namespace atom { + +namespace api { + +class RenderProcessPreferences + : public mate::Wrappable { + public: + static mate::Handle + ForAllBrowserWindow(v8::Isolate* isolate); + + static void BuildPrototype(v8::Isolate* isolate, + v8::Local prototype); + + int AddEntry(const base::DictionaryValue& entry); + void RemoveEntry(int id); + + protected: + RenderProcessPreferences( + v8::Isolate* isolate, + const atom::RenderProcessPreferences::Predicate& predicate); + ~RenderProcessPreferences() override; + + private: + atom::RenderProcessPreferences preferences_; + + DISALLOW_COPY_AND_ASSIGN(RenderProcessPreferences); +}; + +} // namespace api + +} // namespace atom + +#endif // ATOM_BROWSER_API_ATOM_API_RENDER_PROCESS_PREFERENCES_H_ diff --git a/atom/browser/render_process_preferences.cc b/atom/browser/render_process_preferences.cc new file mode 100644 index 000000000000..d109c8714f70 --- /dev/null +++ b/atom/browser/render_process_preferences.cc @@ -0,0 +1,63 @@ +// Copyright (c) 2016 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "atom/browser/render_process_preferences.h" + +#include "atom/common/api/api_messages.h" +#include "content/public/browser/notification_service.h" +#include "content/public/browser/notification_types.h" +#include "content/public/browser/render_process_host.h" + +namespace atom { + +RenderProcessPreferences::RenderProcessPreferences(const Predicate& predicate) + : predicate_(predicate), + next_id_(0), + cache_needs_update_(true) { + registrar_.Add(this, + content::NOTIFICATION_RENDERER_PROCESS_CREATED, + content::NotificationService::AllBrowserContextsAndSources()); +} + +RenderProcessPreferences::~RenderProcessPreferences() { +} + +int RenderProcessPreferences::AddEntry(const base::DictionaryValue& entry) { + int id = ++next_id_; + entries_[id] = entry.CreateDeepCopy(); + cache_needs_update_ = true; + return id; +} + +void RenderProcessPreferences::RemoveEntry(int id) { + cache_needs_update_ = true; + entries_.erase(id); +} + +void RenderProcessPreferences::Observe( + int type, + const content::NotificationSource& source, + const content::NotificationDetails& details) { + DCHECK_EQ(type, content::NOTIFICATION_RENDERER_PROCESS_CREATED); + content::RenderProcessHost* process = + content::Source(source).ptr(); + + if (!predicate_.Run(process)) + return; + + UpdateCache(); + process->Send(new AtomMsg_UpdatePreferences(cached_entries_)); +} + +void RenderProcessPreferences::UpdateCache() { + if (!cache_needs_update_) + return; + + cached_entries_.Clear(); + for (const auto& iter : entries_) + cached_entries_.Append(iter.second->CreateDeepCopy()); + cache_needs_update_ = false; +} + +} // namespace atom diff --git a/atom/browser/render_process_preferences.h b/atom/browser/render_process_preferences.h new file mode 100644 index 000000000000..77bf176f492c --- /dev/null +++ b/atom/browser/render_process_preferences.h @@ -0,0 +1,61 @@ +// Copyright (c) 2016 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ATOM_BROWSER_RENDER_PROCESS_PREFERENCES_H_ +#define ATOM_BROWSER_RENDER_PROCESS_PREFERENCES_H_ + +#include +#include + +#include "base/callback.h" +#include "base/values.h" +#include "content/public/browser/notification_observer.h" +#include "content/public/browser/notification_registrar.h" + +namespace content { +class RenderProcessHost; +} + +namespace atom { + +// Sets user preferences for render processes. +class RenderProcessPreferences : public content::NotificationObserver { + public: + using Predicate = base::Callback; + + // The |predicate| is used to determine whether to set preferences for a + // render process. + explicit RenderProcessPreferences(const Predicate& predicate); + virtual ~RenderProcessPreferences(); + + int AddEntry(const base::DictionaryValue& entry); + void RemoveEntry(int id); + + private: + // content::NotificationObserver: + void Observe(int type, + const content::NotificationSource& source, + const content::NotificationDetails& details) override; + + void UpdateCache(); + + // Manages our notification registrations. + content::NotificationRegistrar registrar_; + + Predicate predicate_; + + int next_id_; + std::unordered_map> entries_; + + // We need to convert the |entries_| to ListValue for multiple times, this + // caches is only updated when we are sending messages. + bool cache_needs_update_; + base::ListValue cached_entries_; + + DISALLOW_COPY_AND_ASSIGN(RenderProcessPreferences); +}; + +} // namespace atom + +#endif // ATOM_BROWSER_RENDER_PROCESS_PREFERENCES_H_ diff --git a/atom/common/api/api_messages.h b/atom/common/api/api_messages.h index eeb26614847b..c7ba38de1d33 100644 --- a/atom/common/api/api_messages.h +++ b/atom/common/api/api_messages.h @@ -37,3 +37,6 @@ IPC_MESSAGE_ROUTED2(AtomViewMsg_Message, // Sent by the renderer when the draggable regions are updated. IPC_MESSAGE_ROUTED1(AtomViewHostMsg_UpdateDraggableRegions, std::vector /* regions */) + +// Update renderer process preferences. +IPC_MESSAGE_CONTROL1(AtomMsg_UpdatePreferences, base::ListValue) diff --git a/atom/common/node_bindings.cc b/atom/common/node_bindings.cc index a55620557e56..ed2ea01675a1 100644 --- a/atom/common/node_bindings.cc +++ b/atom/common/node_bindings.cc @@ -42,6 +42,7 @@ REFERENCE_MODULE(atom_browser_power_monitor); REFERENCE_MODULE(atom_browser_power_save_blocker); REFERENCE_MODULE(atom_browser_protocol); REFERENCE_MODULE(atom_browser_global_shortcut); +REFERENCE_MODULE(atom_browser_render_process_preferences); REFERENCE_MODULE(atom_browser_session); REFERENCE_MODULE(atom_browser_system_preferences); REFERENCE_MODULE(atom_browser_tray); diff --git a/atom/renderer/atom_renderer_client.cc b/atom/renderer/atom_renderer_client.cc index 5613f20d2ecc..114fb90555b4 100644 --- a/atom/renderer/atom_renderer_client.cc +++ b/atom/renderer/atom_renderer_client.cc @@ -11,12 +11,14 @@ #include "atom/common/api/atom_bindings.h" #include "atom/common/api/event_emitter_caller.h" #include "atom/common/color_util.h" +#include "atom/common/native_mate_converters/value_converter.h" #include "atom/common/node_bindings.h" #include "atom/common/node_includes.h" #include "atom/common/options_switches.h" #include "atom/renderer/atom_render_view_observer.h" #include "atom/renderer/guest_view_container.h" #include "atom/renderer/node_array_buffer_bridge.h" +#include "atom/renderer/preferences_manager.h" #include "base/command_line.h" #include "base/strings/utf_string_conversions.h" #include "chrome/renderer/media/chrome_key_systems.h" @@ -29,6 +31,7 @@ #include "content/public/renderer/render_thread.h" #include "content/public/renderer/render_view.h" #include "ipc/ipc_message_macros.h" +#include "native_mate/dictionary.h" #include "net/base/net_errors.h" #include "third_party/WebKit/public/web/WebCustomElement.h" #include "third_party/WebKit/public/web/WebFrameWidget.h" @@ -85,6 +88,23 @@ class AtomRenderFrameObserver : public content::RenderFrameObserver { DISALLOW_COPY_AND_ASSIGN(AtomRenderFrameObserver); }; +v8::Local GetRenderProcessPreferences( + const PreferencesManager* preferences_manager, v8::Isolate* isolate) { + if (preferences_manager->preferences()) + return mate::ConvertToV8(isolate, *preferences_manager->preferences()); + else + return v8::Null(isolate); +} + +void AddRenderBindings(v8::Isolate* isolate, + v8::Local process, + const PreferencesManager* preferences_manager) { + mate::Dictionary dict(isolate, process); + dict.SetMethod( + "getRenderProcessPreferences", + base::Bind(GetRenderProcessPreferences, preferences_manager)); +} + } // namespace AtomRendererClient::AtomRendererClient() @@ -101,6 +121,8 @@ void AtomRendererClient::RenderThreadStarted() { OverrideNodeArrayBuffer(); + preferences_manager_.reset(new PreferencesManager); + #if defined(OS_WIN) // Set ApplicationUserModelID in renderer process. base::CommandLine* command_line = base::CommandLine::ForCurrentProcess(); @@ -201,6 +223,8 @@ void AtomRendererClient::DidCreateScriptContext( // Add atom-shell extended APIs. atom_bindings_->BindTo(env->isolate(), env->process_object()); + AddRenderBindings(env->isolate(), env->process_object(), + preferences_manager_.get()); // Load everything. node_bindings_->LoadEnvironment(env); diff --git a/atom/renderer/atom_renderer_client.h b/atom/renderer/atom_renderer_client.h index 16b975fd41f7..78c2e4959943 100644 --- a/atom/renderer/atom_renderer_client.h +++ b/atom/renderer/atom_renderer_client.h @@ -13,6 +13,7 @@ namespace atom { class AtomBindings; +class PreferencesManager; class NodeBindings; class AtomRendererClient : public content::ContentRendererClient { @@ -61,6 +62,7 @@ class AtomRendererClient : public content::ContentRendererClient { std::unique_ptr node_bindings_; std::unique_ptr atom_bindings_; + std::unique_ptr preferences_manager_; DISALLOW_COPY_AND_ASSIGN(AtomRendererClient); }; diff --git a/atom/renderer/preferences_manager.cc b/atom/renderer/preferences_manager.cc new file mode 100644 index 000000000000..a9ed710a9dbd --- /dev/null +++ b/atom/renderer/preferences_manager.cc @@ -0,0 +1,34 @@ +// Copyright (c) 2016 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "atom/renderer/preferences_manager.h" + +#include "atom/common/api/api_messages.h" +#include "content/public/renderer/render_thread.h" + +namespace atom { + +PreferencesManager::PreferencesManager() { + content::RenderThread::Get()->AddObserver(this); +} + +PreferencesManager::~PreferencesManager() { +} + +bool PreferencesManager::OnControlMessageReceived( + const IPC::Message& message) { + bool handled = true; + IPC_BEGIN_MESSAGE_MAP(PreferencesManager, message) + IPC_MESSAGE_HANDLER(AtomMsg_UpdatePreferences, OnUpdatePreferences) + IPC_MESSAGE_UNHANDLED(handled = false) + IPC_END_MESSAGE_MAP() + return handled; +} + +void PreferencesManager::OnUpdatePreferences( + const base::ListValue& preferences) { + preferences_ = preferences.CreateDeepCopy(); +} + +} // namespace atom diff --git a/atom/renderer/preferences_manager.h b/atom/renderer/preferences_manager.h new file mode 100644 index 000000000000..451928085d12 --- /dev/null +++ b/atom/renderer/preferences_manager.h @@ -0,0 +1,35 @@ +// Copyright (c) 2016 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ATOM_RENDERER_PREFERENCES_MANAGER_H_ +#define ATOM_RENDERER_PREFERENCES_MANAGER_H_ + +#include + +#include "base/values.h" +#include "content/public/renderer/render_process_observer.h" + +namespace atom { + +class PreferencesManager : public content::RenderProcessObserver { + public: + PreferencesManager(); + ~PreferencesManager() override; + + const base::ListValue* preferences() const { return preferences_.get(); } + + private: + // content::RenderThreadObserver: + bool OnControlMessageReceived(const IPC::Message& message) override; + + void OnUpdatePreferences(const base::ListValue& preferences); + + std::unique_ptr preferences_; + + DISALLOW_COPY_AND_ASSIGN(PreferencesManager); +}; + +} // namespace atom + +#endif // ATOM_RENDERER_PREFERENCES_MANAGER_H_ diff --git a/filenames.gypi b/filenames.gypi index 14d01270f089..d8e665715109 100644 --- a/filenames.gypi +++ b/filenames.gypi @@ -109,6 +109,8 @@ 'atom/browser/api/atom_api_power_monitor.h', 'atom/browser/api/atom_api_power_save_blocker.cc', 'atom/browser/api/atom_api_power_save_blocker.h', + 'atom/browser/api/atom_api_render_process_preferences.cc', + 'atom/browser/api/atom_api_render_process_preferences.h', 'atom/browser/api/atom_api_protocol.cc', 'atom/browser/api/atom_api_protocol.h', 'atom/browser/api/atom_api_screen.cc', @@ -220,6 +222,8 @@ 'atom/browser/net/url_request_fetch_job.h', 'atom/browser/node_debugger.cc', 'atom/browser/node_debugger.h', + 'atom/browser/render_process_preferences.cc', + 'atom/browser/render_process_preferences.h', 'atom/browser/ui/accelerator_util.cc', 'atom/browser/ui/accelerator_util.h', 'atom/browser/ui/accelerator_util_mac.mm', @@ -396,6 +400,8 @@ 'atom/renderer/guest_view_container.h', 'atom/renderer/node_array_buffer_bridge.cc', 'atom/renderer/node_array_buffer_bridge.h', + 'atom/renderer/preferences_manager.cc', + 'atom/renderer/preferences_manager.h', 'atom/utility/atom_content_utility_client.cc', 'atom/utility/atom_content_utility_client.h', 'chromium_src/chrome/browser/browser_process.cc', From 7eab259d92c3c10f440388b725018a4bc367b028 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Fri, 27 May 2016 09:20:46 +0900 Subject: [PATCH 08/38] Make it possible to only set preferences for BrowserWindow --- .../atom_api_render_process_preferences.cc | 14 +++++++++++++ atom/browser/atom_browser_client.cc | 20 +++++++++++-------- atom/browser/atom_browser_client.h | 3 +++ atom/browser/web_contents_preferences.h | 1 + 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/atom/browser/api/atom_api_render_process_preferences.cc b/atom/browser/api/atom_api_render_process_preferences.cc index 3bdbfaef18e4..59ae07b45c75 100644 --- a/atom/browser/api/atom_api_render_process_preferences.cc +++ b/atom/browser/api/atom_api_render_process_preferences.cc @@ -4,8 +4,12 @@ #include "atom/browser/api/atom_api_render_process_preferences.h" +#include "atom/browser/atom_browser_client.h" +#include "atom/browser/native_window.h" +#include "atom/browser/window_list.h" #include "atom/common/native_mate_converters/value_converter.h" #include "atom/common/node_includes.h" +#include "content/public/browser/render_process_host.h" #include "native_mate/dictionary.h" #include "native_mate/object_template_builder.h" @@ -16,6 +20,16 @@ namespace api { namespace { bool IsBrowserWindow(content::RenderProcessHost* process) { + content::WebContents* web_contents = + static_cast(AtomBrowserClient::Get())-> + GetWebContentsFromProcessID(process->GetID()); + if (!web_contents) + return false; + + NativeWindow* window = NativeWindow::FromWebContents(web_contents); + if (!window) + return false; + return true; } diff --git a/atom/browser/atom_browser_client.cc b/atom/browser/atom_browser_client.cc index 5a6b49483bcb..e3cb9c5c8c89 100644 --- a/atom/browser/atom_browser_client.cc +++ b/atom/browser/atom_browser_client.cc @@ -73,6 +73,17 @@ AtomBrowserClient::AtomBrowserClient() : delegate_(nullptr) { AtomBrowserClient::~AtomBrowserClient() { } +content::WebContents* AtomBrowserClient::GetWebContentsFromProcessID( + int process_id) { + // If the process is a pending process, we should use the old one. + if (ContainsKey(pending_processes_, process_id)) + process_id = pending_processes_[process_id]; + + // Certain render process will be created with no associated render view, + // for example: ServiceWorker. + return WebContentsPreferences::GetWebContentsFromProcessID(process_id); +} + void AtomBrowserClient::RenderProcessWillLaunch( content::RenderProcessHost* host) { int process_id = host->GetID(); @@ -172,14 +183,7 @@ void AtomBrowserClient::AppendExtraCommandLineSwitches( } #endif - // If the process is a pending process, we should use the old one. - if (ContainsKey(pending_processes_, process_id)) - process_id = pending_processes_[process_id]; - - // Certain render process will be created with no associated render view, - // for example: ServiceWorker. - content::WebContents* web_contents = - WebContentsPreferences::GetWebContentsFromProcessID(process_id); + content::WebContents* web_contents = GetWebContentsFromProcessID(process_id); if (!web_contents) return; diff --git a/atom/browser/atom_browser_client.h b/atom/browser/atom_browser_client.h index 5588f0435afa..cf1a4cc438b6 100644 --- a/atom/browser/atom_browser_client.h +++ b/atom/browser/atom_browser_client.h @@ -34,6 +34,9 @@ class AtomBrowserClient : public brightray::BrowserClient, using Delegate = content::ContentBrowserClient; void set_delegate(Delegate* delegate) { delegate_ = delegate; } + // Returns the WebContents for pending render processes. + content::WebContents* GetWebContentsFromProcessID(int process_id); + // Don't force renderer process to restart for once. static void SuppressRendererProcessRestartForOnce(); diff --git a/atom/browser/web_contents_preferences.h b/atom/browser/web_contents_preferences.h index dd98a9658acf..daf1f6e84de5 100644 --- a/atom/browser/web_contents_preferences.h +++ b/atom/browser/web_contents_preferences.h @@ -29,6 +29,7 @@ class WebContentsPreferences : public content::WebContentsUserData { public: // Get WebContents according to process ID. + // FIXME(zcbenz): This method does not belong here. static content::WebContents* GetWebContentsFromProcessID(int process_id); // Append command paramters according to |web_contents|'s preferences. From a63ff714f8c925fd15aee0879379fecadda50c0c Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Fri, 27 May 2016 09:47:37 +0900 Subject: [PATCH 09/38] Read content_scripts to preferences --- lib/browser/chrome-extension.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/lib/browser/chrome-extension.js b/lib/browser/chrome-extension.js index aa72552d0f63..1ccc130ebb85 100644 --- a/lib/browser/chrome-extension.js +++ b/lib/browser/chrome-extension.js @@ -1,4 +1,6 @@ const {app, protocol, webContents, BrowserWindow} = require('electron') +const renderProcessPreferences = process.atomBinding('render_process_preferences').forAllBrowserWindow() + const fs = require('fs') const path = require('path') const url = require('url') @@ -67,6 +69,34 @@ const startBackgroundPages = function (manifest) { })) } +// 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 fs.readFileSync(path.join(manifest.srcDirectory, relativePath)) + } + + const contentScriptToEntry = function (script) { + return { + matches: script.matches, + js: script.js.map(readArrayOfFiles), + run_at: script.run_at || 'document_idle' + } + } + + try { + const entry = { + contentScripts: manifest.content_scripts.map(contentScriptToEntry) + } + contentScripts[manifest.name] = renderProcessPreferences.addEntry(entry) + } catch (e) { + console.error('Failed to read content scripts', e) + } +} + // Transfer the |manifest| to a format that can be recognized by the // |DevToolsAPI.addExtensions|. const manifestToExtensionInfo = function (manifest) { @@ -84,6 +114,7 @@ const loadDevToolsExtensions = function (win, manifests) { for (const manifest of manifests) { startBackgroundPages(manifest) + injectContentScripts(manifest) } const extensionInfoArray = manifests.map(manifestToExtensionInfo) win.devToolsWebContents.executeJavaScript(`DevToolsAPI.addExtensions(${JSON.stringify(extensionInfoArray)})`) From 97c04735a27bf58b5bbe962eddf8babb0842cf25 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Fri, 27 May 2016 09:55:59 +0900 Subject: [PATCH 10/38] Handle unloading devtools correctly --- lib/browser/chrome-extension.js | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/browser/chrome-extension.js b/lib/browser/chrome-extension.js index 1ccc130ebb85..f02a1113cc90 100644 --- a/lib/browser/chrome-extension.js +++ b/lib/browser/chrome-extension.js @@ -60,7 +60,7 @@ const startBackgroundPages = function (manifest) { const html = new Buffer(`${scripts}`) const contents = webContents.create({}) - backgroundPages[manifest.hostname] = { html: html, contents: contents } + backgroundPages[manifest.hostname] = { html: html, webContents: contents } contents.loadURL(url.format({ protocol: 'chrome-extension', slashes: true, @@ -69,6 +69,13 @@ const startBackgroundPages = function (manifest) { })) } +const removeBackgroundPages = function (manifest) { + if (!backgroundPages[manifest.hostname]) return + + backgroundPages[manifest.hostname].webContents.destroy() + delete backgroundPages[manifest.hostname] +} + // Transfer the content scripts to renderer. const contentScripts = {} @@ -97,6 +104,13 @@ const injectContentScripts = function (manifest) { } } +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) { @@ -200,6 +214,11 @@ app.once('ready', function () { } } BrowserWindow.removeDevToolsExtension = function (name) { + const manifest = manifestMap[name] + if (!manifest) return + + removeBackgroundPages(manifest) + removeContentScripts(manifest) delete manifestMap[name] } From 49d9446cce91baaa532443d0282d81e7b3846f77 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Fri, 27 May 2016 10:29:57 +0900 Subject: [PATCH 11/38] Implement a simple content script injector --- filenames.gypi | 1 + lib/browser/chrome-extension.js | 2 +- lib/renderer/content-scripts-injector.js | 35 ++++++++++++++++++++++++ lib/renderer/init.js | 3 ++ 4 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 lib/renderer/content-scripts-injector.js diff --git a/filenames.gypi b/filenames.gypi index d8e665715109..ae71164a8ab9 100644 --- a/filenames.gypi +++ b/filenames.gypi @@ -49,6 +49,7 @@ 'lib/common/init.js', 'lib/common/reset-search-paths.js', 'lib/renderer/chrome-api.js', + 'lib/renderer/content-scripts-injector.js', 'lib/renderer/init.js', 'lib/renderer/inspector.js', 'lib/renderer/override.js', diff --git a/lib/browser/chrome-extension.js b/lib/browser/chrome-extension.js index f02a1113cc90..fbf0fef5b1c1 100644 --- a/lib/browser/chrome-extension.js +++ b/lib/browser/chrome-extension.js @@ -90,7 +90,7 @@ const injectContentScripts = function (manifest) { return { matches: script.matches, js: script.js.map(readArrayOfFiles), - run_at: script.run_at || 'document_idle' + runAt: script.run_at || 'document_idle' } } diff --git a/lib/renderer/content-scripts-injector.js b/lib/renderer/content-scripts-injector.js new file mode 100644 index 000000000000..0594ada07bfa --- /dev/null +++ b/lib/renderer/content-scripts-injector.js @@ -0,0 +1,35 @@ +const preferences = process.getRenderProcessPreferences() +if (!preferences || preferences.length == 0) return + +const {webFrame} = require('electron') + +// Check whether pattern matches. +// https://developer.chrome.com/extensions/match_patterns +const matchesPattern = function (pattern) { + if (pattern === '') + return true + + const regexp = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$') + return location.href.match(regexp) +} + +// Run injected scripts. +// https://developer.chrome.com/extensions/content_scripts +const injectContentScript = function (script) { + for (const match of script.matches) { + if (!matchesPattern(match)) return + } + + for (const js of script.js) { + if (script.runAt === 'document_start') { + webFrame.executeJavaScript(String(js)) + } + } +} + +// Read the renderer process preferences. +for (const pref of preferences) { + if (pref.contentScripts) { + pref.contentScripts.forEach(injectContentScript) + } +} diff --git a/lib/renderer/init.js b/lib/renderer/init.js index ed0e0e800e62..6138b6ba575b 100644 --- a/lib/renderer/init.js +++ b/lib/renderer/init.js @@ -69,6 +69,9 @@ if (window.location.protocol === 'chrome-devtools:') { // 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') From 7e1f1591856a5204267b9149976ec40ceb4d3c2e Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Fri, 27 May 2016 11:07:06 +0900 Subject: [PATCH 12/38] Run content scripts at correct phase --- atom/renderer/atom_renderer_client.cc | 17 +++++++++++++++++ atom/renderer/atom_renderer_client.h | 1 + lib/browser/chrome-extension.js | 2 +- lib/renderer/content-scripts-injector.js | 22 +++++++++++++--------- 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/atom/renderer/atom_renderer_client.cc b/atom/renderer/atom_renderer_client.cc index 114fb90555b4..4ddbe956b89d 100644 --- a/atom/renderer/atom_renderer_client.cc +++ b/atom/renderer/atom_renderer_client.cc @@ -186,6 +186,23 @@ void AtomRendererClient::RunScriptsAtDocumentStart( // Make sure every page will get a script context created. render_frame->GetWebFrame()->executeScript( blink::WebScriptSource("void 0")); + + // Inform the docuemnt start pharse. + node::Environment* env = node_bindings_->uv_env(); + if (env) { + v8::HandleScope handle_scope(env->isolate()); + mate::EmitEvent(env->isolate(), env->process_object(), "document-start"); + } +} + +void AtomRendererClient::RunScriptsAtDocumentEnd( + content::RenderFrame* render_frame) { + // Inform the docuemnt end pharse. + node::Environment* env = node_bindings_->uv_env(); + if (env) { + v8::HandleScope handle_scope(env->isolate()); + mate::EmitEvent(env->isolate(), env->process_object(), "document-end"); + } } blink::WebSpeechSynthesizer* AtomRendererClient::OverrideSpeechSynthesizer( diff --git a/atom/renderer/atom_renderer_client.h b/atom/renderer/atom_renderer_client.h index 78c2e4959943..1a178e01f4fa 100644 --- a/atom/renderer/atom_renderer_client.h +++ b/atom/renderer/atom_renderer_client.h @@ -37,6 +37,7 @@ class AtomRendererClient : public content::ContentRendererClient { void RenderFrameCreated(content::RenderFrame*) override; void RenderViewCreated(content::RenderView*) override; void RunScriptsAtDocumentStart(content::RenderFrame* render_frame) override; + void RunScriptsAtDocumentEnd(content::RenderFrame* render_frame) override; blink::WebSpeechSynthesizer* OverrideSpeechSynthesizer( blink::WebSpeechSynthesizerClient* client) override; bool OverrideCreatePlugin(content::RenderFrame* render_frame, diff --git a/lib/browser/chrome-extension.js b/lib/browser/chrome-extension.js index fbf0fef5b1c1..9a86e2fc34dc 100644 --- a/lib/browser/chrome-extension.js +++ b/lib/browser/chrome-extension.js @@ -83,7 +83,7 @@ const injectContentScripts = function (manifest) { if (contentScripts[manifest.name] || !manifest.content_scripts) return const readArrayOfFiles = function (relativePath) { - return fs.readFileSync(path.join(manifest.srcDirectory, relativePath)) + return String(fs.readFileSync(path.join(manifest.srcDirectory, relativePath))) } const contentScriptToEntry = function (script) { diff --git a/lib/renderer/content-scripts-injector.js b/lib/renderer/content-scripts-injector.js index 0594ada07bfa..07f330b7cc15 100644 --- a/lib/renderer/content-scripts-injector.js +++ b/lib/renderer/content-scripts-injector.js @@ -1,13 +1,9 @@ -const preferences = process.getRenderProcessPreferences() -if (!preferences || preferences.length == 0) return - const {webFrame} = require('electron') // Check whether pattern matches. // https://developer.chrome.com/extensions/match_patterns const matchesPattern = function (pattern) { - if (pattern === '') - return true + if (pattern === '') return true const regexp = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$') return location.href.match(regexp) @@ -21,15 +17,23 @@ const injectContentScript = function (script) { } for (const js of script.js) { + const fire = () => webFrame.executeJavaScript(js) if (script.runAt === 'document_start') { - webFrame.executeJavaScript(String(js)) + 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) } } } // Read the renderer process preferences. -for (const pref of preferences) { - if (pref.contentScripts) { - pref.contentScripts.forEach(injectContentScript) +const preferences = process.getRenderProcessPreferences() +if (preferences) { + for (const pref of preferences) { + if (pref.contentScripts) { + pref.contentScripts.forEach(injectContentScript) + } } } From 134f9019ebd8c3fcd9a5b2459bc16821346a4b0a Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Fri, 27 May 2016 15:10:46 +0900 Subject: [PATCH 13/38] Enable node integration for pages in devtools extension --- atom/renderer/atom_renderer_client.cc | 39 +++++++++++++++++---------- atom/renderer/atom_renderer_client.h | 6 +++-- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/atom/renderer/atom_renderer_client.cc b/atom/renderer/atom_renderer_client.cc index 4ddbe956b89d..673ec4aa5d75 100644 --- a/atom/renderer/atom_renderer_client.cc +++ b/atom/renderer/atom_renderer_client.cc @@ -34,6 +34,7 @@ #include "native_mate/dictionary.h" #include "net/base/net_errors.h" #include "third_party/WebKit/public/web/WebCustomElement.h" +#include "third_party/WebKit/public/web/WebDocument.h" #include "third_party/WebKit/public/web/WebFrameWidget.h" #include "third_party/WebKit/public/web/WebLocalFrame.h" #include "third_party/WebKit/public/web/WebPluginParams.h" @@ -62,6 +63,7 @@ class AtomRenderFrameObserver : public content::RenderFrameObserver { AtomRenderFrameObserver(content::RenderFrame* frame, AtomRendererClient* renderer_client) : content::RenderFrameObserver(frame), + render_frame_(frame), world_id_(-1), renderer_client_(renderer_client) {} @@ -72,16 +74,17 @@ class AtomRenderFrameObserver : public content::RenderFrameObserver { if (world_id_ != -1 && world_id_ != world_id) return; world_id_ = world_id; - renderer_client_->DidCreateScriptContext(context); + renderer_client_->DidCreateScriptContext(context, render_frame_); } void WillReleaseScriptContext(v8::Local context, int world_id) override { if (world_id_ != world_id) return; - renderer_client_->WillReleaseScriptContext(context); + renderer_client_->WillReleaseScriptContext(context, render_frame_); } private: + content::RenderFrame* render_frame_; int world_id_; AtomRendererClient* renderer_client_; @@ -105,6 +108,11 @@ void AddRenderBindings(v8::Isolate* isolate, base::Bind(GetRenderProcessPreferences, preferences_manager)); } +bool IsDevToolsExtension(content::RenderFrame* render_frame) { + return static_cast(render_frame->GetWebFrame()->document().url()) + .SchemeIs("chrome-extension"); +} + } // namespace AtomRendererClient::AtomRendererClient() @@ -150,15 +158,11 @@ void AtomRendererClient::RenderThreadStarted() { void AtomRendererClient::RenderFrameCreated( content::RenderFrame* render_frame) { new PepperHelper(render_frame); + new AtomRenderFrameObserver(render_frame, this); // Allow file scheme to handle service worker by default. + // FIXME(zcbenz): Can this be moved elsewhere? blink::WebSecurityPolicy::registerURLSchemeAsAllowingServiceWorkers("file"); - - // Only insert node integration for the main frame. - if (!render_frame->IsMainFrame()) - return; - - new AtomRenderFrameObserver(render_frame, this); } void AtomRendererClient::RenderViewCreated(content::RenderView* render_view) { @@ -187,7 +191,7 @@ void AtomRendererClient::RunScriptsAtDocumentStart( render_frame->GetWebFrame()->executeScript( blink::WebScriptSource("void 0")); - // Inform the docuemnt start pharse. + // Inform the document start pharse. node::Environment* env = node_bindings_->uv_env(); if (env) { v8::HandleScope handle_scope(env->isolate()); @@ -197,7 +201,7 @@ void AtomRendererClient::RunScriptsAtDocumentStart( void AtomRendererClient::RunScriptsAtDocumentEnd( content::RenderFrame* render_frame) { - // Inform the docuemnt end pharse. + // Inform the document end pharse. node::Environment* env = node_bindings_->uv_env(); if (env) { v8::HandleScope handle_scope(env->isolate()); @@ -225,7 +229,12 @@ bool AtomRendererClient::OverrideCreatePlugin( } void AtomRendererClient::DidCreateScriptContext( - v8::Handle context) { + v8::Handle context, content::RenderFrame* render_frame) { + // Only allow node integration for the main frame, unless it is a devtools + // extension page. + if (!render_frame->IsMainFrame() && !IsDevToolsExtension(render_frame)) + return; + // Whether the node binding has been initialized. bool first_time = node_bindings_->uv_env() == nullptr; @@ -256,9 +265,11 @@ void AtomRendererClient::DidCreateScriptContext( } void AtomRendererClient::WillReleaseScriptContext( - v8::Handle context) { - node::Environment* env = node::Environment::GetCurrent(context); - mate::EmitEvent(env->isolate(), env->process_object(), "exit"); + v8::Handle context, content::RenderFrame* render_frame) { + if (render_frame->IsMainFrame()) { + node::Environment* env = node::Environment::GetCurrent(context); + mate::EmitEvent(env->isolate(), env->process_object(), "exit"); + } } bool AtomRendererClient::ShouldFork(blink::WebLocalFrame* frame, diff --git a/atom/renderer/atom_renderer_client.h b/atom/renderer/atom_renderer_client.h index 1a178e01f4fa..51872e5bcba9 100644 --- a/atom/renderer/atom_renderer_client.h +++ b/atom/renderer/atom_renderer_client.h @@ -21,8 +21,10 @@ class AtomRendererClient : public content::ContentRendererClient { AtomRendererClient(); virtual ~AtomRendererClient(); - void DidCreateScriptContext(v8::Handle context); - void WillReleaseScriptContext(v8::Handle context); + void DidCreateScriptContext( + v8::Handle context, content::RenderFrame* render_frame); + void WillReleaseScriptContext( + v8::Handle context, content::RenderFrame* render_frame); private: enum NodeIntegration { From c1facec5a601aecdb7ff3af78ffab2f72c349521 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Fri, 27 May 2016 15:12:49 +0900 Subject: [PATCH 14/38] No need to hijack setInjectedScriptForOrigin --- lib/renderer/inspector.js | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/lib/renderer/inspector.js b/lib/renderer/inspector.js index aea87f7f2b81..ca60c84d4be9 100644 --- a/lib/renderer/inspector.js +++ b/lib/renderer/inspector.js @@ -1,7 +1,4 @@ window.onload = function () { - // Make sure |window.chrome| is defined for devtools extensions. - hijackSetInjectedScript(window.InspectorFrontendHost) - // Use menu API to show context menu. window.InspectorFrontendHost.showContextMenuAtPoint = createMenu @@ -9,18 +6,6 @@ window.onload = function () { window.WebInspector.createFileSelectorElement = createFileSelectorElement } -const hijackSetInjectedScript = function (InspectorFrontendHost) { - const {setInjectedScriptForOrigin} = InspectorFrontendHost - InspectorFrontendHost.setInjectedScriptForOrigin = function (origin, source) { - const wrapped = `(function (...args) { - window.chrome = {} - const original = ${source} - original(...args) - })` - setInjectedScriptForOrigin(origin, wrapped) - } -} - var convertToMenuTemplate = function (items) { var fn, i, item, len, template template = [] From 9ab76fb8849f583cdda905874e9ff3ee20d5040e Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Fri, 27 May 2016 16:58:49 +0900 Subject: [PATCH 15/38] Add webContents.sendToAll to send message to all frames --- atom/browser/api/atom_api_web_contents.cc | 8 +++- atom/browser/api/atom_api_web_contents.h | 2 + atom/common/api/api_messages.h | 3 +- atom/common/api/remote_callback_freer.cc | 2 +- atom/renderer/atom_render_view_observer.cc | 50 ++++++++++++++++------ atom/renderer/atom_render_view_observer.h | 3 +- lib/browser/api/web-contents.js | 8 ++++ 7 files changed, 58 insertions(+), 18 deletions(-) diff --git a/atom/browser/api/atom_api_web_contents.cc b/atom/browser/api/atom_api_web_contents.cc index df46eb883df8..b38362972e01 100644 --- a/atom/browser/api/atom_api_web_contents.cc +++ b/atom/browser/api/atom_api_web_contents.cc @@ -1090,7 +1090,12 @@ void WebContents::TabTraverse(bool reverse) { bool WebContents::SendIPCMessage(const base::string16& channel, const base::ListValue& args) { - return Send(new AtomViewMsg_Message(routing_id(), channel, args)); + return Send(new AtomViewMsg_Message(routing_id(), false, channel, args)); +} + +bool WebContents::SendIPCMessageToAll(const base::string16& channel, + const base::ListValue& args) { + return Send(new AtomViewMsg_Message(routing_id(), true, channel, args)); } void WebContents::SendInputEvent(v8::Isolate* isolate, @@ -1258,6 +1263,7 @@ void WebContents::BuildPrototype(v8::Isolate* isolate, .SetMethod("focus", &WebContents::Focus) .SetMethod("tabTraverse", &WebContents::TabTraverse) .SetMethod("_send", &WebContents::SendIPCMessage) + .SetMethod("_sendToAll", &WebContents::SendIPCMessageToAll) .SetMethod("sendInputEvent", &WebContents::SendInputEvent) .SetMethod("beginFrameSubscription", &WebContents::BeginFrameSubscription) diff --git a/atom/browser/api/atom_api_web_contents.h b/atom/browser/api/atom_api_web_contents.h index f3661bedd253..7f90c33ea8f0 100644 --- a/atom/browser/api/atom_api_web_contents.h +++ b/atom/browser/api/atom_api_web_contents.h @@ -124,6 +124,8 @@ class WebContents : public mate::TrackableObject, // Send messages to browser. bool SendIPCMessage(const base::string16& channel, const base::ListValue& args); + bool SendIPCMessageToAll(const base::string16& channel, + const base::ListValue& args); // Send WebInputEvent to the page. void SendInputEvent(v8::Isolate* isolate, v8::Local input_event); diff --git a/atom/common/api/api_messages.h b/atom/common/api/api_messages.h index c7ba38de1d33..ab27d5a2516e 100644 --- a/atom/common/api/api_messages.h +++ b/atom/common/api/api_messages.h @@ -30,7 +30,8 @@ IPC_SYNC_MESSAGE_ROUTED2_1(AtomViewHostMsg_Message_Sync, base::ListValue /* arguments */, base::string16 /* result (in JSON) */) -IPC_MESSAGE_ROUTED2(AtomViewMsg_Message, +IPC_MESSAGE_ROUTED3(AtomViewMsg_Message, + bool /* send_to_all */, base::string16 /* channel */, base::ListValue /* arguments */) diff --git a/atom/common/api/remote_callback_freer.cc b/atom/common/api/remote_callback_freer.cc index 7bc377efc5e7..d1a185d51f39 100644 --- a/atom/common/api/remote_callback_freer.cc +++ b/atom/common/api/remote_callback_freer.cc @@ -35,7 +35,7 @@ void RemoteCallbackFreer::RunDestructor() { base::ASCIIToUTF16("ELECTRON_RENDERER_RELEASE_CALLBACK"); base::ListValue args; args.AppendInteger(object_id_); - Send(new AtomViewMsg_Message(routing_id(), channel, args)); + Send(new AtomViewMsg_Message(routing_id(), false, channel, args)); Observe(nullptr); } diff --git a/atom/renderer/atom_render_view_observer.cc b/atom/renderer/atom_render_view_observer.cc index bbaea351378b..7ee93efb39b7 100644 --- a/atom/renderer/atom_render_view_observer.cc +++ b/atom/renderer/atom_render_view_observer.cc @@ -59,6 +59,34 @@ std::vector> ListValueToVector( return result; } +void EmitIPCEvent(blink::WebFrame* frame, + const base::string16& channel, + const base::ListValue& args) { + if (!frame || frame->isWebRemoteFrame()) + return; + + v8::Isolate* isolate = blink::mainThreadIsolate(); + v8::HandleScope handle_scope(isolate); + + v8::Local context = frame->mainWorldScriptContext(); + v8::Context::Scope context_scope(context); + + // Only emit IPC event for context with node integration. + node::Environment* env = node::Environment::GetCurrent(context); + if (!env) + return; + + v8::Local ipc; + if (GetIPCObject(isolate, context, &ipc)) { + auto args_vector = ListValueToVector(isolate, args); + // Insert the Event object, event.sender is ipc. + mate::Dictionary event = mate::Dictionary::CreateEmpty(isolate); + event.Set("sender", ipc); + args_vector.insert(args_vector.begin(), event.GetHandle()); + mate::EmitEvent(isolate, ipc, channel, args_vector); + } +} + base::StringPiece NetResourceProvider(int key) { if (key == IDR_DIR_HEADER_HTML) { base::StringPiece html_data = @@ -123,7 +151,8 @@ bool AtomRenderViewObserver::OnMessageReceived(const IPC::Message& message) { return handled; } -void AtomRenderViewObserver::OnBrowserMessage(const base::string16& channel, +void AtomRenderViewObserver::OnBrowserMessage(bool send_to_all, + const base::string16& channel, const base::ListValue& args) { if (!document_created_) return; @@ -135,20 +164,13 @@ void AtomRenderViewObserver::OnBrowserMessage(const base::string16& channel, if (!frame || frame->isWebRemoteFrame()) return; - v8::Isolate* isolate = blink::mainThreadIsolate(); - v8::HandleScope handle_scope(isolate); + EmitIPCEvent(frame, channel, args); - v8::Local context = frame->mainWorldScriptContext(); - v8::Context::Scope context_scope(context); - - v8::Local ipc; - if (GetIPCObject(isolate, context, &ipc)) { - auto args_vector = ListValueToVector(isolate, args); - // Insert the Event object, event.sender is ipc. - mate::Dictionary event = mate::Dictionary::CreateEmpty(isolate); - event.Set("sender", ipc); - args_vector.insert(args_vector.begin(), event.GetHandle()); - mate::EmitEvent(isolate, ipc, channel, args_vector); + // Also send the message to all sub-frames. + if (send_to_all) { + for (blink::WebFrame* child = frame->firstChild(); child; + child = child->nextSibling()) + EmitIPCEvent(child, channel, args); } } diff --git a/atom/renderer/atom_render_view_observer.h b/atom/renderer/atom_render_view_observer.h index 4b9d59f3fa08..376138f0849a 100644 --- a/atom/renderer/atom_render_view_observer.h +++ b/atom/renderer/atom_render_view_observer.h @@ -30,7 +30,8 @@ class AtomRenderViewObserver : public content::RenderViewObserver { void DraggableRegionsChanged(blink::WebFrame* frame) override; bool OnMessageReceived(const IPC::Message& message) override; - void OnBrowserMessage(const base::string16& channel, + void OnBrowserMessage(bool send_to_all, + const base::string16& channel, const base::ListValue& args); // Weak reference to renderer client. diff --git a/lib/browser/api/web-contents.js b/lib/browser/api/web-contents.js index 6253f23b3063..03697199836e 100644 --- a/lib/browser/api/web-contents.js +++ b/lib/browser/api/web-contents.js @@ -78,6 +78,14 @@ let wrapWebContents = function (webContents) { return this._send(channel, args) } + // WebContents::sendToAll(channel, args..) + webContents.sendToAll = function (channel, ...args) { + if (channel == null) { + throw new Error('Missing required channel argument') + } + return this._sendToAll(channel, args) + } + // The navigation controller. controller = new NavigationController(webContents) ref1 = NavigationController.prototype From dfe7ae21249b02bbeee00501813ddca3c9e094a0 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Sat, 28 May 2016 10:10:24 +0900 Subject: [PATCH 16/38] Add webContents.fromId --- atom/browser/api/atom_api_web_contents.cc | 2 ++ lib/browser/api/web-contents.js | 11 +++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/atom/browser/api/atom_api_web_contents.cc b/atom/browser/api/atom_api_web_contents.cc index b38362972e01..9f447f8e34e4 100644 --- a/atom/browser/api/atom_api_web_contents.cc +++ b/atom/browser/api/atom_api_web_contents.cc @@ -1344,6 +1344,8 @@ void Initialize(v8::Local exports, v8::Local unused, mate::Dictionary dict(isolate, exports); dict.SetMethod("create", &atom::api::WebContents::Create); dict.SetMethod("_setWrapWebContents", &atom::api::SetWrapWebContents); + dict.SetMethod("fromId", + &mate::TrackableObject::FromWeakMapID); } } // namespace diff --git a/lib/browser/api/web-contents.js b/lib/browser/api/web-contents.js index 03697199836e..1b601f095dbb 100644 --- a/lib/browser/api/web-contents.js +++ b/lib/browser/api/web-contents.js @@ -226,9 +226,12 @@ binding._setWrapWebContents(wrapWebContents) debuggerBinding._setWrapDebugger(wrapDebugger) sessionBinding._setWrapSession(wrapSession) -module.exports.create = function (options) { - if (options == null) { - options = {} +module.exports = { + create (options = {}) { + return binding.create(options) + }, + + fromId (id) { + return binding.fromId(id) } - return binding.create(options) } From e76c36a9a86f2b2e866f795294328c1fdc637821 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Sat, 28 May 2016 10:26:41 +0900 Subject: [PATCH 17/38] Make simple runtime.connect work --- lib/browser/chrome-extension.js | 25 +++++++++- lib/renderer/chrome-api.js | 86 ++++++++++++++++++++++++++++++++- 2 files changed, 109 insertions(+), 2 deletions(-) diff --git a/lib/browser/chrome-extension.js b/lib/browser/chrome-extension.js index 9a86e2fc34dc..b0045294f38e 100644 --- a/lib/browser/chrome-extension.js +++ b/lib/browser/chrome-extension.js @@ -1,4 +1,4 @@ -const {app, protocol, webContents, BrowserWindow} = require('electron') +const {app, ipcMain, protocol, webContents, BrowserWindow} = require('electron') const renderProcessPreferences = process.atomBinding('render_process_preferences').forAllBrowserWindow() const fs = require('fs') @@ -67,6 +67,7 @@ const startBackgroundPages = function (manifest) { hostname: manifest.hostname, pathname: '_generated_background_page.html' })) + contents.openDevTools() } const removeBackgroundPages = function (manifest) { @@ -76,6 +77,28 @@ const removeBackgroundPages = function (manifest) { delete backgroundPages[manifest.hostname] } +// Handle the chrome.* API messages. +ipcMain.on('CHROME_RUNTIME_CONNECT', function (event, hostname, connectInfo) { + const page = backgroundPages[hostname] + if (!page) { + console.error(`Connect to unkown extension ${hostname}`) + return + } + + event.returnValue = page.webContents.id + page.webContents.sendToAll('CHROME_RUNTIME_ONCONNECT', event.sender.id, hostname, connectInfo) +}) + +ipcMain.on('CHROME_PORT_POSTMESSAGE', function (event, webContentsId, hostname, message) { + const contents = webContents.fromId(webContentsId) + if (!contents) { + console.error(`Sending message to extension ${hostname} with unkown webContentsId ${webContentsId}`) + return + } + + contents.sendToAll(`CHROME_PORT_ONMESSAGE_${hostname}`, message) +}) + // Transfer the content scripts to renderer. const contentScripts = {} diff --git a/lib/renderer/chrome-api.js b/lib/renderer/chrome-api.js index 719066a6fae7..84577a7c7941 100644 --- a/lib/renderer/chrome-api.js +++ b/lib/renderer/chrome-api.js @@ -1,4 +1,68 @@ +const {ipcRenderer} = require('electron') const url = require('url') + +// TODO(zcbenz): Set it to correct value for content scripts. +const currentExtensionId = window.location.hostname + +class Event { + constructor () { + this.listeners = [] + } + + addListener (callback) { + this.listeners.push(callback) + } + + emit (...args) { + for (const listener of this.listeners) { + listener(...args) + } + } +} + +class OnConnect extends Event { + constructor () { + super() + + ipcRenderer.on('CHROME_RUNTIME_ONCONNECT', (event, webContentsId, extensionId, connectInfo) => { + this.emit(new Port(webContentsId, extensionId, connectInfo.name)) + }) + } +} + +class MessageSender { + constructor () { + this.tab = null + this.frameId = null + this.id = null + this.url = null + this.tlsChannelId = null + } +} + +class Port { + constructor (webContentsId, extensionId, name) { + this.webContentsId = webContentsId + this.extensionId = extensionId + + this.name = name + this.onDisconnect = new Event() + this.onMessage = new Event() + this.sender = new MessageSender() + + ipcRenderer.on(`CHROME_PORT_ONMESSAGE_${extensionId}`, (event, message) => { + this.onMessage.emit(message, new MessageSender(), function () {}) + }) + } + + disconnect () { + } + + postMessage (message) { + ipcRenderer.send('CHROME_PORT_POSTMESSAGE', this.webContentsId, this.extensionId, message) + } +} + const chrome = window.chrome = window.chrome || {} chrome.extension = { @@ -6,8 +70,28 @@ chrome.extension = { return url.format({ protocol: window.location.protocol, slashes: true, - hostname: window.location.hostname, + hostname: currentExtensionId, pathname: path }) } } + +chrome.runtime = { + getURL: chrome.extension.getURL, + + onConnect: new OnConnect(), + + connect (...args) { + // Parse the optional args. + let extensionId = currentExtensionId + let connectInfo = {name: ''} + if (args.length === 1) { + connectInfo = args[0] + } else if (args.length === 2) { + [extensionId, connectInfo] = args + } + + const webContentsId = ipcRenderer.sendSync('CHROME_RUNTIME_CONNECT', extensionId, connectInfo) + return new Port(webContentsId, extensionId, connectInfo.name) + } +} From 599d3c147b909137bfdd9191e0e70948678e0943 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Sat, 28 May 2016 10:39:11 +0900 Subject: [PATCH 18/38] Background pages and content script should be loaded on startup --- lib/browser/chrome-extension.js | 37 ++++++++++++++++----------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/lib/browser/chrome-extension.js b/lib/browser/chrome-extension.js index b0045294f38e..835d1b150f5b 100644 --- a/lib/browser/chrome-extension.js +++ b/lib/browser/chrome-extension.js @@ -149,10 +149,6 @@ const manifestToExtensionInfo = function (manifest) { const loadDevToolsExtensions = function (win, manifests) { if (!win.devToolsWebContents) return - for (const manifest of manifests) { - startBackgroundPages(manifest) - injectContentScripts(manifest) - } const extensionInfoArray = manifests.map(manifestToExtensionInfo) win.devToolsWebContents.executeJavaScript(`DevToolsAPI.addExtensions(${JSON.stringify(extensionInfoArray)})`) } @@ -182,20 +178,6 @@ app.on('will-quit', function () { // 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)) { - // Preheat the manifest cache. - for (const srcDirectory of loadedExtensions) { - getManifestFromPath(srcDirectory) - } - } - } catch (error) { - // Ignore error - } - // The chrome-extension: can map a extension URL request to real file path. const chromeExtensionHandler = function (request, callback) { const parsed = url.parse(request.url) @@ -227,6 +209,23 @@ 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) + startBackgroundPages(manifest) + injectContentScripts(manifest) + } + } + } catch (error) { + // Ignore error + } + + // The public API to add/remove extensions. BrowserWindow.addDevToolsExtension = function (srcDirectory) { const manifest = getManifestFromPath(srcDirectory) if (manifest) { @@ -245,7 +244,7 @@ app.once('ready', function () { delete manifestMap[name] } - // Load persisted extensions when devtools is opened. + // Load extensions automatically when devtools is opened. const init = BrowserWindow.prototype._init BrowserWindow.prototype._init = function () { init.call(this) From d8db695712464cc7e89935a8381101d56012facb Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Sat, 28 May 2016 12:07:08 +0900 Subject: [PATCH 19/38] Handle port disconnecting --- atom/renderer/atom_renderer_client.cc | 5 ++--- lib/browser/chrome-extension.js | 27 ++++++++++++++++++++++----- lib/renderer/chrome-api.js | 26 ++++++++++++++++++-------- 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/atom/renderer/atom_renderer_client.cc b/atom/renderer/atom_renderer_client.cc index 673ec4aa5d75..283930b829a2 100644 --- a/atom/renderer/atom_renderer_client.cc +++ b/atom/renderer/atom_renderer_client.cc @@ -266,10 +266,9 @@ void AtomRendererClient::DidCreateScriptContext( void AtomRendererClient::WillReleaseScriptContext( v8::Handle context, content::RenderFrame* render_frame) { - if (render_frame->IsMainFrame()) { - node::Environment* env = node::Environment::GetCurrent(context); + node::Environment* env = node::Environment::GetCurrent(context); + if (env) mate::EmitEvent(env->isolate(), env->process_object(), "exit"); - } } bool AtomRendererClient::ShouldFork(blink::WebLocalFrame* frame, diff --git a/lib/browser/chrome-extension.js b/lib/browser/chrome-extension.js index 835d1b150f5b..0c922d9d3e14 100644 --- a/lib/browser/chrome-extension.js +++ b/lib/browser/chrome-extension.js @@ -78,6 +78,8 @@ const removeBackgroundPages = function (manifest) { } // Handle the chrome.* API messages. +let nextId = 0 + ipcMain.on('CHROME_RUNTIME_CONNECT', function (event, hostname, connectInfo) { const page = backgroundPages[hostname] if (!page) { @@ -85,18 +87,33 @@ ipcMain.on('CHROME_RUNTIME_CONNECT', function (event, hostname, connectInfo) { return } - event.returnValue = page.webContents.id - page.webContents.sendToAll('CHROME_RUNTIME_ONCONNECT', event.sender.id, hostname, connectInfo) + const portId = ++nextId + event.returnValue = {webContentsId: page.webContents.id, portId: portId} + + event.sender.once('render-view-deleted', () => { + page.webContents.sendToAll(`CHROME_PORT_ONDISCONNECT_${portId}`) + }) + page.webContents.sendToAll('CHROME_RUNTIME_ONCONNECT', event.sender.id, portId, connectInfo) }) -ipcMain.on('CHROME_PORT_POSTMESSAGE', function (event, webContentsId, hostname, message) { +ipcMain.on('CHROME_PORT_DISCONNECT', function (event, webContentsId, portId) { const contents = webContents.fromId(webContentsId) if (!contents) { - console.error(`Sending message to extension ${hostname} with unkown webContentsId ${webContentsId}`) + console.error(`Disconnet to unkown webContentsId ${webContentsId}`) return } - contents.sendToAll(`CHROME_PORT_ONMESSAGE_${hostname}`, message) + contents.sendToAll(`CHROME_PORT_ONDISCONNECT_${portId}`) +}) + +ipcMain.on('CHROME_PORT_POSTMESSAGE', function (event, webContentsId, portId, message) { + const contents = webContents.fromId(webContentsId) + if (!contents) { + console.error(`Sending message to unkown webContentsId ${webContentsId}`) + return + } + + contents.sendToAll(`CHROME_PORT_ONMESSAGE_${portId}`, message) }) // Transfer the content scripts to renderer. diff --git a/lib/renderer/chrome-api.js b/lib/renderer/chrome-api.js index 84577a7c7941..0040d50bac3d 100644 --- a/lib/renderer/chrome-api.js +++ b/lib/renderer/chrome-api.js @@ -24,8 +24,8 @@ class OnConnect extends Event { constructor () { super() - ipcRenderer.on('CHROME_RUNTIME_ONCONNECT', (event, webContentsId, extensionId, connectInfo) => { - this.emit(new Port(webContentsId, extensionId, connectInfo.name)) + ipcRenderer.on('CHROME_RUNTIME_ONCONNECT', (event, webContentsId, portId, connectInfo) => { + this.emit(new Port(webContentsId, portId, connectInfo.name)) }) } } @@ -41,25 +41,35 @@ class MessageSender { } class Port { - constructor (webContentsId, extensionId, name) { + constructor (webContentsId, portId, name) { this.webContentsId = webContentsId - this.extensionId = extensionId + this.portId = portId this.name = name this.onDisconnect = new Event() this.onMessage = new Event() this.sender = new MessageSender() - ipcRenderer.on(`CHROME_PORT_ONMESSAGE_${extensionId}`, (event, message) => { + ipcRenderer.once(`CHROME_PORT_ONDISCONNECT_${portId}`, () => { + this._onDisconnect() + }) + ipcRenderer.on(`CHROME_PORT_ONMESSAGE_${portId}`, (event, message) => { this.onMessage.emit(message, new MessageSender(), function () {}) }) } disconnect () { + ipcRenderer.send('CHROME_PORT_DISCONNECT', this.webContentsId, this.portId) + this._onDisconnect() } postMessage (message) { - ipcRenderer.send('CHROME_PORT_POSTMESSAGE', this.webContentsId, this.extensionId, message) + ipcRenderer.send('CHROME_PORT_POSTMESSAGE', this.webContentsId, this.portId, message) + } + + _onDisconnect() { + ipcRenderer.removeAllListeners(`CHROME_PORT_ONMESSAGE_${this.portId}`) + this.onDisconnect.emit() } } @@ -91,7 +101,7 @@ chrome.runtime = { [extensionId, connectInfo] = args } - const webContentsId = ipcRenderer.sendSync('CHROME_RUNTIME_CONNECT', extensionId, connectInfo) - return new Port(webContentsId, extensionId, connectInfo.name) + const {webContentsId, portId} = ipcRenderer.sendSync('CHROME_RUNTIME_CONNECT', extensionId, connectInfo) + return new Port(webContentsId, portId, connectInfo.name) } } From f5b430d9e16777aa97c0d1e674299146e10ce597 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Sat, 28 May 2016 15:37:44 +0900 Subject: [PATCH 20/38] Inject chrome.* to content scripts --- lib/browser/chrome-extension.js | 5 +- lib/renderer/chrome-api.js | 63 +++++++++++++----------- lib/renderer/content-scripts-injector.js | 19 +++++-- lib/renderer/init.js | 2 +- 4 files changed, 56 insertions(+), 33 deletions(-) diff --git a/lib/browser/chrome-extension.js b/lib/browser/chrome-extension.js index 0c922d9d3e14..a03be0432ca2 100644 --- a/lib/browser/chrome-extension.js +++ b/lib/browser/chrome-extension.js @@ -123,7 +123,10 @@ const injectContentScripts = function (manifest) { if (contentScripts[manifest.name] || !manifest.content_scripts) return const readArrayOfFiles = function (relativePath) { - return String(fs.readFileSync(path.join(manifest.srcDirectory, relativePath))) + return { + url: `chrome-extension://${manifest.hostname}/${relativePath}`, + code: String(fs.readFileSync(path.join(manifest.srcDirectory, relativePath))) + } } const contentScriptToEntry = function (script) { diff --git a/lib/renderer/chrome-api.js b/lib/renderer/chrome-api.js index 0040d50bac3d..a1bbbe5f102a 100644 --- a/lib/renderer/chrome-api.js +++ b/lib/renderer/chrome-api.js @@ -1,9 +1,6 @@ const {ipcRenderer} = require('electron') const url = require('url') -// TODO(zcbenz): Set it to correct value for content scripts. -const currentExtensionId = window.location.hostname - class Event { constructor () { this.listeners = [] @@ -13,6 +10,13 @@ class Event { 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) @@ -67,41 +71,44 @@ class Port { ipcRenderer.send('CHROME_PORT_POSTMESSAGE', this.webContentsId, this.portId, message) } - _onDisconnect() { + _onDisconnect () { ipcRenderer.removeAllListeners(`CHROME_PORT_ONMESSAGE_${this.portId}`) this.onDisconnect.emit() } } -const chrome = window.chrome = window.chrome || {} +// Inject chrome API to the |context| +exports.injectTo = function (extensionId, context) { + const chrome = context.chrome = context.chrome || {} -chrome.extension = { - getURL: function (path) { - return url.format({ - protocol: window.location.protocol, - slashes: true, - hostname: currentExtensionId, - pathname: path - }) - } -} + chrome.runtime = { + getURL: function (path) { + return url.format({ + protocol: 'chrome-extension', + slashes: true, + hostname: extensionId, + pathname: path + }) + }, -chrome.runtime = { - getURL: chrome.extension.getURL, + onConnect: new OnConnect(), - onConnect: new OnConnect(), + connect (...args) { + // 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 + } - connect (...args) { - // Parse the optional args. - let extensionId = currentExtensionId - let connectInfo = {name: ''} - if (args.length === 1) { - connectInfo = args[0] - } else if (args.length === 2) { - [extensionId, connectInfo] = args + const {webContentsId, portId} = ipcRenderer.sendSync('CHROME_RUNTIME_CONNECT', targetExtensionId, connectInfo) + return new Port(webContentsId, portId, connectInfo.name) } + } - const {webContentsId, portId} = ipcRenderer.sendSync('CHROME_RUNTIME_CONNECT', extensionId, connectInfo) - return new Port(webContentsId, portId, connectInfo.name) + chrome.extension = { + getURL: chrome.runtime.getURL } } diff --git a/lib/renderer/content-scripts-injector.js b/lib/renderer/content-scripts-injector.js index 07f330b7cc15..4e85d587ad36 100644 --- a/lib/renderer/content-scripts-injector.js +++ b/lib/renderer/content-scripts-injector.js @@ -1,4 +1,4 @@ -const {webFrame} = require('electron') +const {runInThisContext} = require('vm') // Check whether pattern matches. // https://developer.chrome.com/extensions/match_patterns @@ -9,6 +9,19 @@ const matchesPattern = function (pattern) { return location.href.match(regexp) } +// Run the code with chrome API integrated. +const runContentScript = function (extensionId, url, code) { + const chrome = {} + require('./chrome-api').injectTo(extensionId, chrome) + const wrapper = `(function (chrome) {\n ${code}\n })` + const compiledWrapper = runInThisContext(wrapper, { + filename: url, + lineOffset: 1, + displayErrors: true + }) + compiledWrapper.call(this, chrome) +} + // Run injected scripts. // https://developer.chrome.com/extensions/content_scripts const injectContentScript = function (script) { @@ -16,8 +29,8 @@ const injectContentScript = function (script) { if (!matchesPattern(match)) return } - for (const js of script.js) { - const fire = () => webFrame.executeJavaScript(js) + for (const {url, code} of script.js) { + const fire = runContentScript.bind(window, script.extensionId, url, code) if (script.runAt === 'document_start') { process.once('document-start', fire) } else if (script.runAt === 'document_end') { diff --git a/lib/renderer/init.js b/lib/renderer/init.js index 6138b6ba575b..b0baf438c6c9 100644 --- a/lib/renderer/init.js +++ b/lib/renderer/init.js @@ -63,7 +63,7 @@ if (window.location.protocol === 'chrome-devtools:') { nodeIntegration = 'true' } else if (window.location.protocol === 'chrome-extension:') { // Add implementations of chrome API. - require('./chrome-api') + require('./chrome-api').injectTo(window.location.hostname, window) nodeIntegration = 'true' } else { // Override default web functions. From db941213601d389be2b1d8abf0b52694f0d9a9e0 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Sat, 28 May 2016 16:01:16 +0900 Subject: [PATCH 21/38] Implement port.sender --- lib/browser/chrome-extension.js | 2 +- lib/renderer/chrome-api.js | 39 +++++++++++++++++++-------------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/lib/browser/chrome-extension.js b/lib/browser/chrome-extension.js index a03be0432ca2..0dac4f985e0b 100644 --- a/lib/browser/chrome-extension.js +++ b/lib/browser/chrome-extension.js @@ -93,7 +93,7 @@ ipcMain.on('CHROME_RUNTIME_CONNECT', function (event, hostname, connectInfo) { event.sender.once('render-view-deleted', () => { page.webContents.sendToAll(`CHROME_PORT_ONDISCONNECT_${portId}`) }) - page.webContents.sendToAll('CHROME_RUNTIME_ONCONNECT', event.sender.id, portId, connectInfo) + page.webContents.sendToAll('CHROME_RUNTIME_ONCONNECT', event.sender.id, portId, hostname, connectInfo) }) ipcMain.on('CHROME_PORT_DISCONNECT', function (event, webContentsId, portId) { diff --git a/lib/renderer/chrome-api.js b/lib/renderer/chrome-api.js index a1bbbe5f102a..0c9fbe9e9e7b 100644 --- a/lib/renderer/chrome-api.js +++ b/lib/renderer/chrome-api.js @@ -24,41 +24,36 @@ class Event { } } -class OnConnect extends Event { - constructor () { - super() - - ipcRenderer.on('CHROME_RUNTIME_ONCONNECT', (event, webContentsId, portId, connectInfo) => { - this.emit(new Port(webContentsId, portId, connectInfo.name)) - }) +class Tab { + constructor (webContentsId) { + this.id = webContentsId } } class MessageSender { - constructor () { - this.tab = null - this.frameId = null - this.id = null - this.url = null - this.tlsChannelId = null + constructor (webContentsId, extensionId) { + this.tab = new Tab(webContentsId) + this.id = extensionId + this.url = `chrome-extension://${extensionId}` } } class Port { - constructor (webContentsId, portId, name) { + constructor (webContentsId, portId, extensionId, name) { this.webContentsId = webContentsId this.portId = portId this.name = name this.onDisconnect = new Event() this.onMessage = new Event() - this.sender = new MessageSender() + this.sender = new MessageSender(webContentsId, extensionId) ipcRenderer.once(`CHROME_PORT_ONDISCONNECT_${portId}`, () => { this._onDisconnect() }) ipcRenderer.on(`CHROME_PORT_ONMESSAGE_${portId}`, (event, message) => { - this.onMessage.emit(message, new MessageSender(), function () {}) + const sendResponse = function () { console.error('sendResponse is not implemented') } + this.onMessage.emit(message, this.sender, sendResponse) }) } @@ -77,6 +72,16 @@ class Port { } } +class OnConnect extends Event { + constructor () { + super() + + ipcRenderer.on('CHROME_RUNTIME_ONCONNECT', (event, webContentsId, portId, extensionId, connectInfo) => { + this.emit(new Port(webContentsId, portId, extensionId, connectInfo.name)) + }) + } +} + // Inject chrome API to the |context| exports.injectTo = function (extensionId, context) { const chrome = context.chrome = context.chrome || {} @@ -104,7 +109,7 @@ exports.injectTo = function (extensionId, context) { } const {webContentsId, portId} = ipcRenderer.sendSync('CHROME_RUNTIME_CONNECT', targetExtensionId, connectInfo) - return new Port(webContentsId, portId, connectInfo.name) + return new Port(webContentsId, portId, extensionId, connectInfo.name) } } From 31628abadc94f737d461f0ae0f776c34db8e6d07 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Sat, 28 May 2016 16:41:12 +0900 Subject: [PATCH 22/38] Implement chrome.tabs.executeScript --- lib/browser/chrome-extension.js | 29 ++++++++++++++++++++++++ lib/renderer/chrome-api.js | 12 ++++++++++ lib/renderer/content-scripts-injector.js | 9 +++++++- 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/lib/browser/chrome-extension.js b/lib/browser/chrome-extension.js index 0dac4f985e0b..6f710f917ca2 100644 --- a/lib/browser/chrome-extension.js +++ b/lib/browser/chrome-extension.js @@ -116,6 +116,35 @@ ipcMain.on('CHROME_PORT_POSTMESSAGE', function (event, webContentsId, portId, me contents.sendToAll(`CHROME_PORT_ONMESSAGE_${portId}`, message) }) +ipcMain.on('CHROME_TABS_EXECUTESCRIPT', function (event, requestId, webContentsId, hostname, details) { + const contents = webContents.fromId(webContentsId) + if (!contents) { + console.error(`Sending message to unkown webContentsId ${webContentsId}`) + return + } + + let code, url + if (details.file) { + code = String(fs.readFileSync(path.join(manifest.srcDirectory, details.file))) + url = `chrome-extension://${hostname}/${details.file}` + } else { + code = details.code + url = `chrome-extension://${hostname}/${String(Math.random()).substr(2, 8)}.js` + } + + contents.send('CHROME_TABS_EXECUTESCRIPT', requestId, event.sender.id, hostname, url, code) +}) + +ipcMain.on(`CHROME_TABS_EXECUTESCRIPT_RESULT`, (event, requestId, webContentsId, result) => { + const contents = webContents.fromId(webContentsId) + if (!contents) { + console.error(`Sending message to unkown webContentsId ${webContentsId}`) + return + } + + contents.send(`CHROME_TABS_EXECUTESCRIPT_RESULT_${requestId}`, result) +}) + // Transfer the content scripts to renderer. const contentScripts = {} diff --git a/lib/renderer/chrome-api.js b/lib/renderer/chrome-api.js index 0c9fbe9e9e7b..a0925599cca1 100644 --- a/lib/renderer/chrome-api.js +++ b/lib/renderer/chrome-api.js @@ -1,6 +1,8 @@ const {ipcRenderer} = require('electron') const url = require('url') +let nextId = 0 + class Event { constructor () { this.listeners = [] @@ -113,6 +115,16 @@ exports.injectTo = function (extensionId, context) { } } + 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) + } + } + chrome.extension = { getURL: chrome.runtime.getURL } diff --git a/lib/renderer/content-scripts-injector.js b/lib/renderer/content-scripts-injector.js index 4e85d587ad36..1ef9fc2137d6 100644 --- a/lib/renderer/content-scripts-injector.js +++ b/lib/renderer/content-scripts-injector.js @@ -1,3 +1,4 @@ +const {ipcRenderer} = require('electron') const {runInThisContext} = require('vm') // Check whether pattern matches. @@ -19,7 +20,7 @@ const runContentScript = function (extensionId, url, code) { lineOffset: 1, displayErrors: true }) - compiledWrapper.call(this, chrome) + return compiledWrapper.call(this, chrome) } // Run injected scripts. @@ -41,6 +42,12 @@ const injectContentScript = function (script) { } } +// Handle the request of chrome.tabs.executeJavaScript. +ipcRenderer.on('CHROME_TABS_EXECUTESCRIPT', function (event, requestId, senderId, extensionId, url, code) { + const result = runContentScript.call(window, extensionId, url, code) + ipcRenderer.send('CHROME_TABS_EXECUTESCRIPT_RESULT', requestId, senderId, result) +}) + // Read the renderer process preferences. const preferences = process.getRenderProcessPreferences() if (preferences) { From d55b96fdf511a10ad647ae03596a105c61901d3e Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Sat, 28 May 2016 17:51:49 +0900 Subject: [PATCH 23/38] Clean up the Chrome API implementation code --- lib/browser/chrome-extension.js | 86 ++++++++++++------------ lib/renderer/chrome-api.js | 4 ++ lib/renderer/content-scripts-injector.js | 14 ++-- 3 files changed, 54 insertions(+), 50 deletions(-) diff --git a/lib/browser/chrome-extension.js b/lib/browser/chrome-extension.js index 6f710f917ca2..e9abb998b806 100644 --- a/lib/browser/chrome-extension.js +++ b/lib/browser/chrome-extension.js @@ -10,37 +10,27 @@ const objectValues = function (object) { return Object.keys(object).map(function (key) { return object[key] }) } -// Mapping between hostname and file path. -const hostPathMap = {} -let hostPathMapNextKey = 0 +// Mapping between extensionId(hostname) and manifest. +const manifestMap = {} // extensionId => manifest +const manifestNameMap = {} // name => manifest -const generateHostForPath = function (path) { - const key = `extension-${++hostPathMapNextKey}` - hostPathMap[key] = path - return key -} - -const getPathForHost = function (host) { - return hostPathMap[host] -} - -// Cache manifests. -const manifestMap = {} +let nextExtensionId = 0 +// Create or get manifest object from |srcDirectory|. const getManifestFromPath = function (srcDirectory) { const manifest = JSON.parse(fs.readFileSync(path.join(srcDirectory, 'manifest.json'))) - if (!manifestMap[manifest.name]) { - const hostname = generateHostForPath(srcDirectory) - manifestMap[manifest.name] = manifest + if (!manifestNameMap[manifest.name]) { + const extensionId = `extension-${++nextExtensionId}` + manifestMap[extensionId] = manifestNameMap[manifest.name] = manifest Object.assign(manifest, { srcDirectory: srcDirectory, - hostname: hostname, + 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: hostname, + hostname: extensionId, pathname: manifest.devtools_page }) }) @@ -52,7 +42,7 @@ const getManifestFromPath = function (srcDirectory) { const backgroundPages = {} const startBackgroundPages = function (manifest) { - if (backgroundPages[manifest.hostname] || !manifest.background) return + if (backgroundPages[manifest.extensionId] || !manifest.background) return const scripts = manifest.background.scripts.map((name) => { return `` @@ -60,30 +50,29 @@ const startBackgroundPages = function (manifest) { const html = new Buffer(`${scripts}`) const contents = webContents.create({}) - backgroundPages[manifest.hostname] = { html: html, webContents: contents } + backgroundPages[manifest.extensionId] = { html: html, webContents: contents } contents.loadURL(url.format({ protocol: 'chrome-extension', slashes: true, - hostname: manifest.hostname, + hostname: manifest.extensionId, pathname: '_generated_background_page.html' })) - contents.openDevTools() } const removeBackgroundPages = function (manifest) { - if (!backgroundPages[manifest.hostname]) return + if (!backgroundPages[manifest.extensionId]) return - backgroundPages[manifest.hostname].webContents.destroy() - delete backgroundPages[manifest.hostname] + backgroundPages[manifest.extensionId].webContents.destroy() + delete backgroundPages[manifest.extensionId] } // Handle the chrome.* API messages. let nextId = 0 -ipcMain.on('CHROME_RUNTIME_CONNECT', function (event, hostname, connectInfo) { - const page = backgroundPages[hostname] +ipcMain.on('CHROME_RUNTIME_CONNECT', function (event, extensionId, connectInfo) { + const page = backgroundPages[extensionId] if (!page) { - console.error(`Connect to unkown extension ${hostname}`) + console.error(`Connect to unkown extension ${extensionId}`) return } @@ -93,7 +82,7 @@ ipcMain.on('CHROME_RUNTIME_CONNECT', function (event, hostname, connectInfo) { event.sender.once('render-view-deleted', () => { page.webContents.sendToAll(`CHROME_PORT_ONDISCONNECT_${portId}`) }) - page.webContents.sendToAll('CHROME_RUNTIME_ONCONNECT', event.sender.id, portId, hostname, connectInfo) + page.webContents.sendToAll('CHROME_RUNTIME_ONCONNECT', event.sender.id, portId, extensionId, connectInfo) }) ipcMain.on('CHROME_PORT_DISCONNECT', function (event, webContentsId, portId) { @@ -116,7 +105,7 @@ ipcMain.on('CHROME_PORT_POSTMESSAGE', function (event, webContentsId, portId, me contents.sendToAll(`CHROME_PORT_ONMESSAGE_${portId}`, message) }) -ipcMain.on('CHROME_TABS_EXECUTESCRIPT', function (event, requestId, webContentsId, hostname, details) { +ipcMain.on('CHROME_TABS_EXECUTESCRIPT', function (event, requestId, webContentsId, extensionId, details) { const contents = webContents.fromId(webContentsId) if (!contents) { console.error(`Sending message to unkown webContentsId ${webContentsId}`) @@ -125,17 +114,18 @@ ipcMain.on('CHROME_TABS_EXECUTESCRIPT', function (event, requestId, webContentsI let code, url if (details.file) { + const manifest = manifestMap[extensionId] code = String(fs.readFileSync(path.join(manifest.srcDirectory, details.file))) - url = `chrome-extension://${hostname}/${details.file}` + url = `chrome-extension://${extensionId}${details.file}` } else { code = details.code - url = `chrome-extension://${hostname}/${String(Math.random()).substr(2, 8)}.js` + url = `chrome-extension://${extensionId}/${String(Math.random()).substr(2, 8)}.js` } - contents.send('CHROME_TABS_EXECUTESCRIPT', requestId, event.sender.id, hostname, url, code) + contents.send('CHROME_TABS_EXECUTESCRIPT', requestId, event.sender.id, extensionId, url, code) }) -ipcMain.on(`CHROME_TABS_EXECUTESCRIPT_RESULT`, (event, requestId, webContentsId, result) => { +ipcMain.on('CHROME_TABS_EXECUTESCRIPT_RESULT', (event, requestId, webContentsId, result) => { const contents = webContents.fromId(webContentsId) if (!contents) { console.error(`Sending message to unkown webContentsId ${webContentsId}`) @@ -153,7 +143,7 @@ const injectContentScripts = function (manifest) { const readArrayOfFiles = function (relativePath) { return { - url: `chrome-extension://${manifest.hostname}/${relativePath}`, + url: `chrome-extension://${manifest.extensionId}/${relativePath}`, code: String(fs.readFileSync(path.join(manifest.srcDirectory, relativePath))) } } @@ -168,6 +158,7 @@ const injectContentScripts = function (manifest) { try { const entry = { + extensionId: manifest.extensionId, contentScripts: manifest.content_scripts.map(contentScriptToEntry) } contentScripts[manifest.name] = renderProcessPreferences.addEntry(entry) @@ -195,9 +186,16 @@ const manifestToExtensionInfo = function (manifest) { } // 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)})`) } @@ -233,8 +231,8 @@ app.once('ready', function () { if (!parsed.hostname || !parsed.path) return callback() if (!/extension-\d+/.test(parsed.hostname)) return callback() - const directory = getPathForHost(parsed.hostname) - if (!directory) return callback() + const manifest = manifestMap[parsed.hostname] + if (!manifest) return callback() if (parsed.path === '/_generated_background_page.html' && backgroundPages[parsed.hostname]) { @@ -244,7 +242,7 @@ app.once('ready', function () { }) } - fs.readFile(path.join(directory, parsed.path), function (err, content) { + fs.readFile(path.join(manifest.srcDirectory, parsed.path), function (err, content) { if (err) { return callback(-6) // FILE_NOT_FOUND } else { @@ -266,8 +264,7 @@ app.once('ready', function () { for (const srcDirectory of loadedExtensions) { // Start background pages and set content scripts. const manifest = getManifestFromPath(srcDirectory) - startBackgroundPages(manifest) - injectContentScripts(manifest) + loadExtension(manifest) } } } catch (error) { @@ -285,12 +282,13 @@ app.once('ready', function () { } } BrowserWindow.removeDevToolsExtension = function (name) { - const manifest = manifestMap[name] + const manifest = manifestNameMap[name] if (!manifest) return removeBackgroundPages(manifest) removeContentScripts(manifest) - delete manifestMap[name] + delete manifestMap[manifest.extensionId] + delete manifestNameMap[name] } // Load extensions automatically when devtools is opened. diff --git a/lib/renderer/chrome-api.js b/lib/renderer/chrome-api.js index a0925599cca1..c49fbd2f8efe 100644 --- a/lib/renderer/chrome-api.js +++ b/lib/renderer/chrome-api.js @@ -44,6 +44,7 @@ class Port { constructor (webContentsId, portId, extensionId, name) { this.webContentsId = webContentsId this.portId = portId + this.disconnected = false this.name = name this.onDisconnect = new Event() @@ -60,6 +61,8 @@ class Port { } disconnect () { + if (this.disconnected) return + ipcRenderer.send('CHROME_PORT_DISCONNECT', this.webContentsId, this.portId) this._onDisconnect() } @@ -69,6 +72,7 @@ class Port { } _onDisconnect () { + this.disconnected = true ipcRenderer.removeAllListeners(`CHROME_PORT_ONMESSAGE_${this.portId}`) this.onDisconnect.emit() } diff --git a/lib/renderer/content-scripts-injector.js b/lib/renderer/content-scripts-injector.js index 1ef9fc2137d6..6e41a8d4f116 100644 --- a/lib/renderer/content-scripts-injector.js +++ b/lib/renderer/content-scripts-injector.js @@ -12,26 +12,26 @@ const matchesPattern = function (pattern) { // Run the code with chrome API integrated. const runContentScript = function (extensionId, url, code) { - const chrome = {} - require('./chrome-api').injectTo(extensionId, chrome) + const context = {} + require('./chrome-api').injectTo(extensionId, context) const wrapper = `(function (chrome) {\n ${code}\n })` const compiledWrapper = runInThisContext(wrapper, { filename: url, lineOffset: 1, displayErrors: true }) - return compiledWrapper.call(this, chrome) + return compiledWrapper.call(this, context.chrome) } // Run injected scripts. // https://developer.chrome.com/extensions/content_scripts -const injectContentScript = function (script) { +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, script.extensionId, url, code) + const fire = runContentScript.bind(window, extensionId, url, code) if (script.runAt === 'document_start') { process.once('document-start', fire) } else if (script.runAt === 'document_end') { @@ -53,7 +53,9 @@ const preferences = process.getRenderProcessPreferences() if (preferences) { for (const pref of preferences) { if (pref.contentScripts) { - pref.contentScripts.forEach(injectContentScript) + for (const script of pref.contentScripts) { + injectContentScript(pref.extensionId, script) + } } } } From ae1f442b0235afc4e6f211b8a9cb7f6150362c34 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Wed, 18 May 2016 21:19:50 +0900 Subject: [PATCH 24/38] Add ipcRenderer.sendTo --- lib/browser/rpc-server.js | 12 +++++++++++- lib/renderer/api/ipc-renderer.js | 8 ++++++++ lib/renderer/chrome-api.js | 21 ++++++++++++++++++++- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/lib/browser/rpc-server.js b/lib/browser/rpc-server.js index fe264cceeeff..8dc90e862cf9 100644 --- a/lib/browser/rpc-server.js +++ b/lib/browser/rpc-server.js @@ -2,7 +2,7 @@ const electron = require('electron') const v8Util = process.atomBinding('v8_util') -const {ipcMain, isPromise} = electron +const {ipcMain, isPromise, webContents} = electron const objectsRegistry = require('./objects-registry') @@ -351,3 +351,13 @@ ipcMain.on('ELECTRON_BROWSER_ASYNC_CALL_TO_GUEST_VIEW', function (event, request event.returnValue = exceptionToMeta(error) } }) + +ipcMain.on('ELECTRON_BROWSER_SEND_TO', function (event, webContentsId, channel, ...args) { + let contents = webContents.fromId(webContentsId) + if (!contents) { + console.error(`Sending message to WebContents with unknown ID ${webContentsId}`) + return + } + + contents.send(channel, ...args) +}) diff --git a/lib/renderer/api/ipc-renderer.js b/lib/renderer/api/ipc-renderer.js index a6b6b1851ea3..24bb9fe0be91 100644 --- a/lib/renderer/api/ipc-renderer.js +++ b/lib/renderer/api/ipc-renderer.js @@ -18,4 +18,12 @@ 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', webContentsId, channel, ...args) +} + module.exports = ipcRenderer diff --git a/lib/renderer/chrome-api.js b/lib/renderer/chrome-api.js index c49fbd2f8efe..40c37aafc2a3 100644 --- a/lib/renderer/chrome-api.js +++ b/lib/renderer/chrome-api.js @@ -130,6 +130,25 @@ exports.injectTo = function (extensionId, context) { } chrome.extension = { - getURL: chrome.runtime.getURL + getURL: chrome.runtime.getURL, + connect: chrome.runtime.connect + onConnect: chrome.runtime.onConnect + } + + chrome.storage = { + sync: { + get () {}, + set () {} + } + } + + chrome.pageAction = { + show: () {}, + hide: () {}, + setTitle: () {}, + getTitle: () {}, + setIcon: () {}, + setPopup: () {}, + getPopup: () {} } } From a58b84bbd76a931c43dfe1c5b9e9908a5f108518 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Wed, 18 May 2016 22:14:51 +0900 Subject: [PATCH 25/38] spec: ipcRenderer.sendTo sends message to WebContents --- spec/api-ipc-spec.js | 31 +++++++++++++++++++++++++----- spec/fixtures/pages/ping-pong.html | 11 +++++++++++ 2 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 spec/fixtures/pages/ping-pong.html diff --git a/spec/api-ipc-spec.js b/spec/api-ipc-spec.js index 102b23ece567..46aa847c17d0 100644 --- a/spec/api-ipc-spec.js +++ b/spec/api-ipc-spec.js @@ -3,11 +3,8 @@ const assert = require('assert') const path = require('path') -const ipcRenderer = require('electron').ipcRenderer -const remote = require('electron').remote - -const ipcMain = remote.require('electron').ipcMain -const BrowserWindow = remote.require('electron').BrowserWindow +const {ipcRenderer, remote} = require('electron') +const {ipcMain, webContents, BrowserWindow} = remote const comparePaths = function (path1, path2) { if (process.platform === 'win32') { @@ -237,6 +234,30 @@ describe('ipc module', function () { }) }) + describe('ipcRenderer.sendTo', function () { + let contents = null + beforeEach(function () { + contents = webContents.create({}) + }) + afterEach(function () { + ipcRenderer.removeAllListeners('pong') + contents.destroy() + contents = null + }) + + it('sends message to WebContents', function (done) { + const webContentsId = remote.getCurrentWebContents().id + ipcRenderer.once('pong', function (event, id) { + assert.equal(webContentsId, id) + done() + }) + contents.once('did-finish-load', function () { + ipcRenderer.sendTo(contents.id, 'ping', webContentsId) + }) + contents.loadURL('file://' + path.join(fixtures, 'pages', 'ping-pong.html')) + }) + }) + describe('remote listeners', function () { var w = null diff --git a/spec/fixtures/pages/ping-pong.html b/spec/fixtures/pages/ping-pong.html new file mode 100644 index 000000000000..d10e7898653b --- /dev/null +++ b/spec/fixtures/pages/ping-pong.html @@ -0,0 +1,11 @@ + + + + + + From ba315248e0731069a982028ea58748d9039d6935 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Sat, 28 May 2016 21:13:00 +0900 Subject: [PATCH 26/38] Use ipcRenderer.sendTo to get rid of routers in main process --- lib/browser/chrome-extension.js | 35 ++---------------------- lib/browser/rpc-server.js | 8 ++++-- lib/renderer/api/ipc-renderer.js | 10 ++++++- lib/renderer/chrome-api.js | 26 +++++++++--------- lib/renderer/content-scripts-injector.js | 4 +-- 5 files changed, 33 insertions(+), 50 deletions(-) diff --git a/lib/browser/chrome-extension.js b/lib/browser/chrome-extension.js index e9abb998b806..ad8af7213c10 100644 --- a/lib/browser/chrome-extension.js +++ b/lib/browser/chrome-extension.js @@ -80,31 +80,12 @@ ipcMain.on('CHROME_RUNTIME_CONNECT', function (event, extensionId, connectInfo) event.returnValue = {webContentsId: page.webContents.id, portId: portId} event.sender.once('render-view-deleted', () => { - page.webContents.sendToAll(`CHROME_PORT_ONDISCONNECT_${portId}`) + if (page.webContents.isDestroyed()) return + page.webContents.sendToAll(`CHROME_PORT_DISCONNECT_${portId}`) }) page.webContents.sendToAll('CHROME_RUNTIME_ONCONNECT', event.sender.id, portId, extensionId, connectInfo) }) -ipcMain.on('CHROME_PORT_DISCONNECT', function (event, webContentsId, portId) { - const contents = webContents.fromId(webContentsId) - if (!contents) { - console.error(`Disconnet to unkown webContentsId ${webContentsId}`) - return - } - - contents.sendToAll(`CHROME_PORT_ONDISCONNECT_${portId}`) -}) - -ipcMain.on('CHROME_PORT_POSTMESSAGE', function (event, webContentsId, portId, message) { - const contents = webContents.fromId(webContentsId) - if (!contents) { - console.error(`Sending message to unkown webContentsId ${webContentsId}`) - return - } - - contents.sendToAll(`CHROME_PORT_ONMESSAGE_${portId}`, message) -}) - ipcMain.on('CHROME_TABS_EXECUTESCRIPT', function (event, requestId, webContentsId, extensionId, details) { const contents = webContents.fromId(webContentsId) if (!contents) { @@ -122,17 +103,7 @@ ipcMain.on('CHROME_TABS_EXECUTESCRIPT', function (event, requestId, webContentsI url = `chrome-extension://${extensionId}/${String(Math.random()).substr(2, 8)}.js` } - contents.send('CHROME_TABS_EXECUTESCRIPT', requestId, event.sender.id, extensionId, url, code) -}) - -ipcMain.on('CHROME_TABS_EXECUTESCRIPT_RESULT', (event, requestId, webContentsId, result) => { - const contents = webContents.fromId(webContentsId) - if (!contents) { - console.error(`Sending message to unkown webContentsId ${webContentsId}`) - return - } - - contents.send(`CHROME_TABS_EXECUTESCRIPT_RESULT_${requestId}`, result) + contents.send('CHROME_TABS_EXECUTESCRIPT', event.sender.id, requestId, extensionId, url, code) }) // Transfer the content scripts to renderer. diff --git a/lib/browser/rpc-server.js b/lib/browser/rpc-server.js index 8dc90e862cf9..68cfcaf297f2 100644 --- a/lib/browser/rpc-server.js +++ b/lib/browser/rpc-server.js @@ -352,12 +352,16 @@ ipcMain.on('ELECTRON_BROWSER_ASYNC_CALL_TO_GUEST_VIEW', function (event, request } }) -ipcMain.on('ELECTRON_BROWSER_SEND_TO', function (event, webContentsId, channel, ...args) { +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 } - contents.send(channel, ...args) + if (sendToAll) { + contents.sendToAll(channel, ...args) + } else { + contents.send(channel, ...args) + } }) diff --git a/lib/renderer/api/ipc-renderer.js b/lib/renderer/api/ipc-renderer.js index 24bb9fe0be91..1081475bbeac 100644 --- a/lib/renderer/api/ipc-renderer.js +++ b/lib/renderer/api/ipc-renderer.js @@ -23,7 +23,15 @@ ipcRenderer.sendTo = function (webContentsId, channel, ...args) { throw new TypeError(`First argument has to be webContentsId`) } - ipcRenderer.send('ELECTRON_BROWSER_SEND_TO', webContentsId, channel, ...args) + 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 diff --git a/lib/renderer/chrome-api.js b/lib/renderer/chrome-api.js index 40c37aafc2a3..646e5bdee187 100644 --- a/lib/renderer/chrome-api.js +++ b/lib/renderer/chrome-api.js @@ -51,10 +51,10 @@ class Port { this.onMessage = new Event() this.sender = new MessageSender(webContentsId, extensionId) - ipcRenderer.once(`CHROME_PORT_ONDISCONNECT_${portId}`, () => { + ipcRenderer.once(`CHROME_PORT_DISCONNECT_${portId}`, () => { this._onDisconnect() }) - ipcRenderer.on(`CHROME_PORT_ONMESSAGE_${portId}`, (event, message) => { + ipcRenderer.on(`CHROME_PORT_POSTMESSAGE_${portId}`, (event, message) => { const sendResponse = function () { console.error('sendResponse is not implemented') } this.onMessage.emit(message, this.sender, sendResponse) }) @@ -63,17 +63,17 @@ class Port { disconnect () { if (this.disconnected) return - ipcRenderer.send('CHROME_PORT_DISCONNECT', this.webContentsId, this.portId) + ipcRenderer.sendToAll(this.webContentsId, `CHROME_PORT_DISCONNECT_${this.portId}`) this._onDisconnect() } postMessage (message) { - ipcRenderer.send('CHROME_PORT_POSTMESSAGE', this.webContentsId, this.portId, message) + ipcRenderer.sendToAll(this.webContentsId, `CHROME_PORT_POSTMESSAGE_${this.portId}`, message) } _onDisconnect () { this.disconnected = true - ipcRenderer.removeAllListeners(`CHROME_PORT_ONMESSAGE_${this.portId}`) + ipcRenderer.removeAllListeners(`CHROME_PORT_POSTMESSAGE_${this.portId}`) this.onDisconnect.emit() } } @@ -131,7 +131,7 @@ exports.injectTo = function (extensionId, context) { chrome.extension = { getURL: chrome.runtime.getURL, - connect: chrome.runtime.connect + connect: chrome.runtime.connect, onConnect: chrome.runtime.onConnect } @@ -143,12 +143,12 @@ exports.injectTo = function (extensionId, context) { } chrome.pageAction = { - show: () {}, - hide: () {}, - setTitle: () {}, - getTitle: () {}, - setIcon: () {}, - setPopup: () {}, - getPopup: () {} + show () {}, + hide () {}, + setTitle () {}, + getTitle () {}, + setIcon () {}, + setPopup () {}, + getPopup () {} } } diff --git a/lib/renderer/content-scripts-injector.js b/lib/renderer/content-scripts-injector.js index 6e41a8d4f116..80115a6c5394 100644 --- a/lib/renderer/content-scripts-injector.js +++ b/lib/renderer/content-scripts-injector.js @@ -43,9 +43,9 @@ const injectContentScript = function (extensionId, script) { } // Handle the request of chrome.tabs.executeJavaScript. -ipcRenderer.on('CHROME_TABS_EXECUTESCRIPT', function (event, requestId, senderId, extensionId, url, code) { +ipcRenderer.on('CHROME_TABS_EXECUTESCRIPT', function (event, senderWebContentsId, requestId, extensionId, url, code) { const result = runContentScript.call(window, extensionId, url, code) - ipcRenderer.send('CHROME_TABS_EXECUTESCRIPT_RESULT', requestId, senderId, result) + ipcRenderer.sendToAll(senderWebContentsId, `CHROME_TABS_EXECUTESCRIPT_RESULT_${requestId}`, result) }) // Read the renderer process preferences. From 62fb4f98202261dfa0ea9da8406f718d8c23871c Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Sat, 28 May 2016 21:23:43 +0900 Subject: [PATCH 27/38] Implement chrome.runtime.sendMessage --- lib/browser/chrome-extension.js | 10 ++++++++++ lib/renderer/chrome-api.js | 34 +++++++++++++++++++++++++++++++-- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/lib/browser/chrome-extension.js b/lib/browser/chrome-extension.js index ad8af7213c10..57c40ddcda93 100644 --- a/lib/browser/chrome-extension.js +++ b/lib/browser/chrome-extension.js @@ -86,6 +86,16 @@ ipcMain.on('CHROME_RUNTIME_CONNECT', function (event, extensionId, connectInfo) page.webContents.sendToAll('CHROME_RUNTIME_ONCONNECT', event.sender.id, portId, extensionId, connectInfo) }) +ipcMain.on('CHROME_RUNTIME_SENDMESSAGE', function (event, extensionId, message) { + const page = backgroundPages[extensionId] + if (!page) { + console.error(`Connect to unkown extension ${extensionId}`) + return + } + + page.webContents.sendToAll('CHROME_RUNTIME_ONMESSAGE', message) +}) + ipcMain.on('CHROME_TABS_EXECUTESCRIPT', function (event, requestId, webContentsId, extensionId, details) { const contents = webContents.fromId(webContentsId) if (!contents) { diff --git a/lib/renderer/chrome-api.js b/lib/renderer/chrome-api.js index 646e5bdee187..1e9f5495c455 100644 --- a/lib/renderer/chrome-api.js +++ b/lib/renderer/chrome-api.js @@ -88,6 +88,16 @@ class OnConnect extends Event { } } +class OnMessage extends Event { + constructor () { + super() + + ipcRenderer.on('CHROME_RUNTIME_ONMESSAGE', (event, message) => { + this.emit(message) + }) + } +} + // Inject chrome API to the |context| exports.injectTo = function (extensionId, context) { const chrome = context.chrome = context.chrome || {} @@ -116,7 +126,25 @@ exports.injectTo = function (extensionId, context) { const {webContentsId, portId} = ipcRenderer.sendSync('CHROME_RUNTIME_CONNECT', targetExtensionId, connectInfo) return new Port(webContentsId, portId, extensionId, connectInfo.name) - } + }, + + sendMessage (...args) { + // Parse the optional args. + let targetExtensionId = extensionId + let message, options, responseCallback + if (args.length === 1) { + message = args[0] + } else if (args.length === 2) { + [targetExtensionId, message] = args + } else { + console.error('responseCallback is not supported') + [targetExtensionId, message, options, responseCallback] = args + } + + ipcRenderer.send('CHROME_RUNTIME_SENDMESSAGE', targetExtensionId, message) + }, + + onMessage: new OnMessage() } chrome.tabs = { @@ -132,7 +160,9 @@ exports.injectTo = function (extensionId, context) { chrome.extension = { getURL: chrome.runtime.getURL, connect: chrome.runtime.connect, - onConnect: chrome.runtime.onConnect + onConnect: chrome.runtime.onConnect, + sendMessage: chrome.runtime.sendMessage, + onMessage: chrome.runtime.onMessage } chrome.storage = { From 5eb9ed17298ed97a93067324a828de5f24a3650f Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Sat, 28 May 2016 21:35:07 +0900 Subject: [PATCH 28/38] Implement chrome.tabs.sendMessage --- lib/browser/chrome-extension.js | 4 ++-- lib/renderer/chrome-api.js | 39 ++++++++++++++------------------- 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/lib/browser/chrome-extension.js b/lib/browser/chrome-extension.js index 57c40ddcda93..ad78e18d998b 100644 --- a/lib/browser/chrome-extension.js +++ b/lib/browser/chrome-extension.js @@ -83,7 +83,7 @@ ipcMain.on('CHROME_RUNTIME_CONNECT', function (event, extensionId, connectInfo) if (page.webContents.isDestroyed()) return page.webContents.sendToAll(`CHROME_PORT_DISCONNECT_${portId}`) }) - page.webContents.sendToAll('CHROME_RUNTIME_ONCONNECT', event.sender.id, portId, extensionId, connectInfo) + page.webContents.sendToAll(`CHROME_RUNTIME_ONCONNECT_${extensionId}`, event.sender.id, portId, connectInfo) }) ipcMain.on('CHROME_RUNTIME_SENDMESSAGE', function (event, extensionId, message) { @@ -93,7 +93,7 @@ ipcMain.on('CHROME_RUNTIME_SENDMESSAGE', function (event, extensionId, message) return } - page.webContents.sendToAll('CHROME_RUNTIME_ONMESSAGE', message) + page.webContents.sendToAll(`CHROME_RUNTIME_ONMESSAGE_${extensionId}`, message) }) ipcMain.on('CHROME_TABS_EXECUTESCRIPT', function (event, requestId, webContentsId, extensionId, details) { diff --git a/lib/renderer/chrome-api.js b/lib/renderer/chrome-api.js index 1e9f5495c455..fc3ea3072f8d 100644 --- a/lib/renderer/chrome-api.js +++ b/lib/renderer/chrome-api.js @@ -78,30 +78,18 @@ class Port { } } -class OnConnect extends Event { - constructor () { - super() - - ipcRenderer.on('CHROME_RUNTIME_ONCONNECT', (event, webContentsId, portId, extensionId, connectInfo) => { - this.emit(new Port(webContentsId, portId, extensionId, connectInfo.name)) - }) - } -} - -class OnMessage extends Event { - constructor () { - super() - - ipcRenderer.on('CHROME_RUNTIME_ONMESSAGE', (event, message) => { - this.emit(message) - }) - } -} - // Inject chrome API to the |context| exports.injectTo = function (extensionId, context) { const chrome = context.chrome = context.chrome || {} + ipcRenderer.on(`CHROME_RUNTIME_ONCONNECT_${extensionId}`, (event, webContentsId, portId, connectInfo) => { + chrome.runtime.onConnect.emit(new Port(webContentsId, portId, extensionId, connectInfo.name)) + }) + + ipcRenderer.on(`CHROME_RUNTIME_ONMESSAGE_${extensionId}`, (event, message) => { + chrome.runtime.onMessage.emit(message) + }) + chrome.runtime = { getURL: function (path) { return url.format({ @@ -112,7 +100,7 @@ exports.injectTo = function (extensionId, context) { }) }, - onConnect: new OnConnect(), + onConnect: new Event(), connect (...args) { // Parse the optional args. @@ -144,7 +132,7 @@ exports.injectTo = function (extensionId, context) { ipcRenderer.send('CHROME_RUNTIME_SENDMESSAGE', targetExtensionId, message) }, - onMessage: new OnMessage() + onMessage: new Event() } chrome.tabs = { @@ -154,6 +142,13 @@ exports.injectTo = function (extensionId, context) { 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.sendToAll(tabId, `CHROME_RUNTIME_ONMESSAGE_${extensionId}`, message) } } From 9ed4730cba8fcaa2a0439868a8fb8b98a665c983 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Sat, 28 May 2016 21:45:23 +0900 Subject: [PATCH 29/38] Pass sender for chrome.runtime.onMessage --- lib/browser/chrome-extension.js | 2 +- lib/renderer/chrome-api.js | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/browser/chrome-extension.js b/lib/browser/chrome-extension.js index ad78e18d998b..9cdebdba7d6c 100644 --- a/lib/browser/chrome-extension.js +++ b/lib/browser/chrome-extension.js @@ -93,7 +93,7 @@ ipcMain.on('CHROME_RUNTIME_SENDMESSAGE', function (event, extensionId, message) return } - page.webContents.sendToAll(`CHROME_RUNTIME_ONMESSAGE_${extensionId}`, message) + page.webContents.sendToAll(`CHROME_RUNTIME_ONMESSAGE_${extensionId}`, event.sender.id, message) }) ipcMain.on('CHROME_TABS_EXECUTESCRIPT', function (event, requestId, webContentsId, extensionId, details) { diff --git a/lib/renderer/chrome-api.js b/lib/renderer/chrome-api.js index fc3ea3072f8d..19bf69f9a0df 100644 --- a/lib/renderer/chrome-api.js +++ b/lib/renderer/chrome-api.js @@ -86,8 +86,8 @@ exports.injectTo = function (extensionId, context) { chrome.runtime.onConnect.emit(new Port(webContentsId, portId, extensionId, connectInfo.name)) }) - ipcRenderer.on(`CHROME_RUNTIME_ONMESSAGE_${extensionId}`, (event, message) => { - chrome.runtime.onMessage.emit(message) + ipcRenderer.on(`CHROME_RUNTIME_ONMESSAGE_${extensionId}`, (event, tabId, message) => { + chrome.runtime.onMessage.emit(message, new MessageSender(tabId, extensionId)) }) chrome.runtime = { @@ -148,8 +148,10 @@ exports.injectTo = function (extensionId, context) { if (responseCallback) { console.error('responseCallback is not supported') } - ipcRenderer.sendToAll(tabId, `CHROME_RUNTIME_ONMESSAGE_${extensionId}`, message) - } + ipcRenderer.send('CHROME_RUNTIME_SENDMESSAGE', extensionId, message) + }, + + onUpdated: new Event() } chrome.extension = { From 30dca2b4e16445f2f0c0c168d3920c882386f510 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Sat, 28 May 2016 21:46:53 +0900 Subject: [PATCH 30/38] Pages in chrome extension should not have node integration --- lib/renderer/init.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/renderer/init.js b/lib/renderer/init.js index b0baf438c6c9..409d7d719f37 100644 --- a/lib/renderer/init.js +++ b/lib/renderer/init.js @@ -64,7 +64,7 @@ if (window.location.protocol === 'chrome-devtools:') { } else if (window.location.protocol === 'chrome-extension:') { // Add implementations of chrome API. require('./chrome-api').injectTo(window.location.hostname, window) - nodeIntegration = 'true' + nodeIntegration = 'false' } else { // Override default web functions. require('./override') From f4fe60d126f9b86bca7dd7bfe99f49c81359cda5 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Sat, 28 May 2016 22:36:22 +0900 Subject: [PATCH 31/38] Set default mimeType for BufferJob --- atom/browser/net/url_request_buffer_job.cc | 23 ++++++++++++++++++++++ lib/browser/chrome-extension.js | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/atom/browser/net/url_request_buffer_job.cc b/atom/browser/net/url_request_buffer_job.cc index c0c8e1584e69..c713099c76a1 100644 --- a/atom/browser/net/url_request_buffer_job.cc +++ b/atom/browser/net/url_request_buffer_job.cc @@ -8,10 +8,24 @@ #include "atom/common/atom_constants.h" #include "base/strings/string_number_conversions.h" +#include "base/strings/utf_string_conversions.h" +#include "net/base/mime_util.h" #include "net/base/net_errors.h" namespace atom { +namespace { + +std::string GetExtFromURL(const GURL& url) { + std::string spec = url.spec(); + size_t index = spec.find_last_of('.'); + if (index == std::string::npos || index == spec.size()) + return std::string(); + return spec.substr(index + 1, spec.size() - index - 1); +} + +} // namespace + URLRequestBufferJob::URLRequestBufferJob( net::URLRequest* request, net::NetworkDelegate* network_delegate) : JsAsker(request, network_delegate), @@ -30,6 +44,15 @@ void URLRequestBufferJob::StartAsync(std::unique_ptr options) { options->GetAsBinary(&binary); } + if (mime_type_.empty()) { + std::string ext = GetExtFromURL(request()->url()); +#if defined(OS_WIN) + net::GetWellKnownMimeTypeFromExtension(base::UTF8ToUTF16(ext), &mime_type_); +#else + net::GetWellKnownMimeTypeFromExtension(ext, &mime_type_); +#endif + } + if (!binary) { NotifyStartError(net::URLRequestStatus( net::URLRequestStatus::FAILED, net::ERR_NOT_IMPLEMENTED)); diff --git a/lib/browser/chrome-extension.js b/lib/browser/chrome-extension.js index 9cdebdba7d6c..609fdd6442bf 100644 --- a/lib/browser/chrome-extension.js +++ b/lib/browser/chrome-extension.js @@ -227,7 +227,7 @@ app.once('ready', function () { if (err) { return callback(-6) // FILE_NOT_FOUND } else { - return callback({mimeType: 'text/html', data: content}) + return callback(content) } }) } From f693b042b58a9c723261b5ee30c78706ef6e7e47 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Sat, 28 May 2016 22:46:15 +0900 Subject: [PATCH 32/38] Fix js lint warnings --- lib/renderer/api/ipc-renderer.js | 4 ++-- lib/renderer/chrome-api.js | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/renderer/api/ipc-renderer.js b/lib/renderer/api/ipc-renderer.js index 1081475bbeac..66c40d311da2 100644 --- a/lib/renderer/api/ipc-renderer.js +++ b/lib/renderer/api/ipc-renderer.js @@ -20,7 +20,7 @@ ipcRenderer.sendToHost = function (...args) { ipcRenderer.sendTo = function (webContentsId, channel, ...args) { if (typeof webContentsId !== 'number') { - throw new TypeError(`First argument has to be webContentsId`) + throw new TypeError('First argument has to be webContentsId') } ipcRenderer.send('ELECTRON_BROWSER_SEND_TO', false, webContentsId, channel, ...args) @@ -28,7 +28,7 @@ ipcRenderer.sendTo = function (webContentsId, channel, ...args) { ipcRenderer.sendToAll = function (webContentsId, channel, ...args) { if (typeof webContentsId !== 'number') { - throw new TypeError(`First argument has to be webContentsId`) + throw new TypeError('First argument has to be webContentsId') } ipcRenderer.send('ELECTRON_BROWSER_SEND_TO', true, webContentsId, channel, ...args) diff --git a/lib/renderer/chrome-api.js b/lib/renderer/chrome-api.js index 19bf69f9a0df..e582f5f67876 100644 --- a/lib/renderer/chrome-api.js +++ b/lib/renderer/chrome-api.js @@ -119,14 +119,13 @@ exports.injectTo = function (extensionId, context) { sendMessage (...args) { // Parse the optional args. let targetExtensionId = extensionId - let message, options, responseCallback + let message if (args.length === 1) { message = args[0] } else if (args.length === 2) { [targetExtensionId, message] = args } else { - console.error('responseCallback is not supported') - [targetExtensionId, message, options, responseCallback] = args + console.error('options and responseCallback are not supported') } ipcRenderer.send('CHROME_RUNTIME_SENDMESSAGE', targetExtensionId, message) From dd804d7432a8d5f95888c376306f4961d1afd808 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Sun, 29 May 2016 10:34:53 +0900 Subject: [PATCH 33/38] Enable specifying custom command line switches --- atom/browser/web_contents_preferences.cc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/atom/browser/web_contents_preferences.cc b/atom/browser/web_contents_preferences.cc index ef9fc4df0b8c..a32e23de68f5 100644 --- a/atom/browser/web_contents_preferences.cc +++ b/atom/browser/web_contents_preferences.cc @@ -150,6 +150,16 @@ void WebContentsPreferences::AppendExtraCommandLineSwitches( command_line->AppendSwitch(switches::kScrollBounce); #endif + // Custom command line switches. + const base::ListValue* args; + if (web_preferences.GetList("commandLineSwitches", &args)) { + for (size_t i = 0; i < args->GetSize(); ++i) { + std::string arg; + if (args->GetString(i, &arg) && !arg.empty()) + command_line->AppendSwitch(arg); + } + } + // Enable blink features. std::string blink_features; if (web_preferences.GetString(options::kBlinkFeatures, &blink_features)) From 2431d886bff43564653fb4968829f2cc6f675d7a Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Sun, 29 May 2016 10:46:48 +0900 Subject: [PATCH 34/38] Current handle sender.tab for background pages --- lib/browser/chrome-extension.js | 24 +++++++++++--- lib/renderer/chrome-api.js | 40 +++++++++++++++--------- lib/renderer/content-scripts-injector.js | 2 +- lib/renderer/init.js | 9 ++++-- 4 files changed, 51 insertions(+), 24 deletions(-) diff --git a/lib/browser/chrome-extension.js b/lib/browser/chrome-extension.js index 609fdd6442bf..e81f4811c1e5 100644 --- a/lib/browser/chrome-extension.js +++ b/lib/browser/chrome-extension.js @@ -49,7 +49,9 @@ const startBackgroundPages = function (manifest) { }).join('') const html = new Buffer(`${scripts}`) - const contents = webContents.create({}) + const contents = webContents.create({ + commandLineSwitches: ['--background-page'] + }) backgroundPages[manifest.extensionId] = { html: html, webContents: contents } contents.loadURL(url.format({ protocol: 'chrome-extension', @@ -77,7 +79,7 @@ ipcMain.on('CHROME_RUNTIME_CONNECT', function (event, extensionId, connectInfo) } const portId = ++nextId - event.returnValue = {webContentsId: page.webContents.id, portId: portId} + event.returnValue = {tabId: page.webContents.id, portId: portId} event.sender.once('render-view-deleted', () => { if (page.webContents.isDestroyed()) return @@ -96,10 +98,22 @@ ipcMain.on('CHROME_RUNTIME_SENDMESSAGE', function (event, extensionId, message) page.webContents.sendToAll(`CHROME_RUNTIME_ONMESSAGE_${extensionId}`, event.sender.id, message) }) -ipcMain.on('CHROME_TABS_EXECUTESCRIPT', function (event, requestId, webContentsId, extensionId, details) { - const contents = webContents.fromId(webContentsId) +ipcMain.on('CHROME_TABS_SEND_MESSAGE', function (event, tabId, extensionId, isBackgroundPage, message) { + const contents = webContents.fromId(tabId) if (!contents) { - console.error(`Sending message to unkown webContentsId ${webContentsId}`) + console.error(`Sending message to unkown 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 unkown tab ${tabId}`) return } diff --git a/lib/renderer/chrome-api.js b/lib/renderer/chrome-api.js index e582f5f67876..4633adfcdffa 100644 --- a/lib/renderer/chrome-api.js +++ b/lib/renderer/chrome-api.js @@ -27,29 +27,29 @@ class Event { } class Tab { - constructor (webContentsId) { - this.id = webContentsId + constructor (tabId) { + this.id = tabId } } class MessageSender { - constructor (webContentsId, extensionId) { - this.tab = new Tab(webContentsId) + constructor (tabId, extensionId) { + this.tab = tabId ? new Tab(tabId) : null this.id = extensionId this.url = `chrome-extension://${extensionId}` } } class Port { - constructor (webContentsId, portId, extensionId, name) { - this.webContentsId = webContentsId + 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(webContentsId, extensionId) + this.sender = new MessageSender(tabId, extensionId) ipcRenderer.once(`CHROME_PORT_DISCONNECT_${portId}`, () => { this._onDisconnect() @@ -63,12 +63,12 @@ class Port { disconnect () { if (this.disconnected) return - ipcRenderer.sendToAll(this.webContentsId, `CHROME_PORT_DISCONNECT_${this.portId}`) + ipcRenderer.sendToAll(this.tabId, `CHROME_PORT_DISCONNECT_${this.portId}`) this._onDisconnect() } postMessage (message) { - ipcRenderer.sendToAll(this.webContentsId, `CHROME_PORT_POSTMESSAGE_${this.portId}`, message) + ipcRenderer.sendToAll(this.tabId, `CHROME_PORT_POSTMESSAGE_${this.portId}`, message) } _onDisconnect () { @@ -79,11 +79,11 @@ class Port { } // Inject chrome API to the |context| -exports.injectTo = function (extensionId, context) { +exports.injectTo = function (extensionId, isBackgroundPage, context) { const chrome = context.chrome = context.chrome || {} - ipcRenderer.on(`CHROME_RUNTIME_ONCONNECT_${extensionId}`, (event, webContentsId, portId, connectInfo) => { - chrome.runtime.onConnect.emit(new Port(webContentsId, portId, extensionId, connectInfo.name)) + 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) => { @@ -103,6 +103,11 @@ exports.injectTo = function (extensionId, context) { onConnect: new Event(), 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: ''} @@ -112,11 +117,16 @@ exports.injectTo = function (extensionId, context) { [targetExtensionId, connectInfo] = args } - const {webContentsId, portId} = ipcRenderer.sendSync('CHROME_RUNTIME_CONNECT', targetExtensionId, connectInfo) - return new Port(webContentsId, portId, extensionId, connectInfo.name) + 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 @@ -147,7 +157,7 @@ exports.injectTo = function (extensionId, context) { if (responseCallback) { console.error('responseCallback is not supported') } - ipcRenderer.send('CHROME_RUNTIME_SENDMESSAGE', extensionId, message) + ipcRenderer.send(`CHROME_TABS_SEND_MESSAGE`, tabId, extensionId, isBackgroundPage, message) }, onUpdated: new Event() diff --git a/lib/renderer/content-scripts-injector.js b/lib/renderer/content-scripts-injector.js index 80115a6c5394..e4a801110f62 100644 --- a/lib/renderer/content-scripts-injector.js +++ b/lib/renderer/content-scripts-injector.js @@ -13,7 +13,7 @@ const matchesPattern = function (pattern) { // Run the code with chrome API integrated. const runContentScript = function (extensionId, url, code) { const context = {} - require('./chrome-api').injectTo(extensionId, context) + require('./chrome-api').injectTo(extensionId, false, context) const wrapper = `(function (chrome) {\n ${code}\n })` const compiledWrapper = runInThisContext(wrapper, { filename: url, diff --git a/lib/renderer/init.js b/lib/renderer/init.js index 409d7d719f37..f0875cae12c2 100644 --- a/lib/renderer/init.js +++ b/lib/renderer/init.js @@ -41,8 +41,9 @@ electron.ipcRenderer.on('ELECTRON_INTERNAL_RENDERER_ASYNC_WEB_FRAME_METHOD', (ev }) // Process command line arguments. -var nodeIntegration = 'false' -var preloadScript = null +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. @@ -54,6 +55,8 @@ for (let arg of process.argv) { 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 } } @@ -63,7 +66,7 @@ if (window.location.protocol === 'chrome-devtools:') { nodeIntegration = 'true' } else if (window.location.protocol === 'chrome-extension:') { // Add implementations of chrome API. - require('./chrome-api').injectTo(window.location.hostname, window) + require('./chrome-api').injectTo(window.location.hostname, isBackgroundPage, window) nodeIntegration = 'false' } else { // Override default web functions. From ec1944c1464ebe67844423efcf7ab9404356aa67 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Sun, 29 May 2016 11:50:14 +0900 Subject: [PATCH 35/38] Implement chrome.tab.onCreated/onRemoved APIs --- lib/browser/chrome-extension.js | 15 +++++++++++++++ lib/renderer/chrome-api.js | 18 ++++++++++++++---- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/lib/browser/chrome-extension.js b/lib/browser/chrome-extension.js index e81f4811c1e5..233f7f1ce8b5 100644 --- a/lib/browser/chrome-extension.js +++ b/lib/browser/chrome-extension.js @@ -68,6 +68,20 @@ const removeBackgroundPages = function (manifest) { delete backgroundPages[manifest.extensionId] } +// Dispatch tabs events. +const hookWindowForTabEvents = function (win) { + const tabId = win.webContents.id + for (const page of objectValues(backgroundPages)) { + page.webContents.sendToAll('CHROME_TABS_ONCREATED', tabId) + } + + win.once('closed', () => { + for (const page of objectValues(backgroundPages)) { + page.webContents.sendToAll('CHROME_TABS_ONREMOVED', tabId) + } + }) +} + // Handle the chrome.* API messages. let nextId = 0 @@ -290,6 +304,7 @@ app.once('ready', function () { const init = BrowserWindow.prototype._init BrowserWindow.prototype._init = function () { init.call(this) + hookWindowForTabEvents(this) this.webContents.on('devtools-opened', () => { loadDevToolsExtensions(this, objectValues(manifestMap)) }) diff --git a/lib/renderer/chrome-api.js b/lib/renderer/chrome-api.js index 4633adfcdffa..a7a216da2aa1 100644 --- a/lib/renderer/chrome-api.js +++ b/lib/renderer/chrome-api.js @@ -90,6 +90,14 @@ exports.injectTo = function (extensionId, isBackgroundPage, context) { 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 = { getURL: function (path) { return url.format({ @@ -100,8 +108,6 @@ exports.injectTo = function (extensionId, isBackgroundPage, context) { }) }, - onConnect: new Event(), - connect (...args) { if (isBackgroundPage) { console.error('chrome.runtime.connect is not supported in background page') @@ -141,7 +147,9 @@ exports.injectTo = function (extensionId, isBackgroundPage, context) { ipcRenderer.send('CHROME_RUNTIME_SENDMESSAGE', targetExtensionId, message) }, - onMessage: new Event() + onConnect: new Event(), + onMessage: new Event(), + onInstalled: new Event() } chrome.tabs = { @@ -160,7 +168,9 @@ exports.injectTo = function (extensionId, isBackgroundPage, context) { ipcRenderer.send(`CHROME_TABS_SEND_MESSAGE`, tabId, extensionId, isBackgroundPage, message) }, - onUpdated: new Event() + onUpdated: new Event(), + onCreated: new Event(), + onRemoved: new Event() } chrome.extension = { From de001a9bbfa85e2e00cd0d460017cf5ccf6f79b2 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Sun, 29 May 2016 11:57:20 +0900 Subject: [PATCH 36/38] Use meaningful name for extensionId --- lib/browser/chrome-extension.js | 8 +++++--- lib/renderer/chrome-api.js | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/browser/chrome-extension.js b/lib/browser/chrome-extension.js index 233f7f1ce8b5..775a31d2d03e 100644 --- a/lib/browser/chrome-extension.js +++ b/lib/browser/chrome-extension.js @@ -14,13 +14,16 @@ const objectValues = function (object) { const manifestMap = {} // extensionId => manifest const manifestNameMap = {} // name => manifest -let nextExtensionId = 0 +const generateExtensionIdFromName = function (name) { + return name.replace(/[\W_]+/g, '-').toLowerCase() +} // Create or get manifest object from |srcDirectory|. const getManifestFromPath = function (srcDirectory) { const manifest = JSON.parse(fs.readFileSync(path.join(srcDirectory, 'manifest.json'))) if (!manifestNameMap[manifest.name]) { - const extensionId = `extension-${++nextExtensionId}` + const extensionId = generateExtensionIdFromName(manifest.name) + console.log(extensionId) manifestMap[extensionId] = manifestNameMap[manifest.name] = manifest Object.assign(manifest, { srcDirectory: srcDirectory, @@ -238,7 +241,6 @@ app.once('ready', function () { const chromeExtensionHandler = function (request, callback) { const parsed = url.parse(request.url) if (!parsed.hostname || !parsed.path) return callback() - if (!/extension-\d+/.test(parsed.hostname)) return callback() const manifest = manifestMap[parsed.hostname] if (!manifest) return callback() diff --git a/lib/renderer/chrome-api.js b/lib/renderer/chrome-api.js index a7a216da2aa1..61fce14c5ba9 100644 --- a/lib/renderer/chrome-api.js +++ b/lib/renderer/chrome-api.js @@ -165,7 +165,7 @@ exports.injectTo = function (extensionId, isBackgroundPage, context) { if (responseCallback) { console.error('responseCallback is not supported') } - ipcRenderer.send(`CHROME_TABS_SEND_MESSAGE`, tabId, extensionId, isBackgroundPage, message) + ipcRenderer.send('CHROME_TABS_SEND_MESSAGE', tabId, extensionId, isBackgroundPage, message) }, onUpdated: new Event(), From 5f3fdbe6354a8efc46b2d74083534ff589031c41 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Sun, 29 May 2016 12:10:32 +0900 Subject: [PATCH 37/38] Simplify the implementation of sendToAll --- atom/browser/api/atom_api_web_contents.cc | 11 +++-------- atom/browser/api/atom_api_web_contents.h | 5 ++--- lib/browser/api/web-contents.js | 13 ++++--------- 3 files changed, 9 insertions(+), 20 deletions(-) diff --git a/atom/browser/api/atom_api_web_contents.cc b/atom/browser/api/atom_api_web_contents.cc index 9f447f8e34e4..736fb7ca0fb1 100644 --- a/atom/browser/api/atom_api_web_contents.cc +++ b/atom/browser/api/atom_api_web_contents.cc @@ -1088,14 +1088,10 @@ void WebContents::TabTraverse(bool reverse) { web_contents()->FocusThroughTabTraversal(reverse); } -bool WebContents::SendIPCMessage(const base::string16& channel, +bool WebContents::SendIPCMessage(bool all_frames, + const base::string16& channel, const base::ListValue& args) { - return Send(new AtomViewMsg_Message(routing_id(), false, channel, args)); -} - -bool WebContents::SendIPCMessageToAll(const base::string16& channel, - const base::ListValue& args) { - return Send(new AtomViewMsg_Message(routing_id(), true, channel, args)); + return Send(new AtomViewMsg_Message(routing_id(), all_frames, channel, args)); } void WebContents::SendInputEvent(v8::Isolate* isolate, @@ -1263,7 +1259,6 @@ void WebContents::BuildPrototype(v8::Isolate* isolate, .SetMethod("focus", &WebContents::Focus) .SetMethod("tabTraverse", &WebContents::TabTraverse) .SetMethod("_send", &WebContents::SendIPCMessage) - .SetMethod("_sendToAll", &WebContents::SendIPCMessageToAll) .SetMethod("sendInputEvent", &WebContents::SendInputEvent) .SetMethod("beginFrameSubscription", &WebContents::BeginFrameSubscription) diff --git a/atom/browser/api/atom_api_web_contents.h b/atom/browser/api/atom_api_web_contents.h index 7f90c33ea8f0..e03ab653a8ce 100644 --- a/atom/browser/api/atom_api_web_contents.h +++ b/atom/browser/api/atom_api_web_contents.h @@ -122,10 +122,9 @@ class WebContents : public mate::TrackableObject, void TabTraverse(bool reverse); // Send messages to browser. - bool SendIPCMessage(const base::string16& channel, + bool SendIPCMessage(bool all_frames, + const base::string16& channel, const base::ListValue& args); - bool SendIPCMessageToAll(const base::string16& channel, - const base::ListValue& args); // Send WebInputEvent to the page. void SendInputEvent(v8::Isolate* isolate, v8::Local input_event); diff --git a/lib/browser/api/web-contents.js b/lib/browser/api/web-contents.js index 1b601f095dbb..0f79a185ffc9 100644 --- a/lib/browser/api/web-contents.js +++ b/lib/browser/api/web-contents.js @@ -71,20 +71,15 @@ let wrapWebContents = function (webContents) { webContents.setMaxListeners(0) // WebContents::send(channel, args..) - webContents.send = function (channel, ...args) { - if (channel == null) { - throw new Error('Missing required channel argument') - } - return this._send(channel, args) - } - // WebContents::sendToAll(channel, args..) - webContents.sendToAll = function (channel, ...args) { + const sendWrapper = (allFrames, channel, ...args) => { if (channel == null) { throw new Error('Missing required channel argument') } - return this._sendToAll(channel, args) + return webContents._send(allFrames, channel, args) } + webContents.send = sendWrapper.bind(null, false) + webContents.sendToAll = sendWrapper.bind(null, true) // The navigation controller. controller = new NavigationController(webContents) From d1e56f416ce75059d2bb80d3ea7138d32d091c86 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Sun, 29 May 2016 15:19:41 +0900 Subject: [PATCH 38/38] Update the docs on using DevTools extension --- docs/tutorial/devtools-extension.md | 84 +++++++++++++---------------- 1 file changed, 37 insertions(+), 47 deletions(-) diff --git a/docs/tutorial/devtools-extension.md b/docs/tutorial/devtools-extension.md index 1791c0459207..f004b1f31127 100644 --- a/docs/tutorial/devtools-extension.md +++ b/docs/tutorial/devtools-extension.md @@ -1,62 +1,52 @@ # DevTools Extension -To make debugging easier, Electron has basic support for the -[Chrome DevTools Extension][devtools-extension]. +Electron supports the [Chrome DevTools Extension][devtools-extension], which can +be used to extend the ability of devtools for debugging popular web frameworks. -For most DevTools extensions you can simply download the source code and use -the `BrowserWindow.addDevToolsExtension` API to load them. The loaded extensions -will be remembered so you don't need to call the API every time when creating -a window. +## How to load a DevTools Extension -** NOTE: React DevTools does not work, follow the issue here https://github.com/electron/electron/issues/915 ** +To load an extension in Electron, you need to download it in Chrome browser, +locate its filesystem path, and then load it by calling the +`BrowserWindow.addDevToolsExtension(extension)` API. -For example, to use the [React DevTools Extension](https://github.com/facebook/react-devtools) -, first you need to download its source code: +Using the [React Developer Tools][react-devtools] as example: -```bash -$ cd /some-directory -$ git clone --recursive https://github.com/facebook/react-devtools.git -``` +1. Install it in Chrome browser. +1. Navigate to `chrome://extensions`, and find its extension ID, which is a hash + string like `fmkadmapgofadopljbjfkapdkoienihi`. +1. Find out filesystem location used by Chrome for storing extensions: + * on Windows it is `%LOCALAPPDATA%\Google\Chrome\User Data\Default\Extensions`; + * on Linux it is `~/.config/google-chrome/Default/Extensions/`; + * on OS X it is `~/Library/Application Support/Google/Chrome/Default/Extensions`. +1. Pass the location of the extension to `BrowserWindow.addDevToolsExtension` + API, for the React Developer Tools, it is something like: + `~/Library/Application Support/Google/Chrome/Default/Extensions/fmkadmapgofadopljbjfkapdkoienihi/0.14.10_0` -Follow the instructions in [`react-devtools/shells/chrome/Readme.md`](https://github.com/facebook/react-devtools/blob/master/shells/chrome/Readme.md) to build the extension. +The name of the extension is returned by `BrowserWindow.addDevToolsExtension`, +and you can pass the name of the extension to the `BrowserWindow.removeDevToolsExtension` +API to unload it. -Then you can load the extension in Electron by opening DevTools in any window, -and running the following code in the DevTools console: +## Supported DevTools Extensions -```javascript -const BrowserWindow = require('electron').remote.BrowserWindow; -BrowserWindow.addDevToolsExtension('/some-directory/react-devtools/shells/chrome'); -``` +Electron only supports a limited set of `chrome.*` APIs, so some extensions +using unsupported `chrome.*` APIs for chrome extension features may not work. +Following Devtools Extensions are tested and guaranteed to work in Electron: -To unload the extension, you can call the `BrowserWindow.removeDevToolsExtension` -API with its name and it will not load the next time you open the DevTools: +* [Ember Inspector](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi) +* [React Developer Tools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi) +* [Backbone Debugger](https://chrome.google.com/webstore/detail/backbone-debugger/bhljhndlimiafopmmhjlgfpnnchjjbhd) +* [jQuery Debugger](https://chrome.google.com/webstore/detail/jquery-debugger/dbhhnnnpaeobfddmlalhnehgclcmjimi) +* [AngularJS Batarang](https://chrome.google.com/webstore/detail/angularjs-batarang/ighdmehidhipcmcojjgiloacoafjmpfk) +* [Vue.js devtools](https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd) -```javascript -BrowserWindow.removeDevToolsExtension('React Developer Tools'); -``` +### What should I do if a DevTools Extension is not working? -## Format of DevTools Extension +Fist please make sure the extension is still being maintained, some extensions +can not even work for recent versions of Chrome browser, and we are not able to +do anything for them. -Ideally all DevTools extensions written for the Chrome browser can be loaded by -Electron, but they have to be in a plain directory. For those packaged with -`crx` extensions, there is no way for Electron to load them unless you find a -way to extract them into a directory. - -## Background Pages - -Currently Electron doesn't support the background pages feature in Chrome -extensions, so some DevTools extensions that rely on this feature may -not work in Electron. - -## `chrome.*` APIs - -Some Chrome extensions may use `chrome.*` APIs for features and while there has -been some effort to implement those APIs in Electron, not all have been -implemented. - -Given that not all `chrome.*` APIs are implemented if the DevTools extension is -using APIs other than `chrome.devtools.*`, the extension is very likely not to -work. You can report failing extensions in the issue tracker so that we can add -support for those APIs. +Then file a bug at Electron's issues list, and describe which part of the +extension is not working as expected. [devtools-extension]: https://developer.chrome.com/extensions/devtools +[react-devtools]: https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi