Implement window overrides in main context
authorKevin Sawicki <kevinsawicki@gmail.com>
Thu, 12 Jan 2017 00:36:59 +0000 (16:36 -0800)
committerKevin Sawicki <kevinsawicki@gmail.com>
Mon, 16 Jan 2017 20:38:16 +0000 (12:38 -0800)
13 files changed:
atom/browser/web_contents_preferences.cc
atom/common/options_switches.cc
atom/common/options_switches.h
atom/renderer/atom_renderer_client.cc
electron.gyp
filenames.gypi
lib/browser/guest-window-manager.js
lib/isolated_renderer/init.js [new file with mode: 0644]
lib/renderer/override.js
lib/renderer/window-setup.js [new file with mode: 0644]
spec/api-browser-window-spec.js
spec/fixtures/api/isolated.html
spec/webview-spec.js

index d5bde07250e6be2e91c07edbe6b5793297e7a05f..23f834e8e39a0bb970892c0565403266eba484cd 100644 (file)
@@ -196,7 +196,7 @@ void WebContentsPreferences::AppendExtraCommandLineSwitches(
   if (window) {
     bool visible = window->IsVisible() && !window->IsMinimized();
     if (!visible)  // Default state is visible.
-      command_line->AppendSwitch("hidden-page");
+      command_line->AppendSwitch(switches::kHiddenPage);
   }
 
   // Use frame scheduling for offscreen renderers.
index f1eecdbd47c73929411576be342d11016f0fa9ab..f247025f36d8c3e5b946c5173c9c330d69910b3a 100644 (file)
@@ -102,7 +102,7 @@ const char kNodeIntegration[] = "nodeIntegration";
 // Enable context isolation of Electron APIs and preload script
 const char kContextIsolation[] = "contextIsolation";
 
-// Instancd ID of guest WebContents.
+// Instance ID of guest WebContents.
 const char kGuestInstanceID[] = "guestInstanceId";
 
 // Web runtime features.
@@ -170,6 +170,7 @@ const char kContextIsolation[] = "context-isolation";
 const char kGuestInstanceID[]  = "guest-instance-id";
 const char kOpenerID[]         = "opener-id";
 const char kScrollBounce[]     = "scroll-bounce";
+const char kHiddenPage[]       = "hidden-page";
 
 // Widevine options
 // Path to Widevine CDM binaries.
index 2742d0c8259befc637d7a1973d3eaf334179534b..e368b0a5fc1ddf50455d6b37aee38e4338e0b440 100644 (file)
@@ -91,6 +91,7 @@ extern const char kContextIsolation[];
 extern const char kGuestInstanceID[];
 extern const char kOpenerID[];
 extern const char kScrollBounce[];
+extern const char kHiddenPage[];
 
 extern const char kWidevineCdmPath[];
 extern const char kWidevineCdmVersion[];
index e785e26c15521fb4388590b5f8b7e5312fe4a5e1..7ef04732c4b8d0e3e9c807e669ae3b70c40fe195 100644 (file)
@@ -4,6 +4,8 @@
 
 #include "atom/renderer/atom_renderer_client.h"
 
+#include "atom_natives.h"  // NOLINT: This file is generated with js2c
+
 #include <string>
 #include <vector>
 
@@ -14,6 +16,7 @@
 #include "atom/common/native_mate_converters/value_converter.h"
 #include "atom/common/node_bindings.h"
 #include "atom/common/options_switches.h"
+#include "atom/renderer/api/atom_api_renderer_ipc.h"
 #include "atom/renderer/atom_render_view_observer.h"
 #include "atom/renderer/content_settings_observer.h"
 #include "atom/renderer/guest_view_container.h"
@@ -90,6 +93,39 @@ class AtomRenderFrameObserver : public content::RenderFrameObserver {
         World::ISOLATED_WORLD, &source, 1, ExtensionGroup::MAIN_GROUP);
   }
 
