diff --git a/BUILD.gn b/BUILD.gn index bb18a8596dee..67a0b93df758 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -137,6 +137,37 @@ npm_action("atom_browserify_isolated") { ] } +npm_action("atom_browserify_content_script") { + script = "browserify" + deps = [ + ":build_electron_definitions", + ] + + inputs = [ + "lib/content_script/init.js", + "tsconfig.electron.json", + "tsconfig.json", + ] + + outputs = [ + "$target_gen_dir/js2c/content_script_bundle.js", + ] + + args = [ + "lib/content_script/init.js", + "-t", + "aliasify", + "-p", + "[", + "tsify", + "-p", + "tsconfig.electron.json", + "]", + "-o", + rebase_path(outputs[0]), + ] +} + copy("atom_js2c_copy") { sources = [ "lib/common/asar.js", @@ -149,12 +180,14 @@ copy("atom_js2c_copy") { action("atom_js2c") { deps = [ + ":atom_browserify_content_script", ":atom_browserify_isolated", ":atom_browserify_sandbox", ":atom_js2c_copy", ] browserify_sources = [ + "$target_gen_dir/js2c/content_script_bundle.js", "$target_gen_dir/js2c/isolated_bundle.js", "$target_gen_dir/js2c/preload_bundle.js", ] diff --git a/atom/renderer/atom_render_frame_observer.cc b/atom/renderer/atom_render_frame_observer.cc index 6bddad4d085d..19f527d73be6 100644 --- a/atom/renderer/atom_render_frame_observer.cc +++ b/atom/renderer/atom_render_frame_observer.cc @@ -113,6 +113,12 @@ void AtomRenderFrameObserver::DidCreateScriptContext( CreateIsolatedWorldContext(); renderer_client_->SetupMainWorldOverrides(context, render_frame_); } + + if (world_id >= World::ISOLATED_WORLD_EXTENSIONS && + world_id <= World::ISOLATED_WORLD_EXTENSIONS_END) { + renderer_client_->SetupExtensionWorldOverrides(context, render_frame_, + world_id); + } } void AtomRenderFrameObserver::DraggableRegionsChanged() { diff --git a/atom/renderer/atom_render_frame_observer.h b/atom/renderer/atom_render_frame_observer.h index 0390e8ddf9fb..4bb547e0f5b5 100644 --- a/atom/renderer/atom_render_frame_observer.h +++ b/atom/renderer/atom_render_frame_observer.h @@ -11,6 +11,7 @@ #include "base/strings/string16.h" #include "content/public/renderer/render_frame_observer.h" #include "ipc/ipc_platform_file.h" +#include "third_party/blink/public/platform/web_isolated_world_ids.h" #include "third_party/blink/public/web/web_local_frame.h" namespace base { @@ -21,9 +22,19 @@ namespace atom { enum World { MAIN_WORLD = 0, + // Use a high number far away from 0 to not collide with any other world // IDs created internally by Chrome. - ISOLATED_WORLD = 999 + ISOLATED_WORLD = 999, + + // Numbers for isolated worlds for extensions are set in + // lib/renderer/content-script-injector.ts, and are greater than or equal to + // this number, up to ISOLATED_WORLD_EXTENSIONS_END. + ISOLATED_WORLD_EXTENSIONS = 1 << 20, + + // Last valid isolated world ID. + ISOLATED_WORLD_EXTENSIONS_END = + blink::IsolatedWorldId::kEmbedderWorldIdLimit - 1 }; // Helper class to forward the messages to the client. diff --git a/atom/renderer/atom_renderer_client.cc b/atom/renderer/atom_renderer_client.cc index e8e5b8be69d2..74a0f475cf7b 100644 --- a/atom/renderer/atom_renderer_client.cc +++ b/atom/renderer/atom_renderer_client.cc @@ -210,6 +210,27 @@ void AtomRendererClient::SetupMainWorldOverrides( &isolated_bundle_args, nullptr); } +void AtomRendererClient::SetupExtensionWorldOverrides( + v8::Handle context, + content::RenderFrame* render_frame, + int world_id) { + auto* isolate = context->GetIsolate(); + + std::vector> isolated_bundle_params = { + node::FIXED_ONE_BYTE_STRING(isolate, "nodeProcess"), + node::FIXED_ONE_BYTE_STRING(isolate, "isolatedWorld"), + node::FIXED_ONE_BYTE_STRING(isolate, "worldId")}; + + std::vector> isolated_bundle_args = { + GetEnvironment(render_frame)->process_object(), + GetContext(render_frame->GetWebFrame(), isolate)->Global(), + v8::Integer::New(isolate, world_id)}; + + node::per_process::native_module_loader.CompileAndCall( + context, "electron/js2c/content_script_bundle", &isolated_bundle_params, + &isolated_bundle_args, nullptr); +} + node::Environment* AtomRendererClient::GetEnvironment( content::RenderFrame* render_frame) const { if (injected_frames_.find(render_frame) == injected_frames_.end()) diff --git a/atom/renderer/atom_renderer_client.h b/atom/renderer/atom_renderer_client.h index e753e0bc22ae..23d426f18406 100644 --- a/atom/renderer/atom_renderer_client.h +++ b/atom/renderer/atom_renderer_client.h @@ -33,6 +33,9 @@ class AtomRendererClient : public RendererClientBase { content::RenderFrame* render_frame) override; void SetupMainWorldOverrides(v8::Handle context, content::RenderFrame* render_frame) override; + void SetupExtensionWorldOverrides(v8::Handle context, + content::RenderFrame* render_frame, + int world_id) override; private: // content::ContentRendererClient: diff --git a/atom/renderer/atom_sandboxed_renderer_client.cc b/atom/renderer/atom_sandboxed_renderer_client.cc index 814fabf93fbf..fe6655bb22ff 100644 --- a/atom/renderer/atom_sandboxed_renderer_client.cc +++ b/atom/renderer/atom_sandboxed_renderer_client.cc @@ -270,6 +270,30 @@ void AtomSandboxedRendererClient::SetupMainWorldOverrides( &isolated_bundle_args, nullptr); } +void AtomSandboxedRendererClient::SetupExtensionWorldOverrides( + v8::Handle context, + content::RenderFrame* render_frame, + int world_id) { + auto* isolate = context->GetIsolate(); + + mate::Dictionary process = mate::Dictionary::CreateEmpty(isolate); + process.SetMethod("binding", GetBinding); + + std::vector> isolated_bundle_params = { + node::FIXED_ONE_BYTE_STRING(isolate, "nodeProcess"), + node::FIXED_ONE_BYTE_STRING(isolate, "isolatedWorld"), + node::FIXED_ONE_BYTE_STRING(isolate, "worldId")}; + + std::vector> isolated_bundle_args = { + process.GetHandle(), + GetContext(render_frame->GetWebFrame(), isolate)->Global(), + v8::Integer::New(isolate, world_id)}; + + node::per_process::native_module_loader.CompileAndCall( + context, "electron/js2c/content_script_bundle", &isolated_bundle_params, + &isolated_bundle_args, nullptr); +} + void AtomSandboxedRendererClient::WillReleaseScriptContext( v8::Handle context, content::RenderFrame* render_frame) { diff --git a/atom/renderer/atom_sandboxed_renderer_client.h b/atom/renderer/atom_sandboxed_renderer_client.h index c7136e39cd06..ee6c925c368c 100644 --- a/atom/renderer/atom_sandboxed_renderer_client.h +++ b/atom/renderer/atom_sandboxed_renderer_client.h @@ -32,6 +32,9 @@ class AtomSandboxedRendererClient : public RendererClientBase { content::RenderFrame* render_frame) override; void SetupMainWorldOverrides(v8::Handle context, content::RenderFrame* render_frame) override; + void SetupExtensionWorldOverrides(v8::Handle context, + content::RenderFrame* render_frame, + int world_id) override; // content::ContentRendererClient: void RenderFrameCreated(content::RenderFrame*) override; void RenderViewCreated(content::RenderView*) override; diff --git a/atom/renderer/renderer_client_base.h b/atom/renderer/renderer_client_base.h index 3ec129a79c36..918e18870c88 100644 --- a/atom/renderer/renderer_client_base.h +++ b/atom/renderer/renderer_client_base.h @@ -34,6 +34,9 @@ class RendererClientBase : public content::ContentRendererClient { virtual void DidClearWindowObject(content::RenderFrame* render_frame); virtual void SetupMainWorldOverrides(v8::Handle context, content::RenderFrame* render_frame) = 0; + virtual void SetupExtensionWorldOverrides(v8::Handle context, + content::RenderFrame* render_frame, + int world_id) = 0; bool isolated_world() const { return isolated_world_; } diff --git a/docs/api/web-frame.md b/docs/api/web-frame.md index 345ae2ff5825..4bd41d2530e1 100644 --- a/docs/api/web-frame.md +++ b/docs/api/web-frame.md @@ -95,6 +95,12 @@ webFrame.setSpellCheckProvider('en-US', { }) ``` +### `webFrame.insertCSS(css)` + +* `css` String - CSS source code. + +Inserts `css` as a style sheet in the document. + ### `webFrame.insertText(text)` * `text` String @@ -119,7 +125,7 @@ this limitation. ### `webFrame.executeJavaScriptInIsolatedWorld(worldId, scripts[, userGesture, callback])` -* `worldId` Integer - The ID of the world to run the javascript in, `0` is the default world, `999` is the world used by Electrons `contextIsolation` feature. You can provide any integer here. +* `worldId` Integer - The ID of the world to run the javascript in, `0` is the default world, `999` is the world used by Electrons `contextIsolation` feature. Chrome extensions reserve the range of IDs in `[1 << 20, 1 << 29)`. You can provide any integer here. * `scripts` [WebSource[]](structures/web-source.md) * `userGesture` Boolean (optional) - Default is `false`. * `callback` Function (optional) - Called after script has been executed. @@ -129,27 +135,27 @@ Work like `executeJavaScript` but evaluates `scripts` in an isolated context. ### `webFrame.setIsolatedWorldContentSecurityPolicy(worldId, csp)` _(Deprecated)_ -* `worldId` Integer - The ID of the world to run the javascript in, `0` is the default world, `999` is the world used by Electrons `contextIsolation` feature. You can provide any integer here. +* `worldId` Integer - The ID of the world to run the javascript in, `0` is the default world, `999` is the world used by Electrons `contextIsolation` feature. Chrome extensions reserve the range of IDs in `[1 << 20, 1 << 29)`. You can provide any integer here. * `csp` String Set the content security policy of the isolated world. ### `webFrame.setIsolatedWorldHumanReadableName(worldId, name)` _(Deprecated)_ -* `worldId` Integer - The ID of the world to run the javascript in, `0` is the default world, `999` is the world used by Electrons `contextIsolation` feature. You can provide any integer here. +* `worldId` Integer - The ID of the world to run the javascript in, `0` is the default world, `999` is the world used by Electrons `contextIsolation` feature. Chrome extensions reserve the range of IDs in `[1 << 20, 1 << 29)`. You can provide any integer here. * `name` String Set the name of the isolated world. Useful in devtools. ### `webFrame.setIsolatedWorldSecurityOrigin(worldId, securityOrigin)` _(Deprecated)_ -* `worldId` Integer - The ID of the world to run the javascript in, `0` is the default world, `999` is the world used by Electrons `contextIsolation` feature. You can provide any integer here. +* `worldId` Integer - The ID of the world to run the javascript in, `0` is the default world, `999` is the world used by Electrons `contextIsolation` feature. Chrome extensions reserve the range of IDs in `[1 << 20, 1 << 29)`. You can provide any integer here. * `securityOrigin` String Set the security origin of the isolated world. ### `webFrame.setIsolatedWorldInfo(worldId, info)` -* `worldId` Integer - The ID of the world to run the javascript in, `0` is the default world, `999` is the world used by Electrons `contextIsolation` feature. You can provide any integer here. +* `worldId` Integer - The ID of the world to run the javascript in, `0` is the default world, `999` is the world used by Electrons `contextIsolation` feature. Chrome extensions reserve the range of IDs in `[1 << 20, 1 << 29)`. You can provide any integer here. * `info` Object * `securityOrigin` String (optional) - Security origin for the isolated world. * `csp` String (optional) - Content Security Policy for the isolated world. diff --git a/lib/content_script/init.js b/lib/content_script/init.js new file mode 100644 index 000000000000..1f94e7fe15ec --- /dev/null +++ b/lib/content_script/init.js @@ -0,0 +1,35 @@ +'use strict' + +/* global nodeProcess, isolatedWorld, worldId */ + +const { EventEmitter } = require('events') + +process.atomBinding = require('@electron/internal/common/atom-binding-setup').atomBindingSetup(nodeProcess.binding, 'renderer') + +const v8Util = process.atomBinding('v8_util') +// The `lib/renderer/ipc-renderer-internal.js` module looks for the ipc object in the +// "ipc-internal" hidden value +v8Util.setHiddenValue(global, 'ipc-internal', new EventEmitter()) +// The process object created by browserify is not an event emitter, fix it so +// the API is more compatible with non-sandboxed renderers. +for (const prop of Object.keys(EventEmitter.prototype)) { + if (process.hasOwnProperty(prop)) { + delete process[prop] + } +} +Object.setPrototypeOf(process, EventEmitter.prototype) + +const isolatedWorldArgs = v8Util.getHiddenValue(isolatedWorld, 'isolated-world-args') + +if (isolatedWorldArgs) { + const { ipcRendererInternal, guestInstanceId, isHiddenPage, openerId, usesNativeWindowOpen } = isolatedWorldArgs + const { windowSetup } = require('@electron/internal/renderer/window-setup') + windowSetup(ipcRendererInternal, guestInstanceId, openerId, isHiddenPage, usesNativeWindowOpen) +} + +const extensionId = v8Util.getHiddenValue(isolatedWorld, `extension-${worldId}`) + +if (extensionId) { + const chromeAPI = require('@electron/internal/renderer/chrome-api') + chromeAPI.injectTo(extensionId, false, window) +} diff --git a/lib/renderer/content-scripts-injector.ts b/lib/renderer/content-scripts-injector.ts index 2bd4129840ef..2a39e17ba817 100644 --- a/lib/renderer/content-scripts-injector.ts +++ b/lib/renderer/content-scripts-injector.ts @@ -1,5 +1,24 @@ import { ipcRendererInternal } from '@electron/internal/renderer/ipc-renderer-internal' -import { runInThisContext } from 'vm' +import { webFrame } from 'electron' + +const v8Util = process.atomBinding('v8_util') + +const IsolatedWorldIDs = { + /** + * Start of extension isolated world IDs, as defined in + * atom_render_frame_observer.h + */ + ISOLATED_WORLD_EXTENSIONS: 1 << 20 +} + +let isolatedWorldIds = IsolatedWorldIDs.ISOLATED_WORLD_EXTENSIONS +const extensionWorldId: {[key: string]: number | undefined} = {} + +// https://cs.chromium.org/chromium/src/extensions/renderer/script_injection.cc?type=cs&sq=package:chromium&g=0&l=52 +const getIsolatedWorldIdForInstance = () => { + // TODO(samuelmaddock): allocate and cleanup IDs + return isolatedWorldIds++ +} // Check whether pattern matches. // https://developer.chrome.com/extensions/match_patterns @@ -12,21 +31,21 @@ const matchesPattern = function (pattern: string) { // Run the code with chrome API integrated. const runContentScript = function (this: any, extensionId: string, url: string, code: string) { - const context: { chrome?: any } = {} - require('@electron/internal/renderer/chrome-api').injectTo(extensionId, false, context) - const wrapper = `((chrome) => {\n ${code}\n })` - try { - const compiledWrapper = runInThisContext(wrapper, { - filename: url, - lineOffset: 1, - displayErrors: true - }) - return compiledWrapper.call(this, context.chrome) - } catch (error) { - // TODO(samuelmaddock): Run scripts in isolated world, see chromium script_injection.cc - console.error(`Error running content script JavaScript for '${extensionId}'`) - console.error(error) - } + // Assign unique world ID to each extension + const worldId = extensionWorldId[extensionId] || + (extensionWorldId[extensionId] = getIsolatedWorldIdForInstance()) + + // store extension ID for content script to read in isolated world + v8Util.setHiddenValue(global, `extension-${worldId}`, extensionId) + + webFrame.setIsolatedWorldInfo(worldId, { + name: `${extensionId} [${worldId}]` + // TODO(samuelmaddock): read `content_security_policy` from extension manifest + // csp: manifest.content_security_policy, + }) + + const sources = [{ code, url }] + webFrame.executeJavaScriptInIsolatedWorld(worldId, sources) } const runAllContentScript = function (scripts: Array, extensionId: string) { @@ -36,28 +55,7 @@ const runAllContentScript = function (scripts: Array, ex } const runStylesheet = function (this: any, url: string, code: string) { - const wrapper = `((code) => { - function init() { - const styleElement = document.createElement('style'); - styleElement.textContent = code; - document.head.append(styleElement); - } - document.addEventListener('DOMContentLoaded', init); - })` - - try { - const compiledWrapper = runInThisContext(wrapper, { - filename: url, - lineOffset: 1, - displayErrors: true - }) - - return compiledWrapper.call(this, code) - } catch (error) { - // TODO(samuelmaddock): Insert stylesheet directly into document, see chromium script_injection.cc - console.error(`Error inserting content script stylesheet ${url}`) - console.error(error) - } + webFrame.insertCSS(code) } const runAllStylesheet = function (css: Array) {