+  void SetupMainWorldOverrides(v8::Handle<v8::Context> context) {
+    // Setup window overrides in the main world context
+    v8::Isolate* isolate = context->GetIsolate();
+
+    // Wrap the bundle into a function that receives the binding object as
+    // an argument.
+    std::string bundle(node::isolated_bundle_native,
+        node::isolated_bundle_native + sizeof(node::isolated_bundle_native));
+    std::string wrapper = "(function (binding) {\n" + bundle + "\n})";
+    auto script = v8::Script::Compile(
+        mate::ConvertToV8(isolate, wrapper)->ToString());
+    auto func = v8::Handle<v8::Function>::Cast(
+        script->Run(context).ToLocalChecked());
+
+    auto binding = v8::Object::New(isolate);
+    api::Initialize(binding, v8::Null(isolate), context, nullptr);
+
+    // Pass in CLI flags needed to setup window
+    base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
+    mate::Dictionary dict(isolate, binding);
+    if (command_line->HasSwitch(switches::kGuestInstanceID))
+      dict.Set("guestInstanceId",
+               command_line->GetSwitchValueASCII(switches::kGuestInstanceID));
+    if (command_line->HasSwitch(switches::kOpenerID))
+      dict.Set("openerId",
+               command_line->GetSwitchValueASCII(switches::kOpenerID));
+    dict.Set("hiddenPage", command_line->HasSwitch(switches::kHiddenPage));
+
+    v8::Local<v8::Value> args[] = { binding };
+
+    ignore_result(func->Call(context, v8::Null(isolate), 1, args));
+  }
+
   bool IsMainWorld(int world_id) {
     return world_id == World::MAIN_WORLD;
   }
@@ -111,8 +147,10 @@ class AtomRenderFrameObserver : public content::RenderFrameObserver {
     if (NotifyClient(world_id))
       renderer_client_->DidCreateScriptContext(context, render_frame_);
 
-    if (renderer_client_->isolated_world() && IsMainWorld(world_id))
+    if (renderer_client_->isolated_world() && IsMainWorld(world_id)) {
       CreateIsolatedWorldContext();
+      SetupMainWorldOverrides(context);
+    }
   }
 
   void WillReleaseScriptContext(v8::Local<v8::Context> context,
index 495d0fd0878db1bf035b997935afb042dc42951e..3ca6717b987df37a1e2a6a7ffaeb912efbfefb2f 100644 (file)
       ],
       'actions': [
         {
-          'action_name': 'atom_browserify',
+          'action_name': 'atom_browserify_sandbox',
           'inputs': [
             '<@(browserify_entries)',
           ],
             '-o',
             '<@(_outputs)',
           ],
-        }
+        },
+        {
+          'action_name': 'atom_browserify_isolated_context',
+          'inputs': [
+            '<@(isolated_context_browserify_entries)',
+          ],
+          'outputs': [
+            '<(js2c_input_dir)/isolated_bundle.js',
+          ],
+          'action': [
+            'npm',
+            'run',
+            '--silent',
+            'browserify',
+            '--',
+            'lib/isolated_renderer/init.js',
+            '-o',
+            '<@(_outputs)',
+          ],
+        },
       ],
     },  # target atom_browserify
     {
             # List all input files that should trigger a rebuild with js2c
             '<@(js2c_sources)',
             '<(js2c_input_dir)/preload_bundle.js',
+            '<(js2c_input_dir)/isolated_bundle.js',
           ],
           'outputs': [
             '<(SHARED_INTERMEDIATE_DIR)/atom_natives.h',
index b7a53a484057ccac4f9e8f12b8634edd34f3bd72..6fe7612a55d8307de88f1864b59fd44bde7d39d7 100644 (file)
@@ -56,6 +56,7 @@
       'lib/renderer/init.js',
       'lib/renderer/inspector.js',
       'lib/renderer/override.js',
+      'lib/renderer/window-setup.js',
       'lib/renderer/web-view/guest-view-internal.js',
       'lib/renderer/web-view/web-view.js',
       'lib/renderer/web-view/web-view-attributes.js',
       'lib/renderer/api/ipc-renderer-setup.js',
       'lib/sandboxed_renderer/init.js',
     ],
+    'isolated_context_browserify_entries': [
+      'lib/renderer/window-setup.js',
+      'lib/isolated_renderer/init.js',
+    ],
     'js2c_sources': [
       'lib/common/asar.js',
       'lib/common/asar_init.js',
index 62abc8663c7eb12330bf01d0cdb4e74c340aa2de..e5bfa7412386739e7153a21985a4471e8010e81a 100644 (file)
@@ -2,6 +2,7 @@
 
 const {BrowserWindow, ipcMain, webContents} = require('electron')
 const {isSameOrigin} = process.atomBinding('v8_util')
+const parseFeaturesString = require('../common/parse-features-string')
 
 const hasProp = {}.hasOwnProperty
 const frameToGuest = {}
@@ -176,8 +177,68 @@ const canAccessWindow = function (sender, target) {
          isSameOrigin(sender.getURL(), target.getURL())
 }
 
-// Routed window.open messages.
-ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN', function (event, url, frameName,
+// Routed window.open messages with raw options
+ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN', (event, url, frameName, features) => {
+  if (url == null || url === '') url = 'about:blank'
+  if (frameName == null) frameName = ''
+  if (features == null) features = ''
+
+  const options = {}
+
+  const ints = ['x', 'y', 'width', 'height', 'minWidth', 'maxWidth', 'minHeight', 'maxHeight', 'zoomFactor']
+  const webPreferences = ['zoomFactor', 'nodeIntegration', 'preload']
+  const disposition = 'new-window'
+
+  // Used to store additional features
+  const additionalFeatures = []
+
+  // Parse the features
+  parseFeaturesString(features, function (key, value) {
+    if (value === undefined) {
+      additionalFeatures.push(key)
+    } else {
+      if (webPreferences.includes(key)) {
+        if (options.webPreferences == null) {
+          options.webPreferences = {}
+        }
+        options.webPreferences[key] = value
+      } else {
+        options[key] = value
+      }
+    }
+  })
+  if (options.left) {
+    if (options.x == null) {
+      options.x = options.left
+    }
+  }
+  if (options.top) {
+    if (options.y == null) {
+      options.y = options.top
+    }
+  }
+  if (options.title == null) {
+    options.title = frameName
+  }
+  if (options.width == null) {
+    options.width = 800
+  }
+  if (options.height == null) {
+    options.height = 600
+  }
+
+  for (const name of ints) {
+    if (options[name] != null) {
+      options[name] = parseInt(options[name], 10)
+    }
+  }
+
+  ipcMain.emit('ELECTRON_GUEST_WINDOW_MANAGER_INTERNAL_WINDOW_OPEN', event,
+               url, frameName, disposition, options, additionalFeatures)
+})
+
+// Routed window.open messages with fully parsed options
+ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_INTERNAL_WINDOW_OPEN', function (event, url, frameName,
                                                                   disposition, options,
                                                                   additionalFeatures, postData) {
   options = mergeBrowserWindowOptions(event.sender, options)
@@ -229,6 +290,10 @@ ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_METHOD', function (event, guest
 })
 
 ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_POSTMESSAGE', function (event, guestId, message, targetOrigin, sourceOrigin) {
+  if (targetOrigin == null) {
+    targetOrigin = '*'
+  }
+
   const guestContents = webContents.fromId(guestId)
   if (guestContents == null) return
 
diff --git a/lib/isolated_renderer/init.js b/lib/isolated_renderer/init.js
new file mode 100644 (file)
index 0000000..01e3cac
--- /dev/null
@@ -0,0 +1,22 @@
+/* global binding */
+
+'use strict'
+
+const {guestInstanceId, hiddenPage, openerId, send, sendSync} = binding
+const {parse} = JSON
+
+const ipcRenderer = {
+  send (...args) {
+    return send('ipc-message', args)
+  },
+
+  sendSync (...args) {
+    return parse(sendSync('ipc-message-sync', args))
+  },
+
+  // No-ops since events aren't received
+  on () {},
+  once () {}
+}
+
+require('../renderer/window-setup')(ipcRenderer, guestInstanceId, openerId, hiddenPage)
index a05eb898bbc0e5fddeb7cf9bfc486753e94abade..f31e9c0e8cdcc5ac0c7972f399a9839876dea780 100644 (file)
@@ -1,244 +1,8 @@
 'use strict'
 
 const {ipcRenderer} = require('electron')
-const parseFeaturesString = require('../common/parse-features-string')
 
-const {defineProperty} = Object
+const {guestInstanceId, openerId} = process
+const hiddenPage = process.argv.includes('--hidden-page')
 
-// Helper function to resolve relative url.
-const a = window.top.document.createElement('a')
-const resolveURL = function (url) {
-  a.href = url
-  return a.href
-}
-
-// Window object returned by "window.open".
-const BrowserWindowProxy = (function () {
-  BrowserWindowProxy.proxies = {}
-
-  BrowserWindowProxy.getOrCreate = function (guestId) {
-    let proxy = this.proxies[guestId]
-    if (proxy == null) {
-      proxy = new BrowserWindowProxy(guestId)
-      this.proxies[guestId] = proxy
-    }
-    return proxy
-  }
-
-  BrowserWindowProxy.remove = function (guestId) {
-    delete this.proxies[guestId]
-  }
-
-  function BrowserWindowProxy (guestId1) {
-    defineProperty(this, 'guestId', {
-      configurable: false,
-      enumerable: true,
-      writeable: false,
-      value: guestId1
-    })
-
-    this.closed = false
-    ipcRenderer.once('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_CLOSED_' + this.guestId, () => {
-      BrowserWindowProxy.remove(this.guestId)
-      this.closed = true
-    })
-  }
-
-  BrowserWindowProxy.prototype.close = function () {
-    ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_CLOSE', this.guestId)
-  }
-
-  BrowserWindowProxy.prototype.focus = function () {
-    ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_METHOD', this.guestId, 'focus')
-  }
-
-  BrowserWindowProxy.prototype.blur = function () {
-    ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_METHOD', this.guestId, 'blur')
-  }
-
-  BrowserWindowProxy.prototype.print = function () {
-    ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD', this.guestId, 'print')
-  }
-
-  defineProperty(BrowserWindowProxy.prototype, 'location', {
-    get: function () {
-      return ipcRenderer.sendSync('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD_SYNC', this.guestId, 'getURL')
-    },
-    set: function (url) {
-      url = resolveURL(url)
-      return ipcRenderer.sendSync('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD_SYNC', this.guestId, 'loadURL', url)
-    }
-  })
-
-  BrowserWindowProxy.prototype.postMessage = function (message, targetOrigin) {
-    if (targetOrigin == null) {
-      targetOrigin = '*'
-    }
-    ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_POSTMESSAGE', this.guestId, message, targetOrigin, window.location.origin)
-  }
-
-  BrowserWindowProxy.prototype['eval'] = function (...args) {
-    ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD', this.guestId, 'executeJavaScript', ...args)
-  }
-
-  return BrowserWindowProxy
-})()
-
-if (process.guestInstanceId == null) {
-  // Override default window.close.
-  window.close = function () {
-    ipcRenderer.sendSync('ELECTRON_BROWSER_WINDOW_CLOSE')
-  }
-}
-
-// Make the browser window or guest view emit "new-window" event.
-window.open = function (url, frameName, features) {
-  let guestId, j, len1, name, options, additionalFeatures
-  if (frameName == null) {
-    frameName = ''
-  }
-  if (features == null) {
-    features = ''
-  }
-  options = {}
-
-  const ints = ['x', 'y', 'width', 'height', 'minWidth', 'maxWidth', 'minHeight', 'maxHeight', 'zoomFactor']
-  const webPreferences = ['zoomFactor', 'nodeIntegration', 'preload']
-  const disposition = 'new-window'
-
-  // Used to store additional features
-  additionalFeatures = []
-
-  // Parse the features
-  parseFeaturesString(features, function (key, value) {
-    if (value === undefined) {
-      additionalFeatures.push(key)
-    } else {
-      if (webPreferences.includes(key)) {
-        if (options.webPreferences == null) {
-          options.webPreferences = {}
-        }
-        options.webPreferences[key] = value
-      } else {
-        options[key] = value
-      }
-    }
-  })
-  if (options.left) {
-    if (options.x == null) {
-      options.x = options.left
-    }
-  }
-  if (options.top) {
-    if (options.y == null) {
-      options.y = options.top
-    }
-  }
-  if (options.title == null) {
-    options.title = frameName
-  }
-  if (options.width == null) {
-    options.width = 800
-  }
-  if (options.height == null) {
-    options.height = 600
-  }
-
-  // Resolve relative urls.
-  if (url == null || url === '') {
-    url = 'about:blank'
-  } else {
-    url = resolveURL(url)
-  }
-  for (j = 0, len1 = ints.length; j < len1; j++) {
-    name = ints[j]
-    if (options[name] != null) {
-      options[name] = parseInt(options[name], 10)
-    }
-  }
-  guestId = ipcRenderer.sendSync('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN', url, frameName, disposition, options, additionalFeatures)
-  if (guestId) {
-    return BrowserWindowProxy.getOrCreate(guestId)
-  } else {
-    return null
-  }
-}
-
-window.alert = function (message, title) {
-  ipcRenderer.sendSync('ELECTRON_BROWSER_WINDOW_ALERT', message, title)
-}
-
-window.confirm = function (message, title) {
-  return ipcRenderer.sendSync('ELECTRON_BROWSER_WINDOW_CONFIRM', message, title)
-}
-
-// But we do not support prompt().
-window.prompt = function () {
-  throw new Error('prompt() is and will not be supported.')
-}
-
-if (process.openerId != null) {
-  window.opener = BrowserWindowProxy.getOrCreate(process.openerId)
-}
-
-ipcRenderer.on('ELECTRON_GUEST_WINDOW_POSTMESSAGE', function (event, sourceId, message, sourceOrigin) {
-  // Manually dispatch event instead of using postMessage because we also need to
-  // set event.source.
-  event = document.createEvent('Event')
-  event.initEvent('message', false, false)
-  event.data = message
-  event.origin = sourceOrigin
-  event.source = BrowserWindowProxy.getOrCreate(sourceId)
-  window.dispatchEvent(event)
-})
-
-// Forward history operations to browser.
-const sendHistoryOperation = function (...args) {
-  ipcRenderer.send('ELECTRON_NAVIGATION_CONTROLLER', ...args)
-}
-
-const getHistoryOperation = function (...args) {
-  return ipcRenderer.sendSync('ELECTRON_SYNC_NAVIGATION_CONTROLLER', ...args)
-}
-
-window.history.back = function () {
-  sendHistoryOperation('goBack')
-}
-
-window.history.forward = function () {
-  sendHistoryOperation('goForward')
-}
-
-window.history.go = function (offset) {
-  sendHistoryOperation('goToOffset', offset)
-}
-
-defineProperty(window.history, 'length', {
-  get: function () {
-    return getHistoryOperation('length')
-  }
-})
-
-// The initial visibilityState.
-let cachedVisibilityState = process.argv.includes('--hidden-page') ? 'hidden' : 'visible'
-
-// Subscribe to visibilityState changes.
-ipcRenderer.on('ELECTRON_RENDERER_WINDOW_VISIBILITY_CHANGE', function (event, visibilityState) {
-  if (cachedVisibilityState !== visibilityState) {
-    cachedVisibilityState = visibilityState
-    document.dispatchEvent(new Event('visibilitychange'))
-  }
-})
-
-// Make document.hidden and document.visibilityState return the correct value.
-defineProperty(document, 'hidden', {
-  get: function () {
-    return cachedVisibilityState !== 'visible'
-  }
-})
-
-defineProperty(document, 'visibilityState', {
-  get: function () {
-    return cachedVisibilityState
-  }
-})
+require('./window-setup')(ipcRenderer, guestInstanceId, openerId, hiddenPage)
diff --git a/lib/renderer/window-setup.js b/lib/renderer/window-setup.js
new file mode 100644 (file)
index 0000000..3aa451d
--- /dev/null
@@ -0,0 +1,163 @@
+// This file should have no requires since it is used by the isolated context
+// preload bundle. Instead arguments should be passed in for everything it
+// needs.
+
+'use strict'
+
+const {defineProperty} = Object
+
+// Helper function to resolve relative url.
+const a = window.top.document.createElement('a')
+const resolveURL = function (url) {
+  a.href = url
+  return a.href
+}
+
+const windowProxies = {}
+
+module.exports = (ipcRenderer, guestInstanceId, openerId, hiddenPage) => {
+  const getOrCreateProxy = (guestId) => {
+    let proxy = windowProxies[guestId]
+    if (proxy == null) {
+      proxy = new BrowserWindowProxy(guestId)
+      windowProxies[guestId] = proxy
+    }
+    return proxy
+  }
+
+  const removeProxy = (guestId) => {
+    delete windowProxies[guestId]
+  }
+
+  function BrowserWindowProxy (guestId) {
+    this.closed = false
+
+    ipcRenderer.once(`ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_CLOSED_${guestId}`, () => {
+      removeProxy(this.guestId)
+      this.closed = true
+    })
+
+    this.close = () => {
+      ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_CLOSE', guestId)
+    }
+
+    this.focus = () => {
+      ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_METHOD', guestId, 'focus')
+    }
+
+    this.blur = () => {
+      ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_METHOD', guestId, 'blur')
+    }
+
+    this.print = () => {
+      ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD', guestId, 'print')
+    }
+
+    this.postMessage = (message, targetOrigin) => {
+      ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_POSTMESSAGE', guestId, message, targetOrigin, window.location.origin)
+    }
+
+    this.eval = (...args) => {
+      ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD', guestId, 'executeJavaScript', ...args)
+    }
+  }
+
+  if (guestInstanceId == null) {
+    // Override default window.close.
+    window.close = function () {
+      ipcRenderer.sendSync('ELECTRON_BROWSER_WINDOW_CLOSE')
+    }
+  }
+
+  // Make the browser window or guest view emit "new-window" event.
+  window.open = function (url, frameName, features) {
+    if (url != null && url.length > 0) {
+      url = resolveURL(url)
+    }
+    const guestId = ipcRenderer.sendSync('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN', url, frameName, features)
+    if (guestId != null) {
+      return getOrCreateProxy(guestId)
+    } else {
+      return null
+    }
+  }
+
+  window.alert = function (message, title) {
+    ipcRenderer.sendSync('ELECTRON_BROWSER_WINDOW_ALERT', message, title)
+  }
+
+  window.confirm = function (message, title) {
+    return ipcRenderer.sendSync('ELECTRON_BROWSER_WINDOW_CONFIRM', message, title)
+  }
+
+  // But we do not support prompt().
+  window.prompt = function () {
+    throw new Error('prompt() is and will not be supported.')
+  }
+
+  if (openerId != null) {
+    window.opener = getOrCreateProxy(openerId)
+  }
+
+  ipcRenderer.on('ELECTRON_GUEST_WINDOW_POSTMESSAGE', function (event, sourceId, message, sourceOrigin) {
+    // Manually dispatch event instead of using postMessage because we also need to
+    // set event.source.
+    event = document.createEvent('Event')
+    event.initEvent('message', false, false)
+    event.data = message
+    event.origin = sourceOrigin
+    event.source = BrowserWindowProxy.getOrCreate(sourceId)
+    window.dispatchEvent(event)
+  })
+
+  // Forward history operations to browser.
+  const sendHistoryOperation = function (...args) {
+    ipcRenderer.send('ELECTRON_NAVIGATION_CONTROLLER', ...args)
+  }
+
+  const getHistoryOperation = function (...args) {
+    return ipcRenderer.sendSync('ELECTRON_SYNC_NAVIGATION_CONTROLLER', ...args)
+  }
+
+  window.history.back = function () {
+    sendHistoryOperation('goBack')
+  }
+
+  window.history.forward = function () {
+    sendHistoryOperation('goForward')
+  }
+
+  window.history.go = function (offset) {
+    sendHistoryOperation('goToOffset', offset)
+  }
+
+  defineProperty(window.history, 'length', {
+    get: function () {
+      return getHistoryOperation('length')
+    }
+  })
+
+  // The initial visibilityState.
+  let cachedVisibilityState = hiddenPage ? 'hidden' : 'visible'
+
+  // Subscribe to visibilityState changes.
+  ipcRenderer.on('ELECTRON_RENDERER_WINDOW_VISIBILITY_CHANGE', function (event, visibilityState) {
+    if (cachedVisibilityState !== visibilityState) {
+      cachedVisibilityState = visibilityState
+      document.dispatchEvent(new Event('visibilitychange'))
+    }
+  })
+
+  // Make document.hidden and document.visibilityState return the correct value.
+  defineProperty(document, 'hidden', {
+    get: function () {
+      return cachedVisibilityState !== 'visible'
+    }
+  })
+
+  defineProperty(document, 'visibilityState', {
+    get: function () {
+      return cachedVisibilityState
+    }
+  })
+}
index b777c684d1ad83d772dc4de488bb7addd8a1815a..8fd05a327f1f01a6cae677e8eacc5e4ac2a1b3b6 100644 (file)
@@ -1836,6 +1836,27 @@ describe('BrowserWindow module', function () {
   })
 
   describe('contextIsolation option', () => {
+    const expectedContextData = {
+      preloadContext: {
+        preloadProperty: 'number',
+        pageProperty: 'undefined',
+        typeofRequire: 'function',
+        typeofProcess: 'object',
+        typeofArrayPush: 'function',
+        typeofFunctionApply: 'function'
+      },
+      pageContext: {
+        preloadProperty: 'undefined',
+        pageProperty: 'string',
+        typeofRequire: 'undefined',
+        typeofProcess: 'undefined',
+        typeofArrayPush: 'number',
+        typeofFunctionApply: 'boolean',
+        typeofPreloadExecuteJavaScriptProperty: 'number',
+        typeofOpenedWindow: 'object'
+      }
+    }
+
     beforeEach(() => {
       if (w != null) w.destroy()
       w = new BrowserWindow({
@@ -1849,56 +1870,18 @@ describe('BrowserWindow module', function () {
 
     it('separates the page context from the Electron/preload context', (done) => {
       ipcMain.once('isolated-world', (event, data) => {
-        assert.deepEqual(data, {
-          preloadContext: {
-            preloadProperty: 'number',
-            pageProperty: 'undefined',
-            typeofRequire: 'function',
-            typeofProcess: 'object',
-            typeofArrayPush: 'function',
-            typeofFunctionApply: 'function'
-          },
-          pageContext: {
-            preloadProperty: 'undefined',
-            pageProperty: 'string',
-            typeofRequire: 'undefined',
-            typeofProcess: 'undefined',
-            typeofArrayPush: 'number',
-            typeofFunctionApply: 'boolean',
-            typeofPreloadExecuteJavaScriptProperty: 'number'
-          }
-        })
+        assert.deepEqual(data, expectedContextData)
         done()
       })
-
       w.loadURL('file://' + fixtures + '/api/isolated.html')
     })
 
     it('recreates the contexts on reload', (done) => {
       w.webContents.once('did-finish-load', () => {
         ipcMain.once('isolated-world', (event, data) => {
-          assert.deepEqual(data, {
-            preloadContext: {
-              preloadProperty: 'number',
-              pageProperty: 'undefined',
-              typeofRequire: 'function',
-              typeofProcess: 'object',
-              typeofArrayPush: 'function',
-              typeofFunctionApply: 'function'
-            },
-            pageContext: {
-              preloadProperty: 'undefined',
-              pageProperty: 'string',
-              typeofRequire: 'undefined',
-              typeofProcess: 'undefined',
-              typeofArrayPush: 'number',
-              typeofFunctionApply: 'boolean',
-              typeofPreloadExecuteJavaScriptProperty: 'number'
-            }
-          })
+          assert.deepEqual(data, expectedContextData)
           done()
         })
-
         w.webContents.reload()
       })
       w.loadURL('file://' + fixtures + '/api/isolated.html')
@@ -1909,7 +1892,6 @@ describe('BrowserWindow module', function () {
         assert.equal(window.webContents.getWebPreferences().contextIsolation, true)
         done()
       })
-
       w.loadURL('file://' + fixtures + '/pages/window-open.html')
     })
   })
index a0cc84770e91d3e86de53c384f7dcbb468bceaab..562bf01b7c1037431642c66e1e98c14514174bba 100644 (file)
@@ -7,6 +7,10 @@
       window.hello = 'world'
       Array.prototype.push = 3
       Function.prototype.apply = true
+
+      const opened = window.open()
+      opened.close()
+
       window.postMessage({
         preloadProperty: typeof window.foo,
         pageProperty: typeof window.hello,
@@ -14,7 +18,8 @@
         typeofProcess: typeof process,
         typeofArrayPush: typeof Array.prototype.push,
         typeofFunctionApply: typeof Function.prototype.apply,
-        typeofPreloadExecuteJavaScriptProperty: typeof window.preloadExecuteJavaScriptProperty
+        typeofPreloadExecuteJavaScriptProperty: typeof window.preloadExecuteJavaScriptProperty,
+        typeofOpenedWindow: typeof opened
       }, '*')
     </script>
   </head>
index 2d3ce3c06e319102d7b83fc947e55022c0686cb9..616fc459346b2636b0177d917756daee2e0a4a23 100644 (file)
@@ -448,13 +448,15 @@ describe('<webview> tag', function () {
             typeofProcess: 'undefined',
             typeofArrayPush: 'number',
             typeofFunctionApply: 'boolean',
-            typeofPreloadExecuteJavaScriptProperty: 'number'
+            typeofPreloadExecuteJavaScriptProperty: 'number',
+            typeofOpenedWindow: 'object'
           }
         })
         done()
       })
 
       webview.setAttribute('preload', path.join(fixtures, 'api', 'isolated-preload.js'))
+      webview.setAttribute('allowpopups', 'yes')
       webview.setAttribute('webpreferences', 'contextIsolation=yes')
       webview.src = 'file://' + fixtures + '/api/isolated.html'
       document.body.appendChild(webview)