Browse Source

feat: add support for content scripts 'all_frames' option (#17258)

* feat: add support for content scripts 'all_frames' option

* merged content script tests

'all_frames' test now runs on all variants of sandbox/contentIsolation configurations :D
Samuel Maddock 6 years ago
parent
commit
8ee153dae1

+ 2 - 1
lib/browser/chrome-extension.js

@@ -345,7 +345,8 @@ const injectContentScripts = function (manifest) {
       matches: script.matches,
       js: script.js ? script.js.map(readArrayOfFiles) : [],
       css: script.css ? script.css.map(readArrayOfFiles) : [],
-      runAt: script.run_at || 'document_idle'
+      runAt: script.run_at || 'document_idle',
+      allFrames: script.all_frames || false
     }
   }
 

+ 1 - 0
lib/renderer/content-scripts-injector.ts

@@ -69,6 +69,7 @@ const runAllStylesheet = function (css: Array<Electron.InjectionBase>) {
 // Run injected scripts.
 // https://developer.chrome.com/extensions/content_scripts
 const injectContentScript = function (extensionId: string, script: Electron.ContentScript) {
+  if (!process.isMainFrame && !script.allFrames) return
   if (!script.matches.some(matchesPattern)) return
 
   if (script.js) {

+ 1 - 3
lib/renderer/init.ts

@@ -85,9 +85,7 @@ switch (window.location.protocol) {
     windowSetup(ipcRendererInternal, guestInstanceId, openerId, isHiddenPage, usesNativeWindowOpen)
 
     // Inject content scripts.
-    if (process.isMainFrame) {
-      require('@electron/internal/renderer/content-scripts-injector')(process.getRenderProcessPreferences)
-    }
+    require('@electron/internal/renderer/content-scripts-injector')(process.getRenderProcessPreferences)
   }
 }
 

+ 121 - 41
spec/content-script-spec.js

@@ -3,68 +3,148 @@ const { remote } = require('electron')
 const path = require('path')
 
 const { closeWindow } = require('./window-helpers')
+const { emittedNTimes } = require('./events-helpers')
 
-const { BrowserWindow } = remote
+const { BrowserWindow, ipcMain } = remote
+
+describe('chrome extension content scripts', () => {
+  const fixtures = path.resolve(__dirname, 'fixtures')
+  const extensionPath = path.resolve(fixtures, 'extensions')
+
+  const addExtension = (name) => BrowserWindow.addExtension(path.resolve(extensionPath, name))
+  const removeAllExtensions = () => {
+    Object.keys(BrowserWindow.getExtensions()).map(extName => {
+      BrowserWindow.removeExtension(extName)
+    })
+  }
+
+  let responseIdCounter = 0
+  const executeJavaScriptInFrame = (webContents, frameRoutingId, code) => {
+    return new Promise(resolve => {
+      const responseId = responseIdCounter++
+      ipcMain.once(`executeJavaScriptInFrame_${responseId}`, (event, result) => {
+        resolve(result)
+      })
+      webContents.send('executeJavaScriptInFrame', frameRoutingId, code, responseId)
+    })
+  }
 
-describe('chrome content scripts', () => {
   const generateTests = (sandboxEnabled, contextIsolationEnabled) => {
     describe(`with sandbox ${sandboxEnabled ? 'enabled' : 'disabled'} and context isolation ${contextIsolationEnabled ? 'enabled' : 'disabled'}`, () => {
       let w
 
-      beforeEach(async () => {
-        await closeWindow(w)
-        w = new BrowserWindow({
-          show: false,
-          width: 400,
-          height: 400,
-          webPreferences: {
-            contextIsolation: contextIsolationEnabled,
-            sandbox: sandboxEnabled
-          }
+      describe('supports "run_at" option', () => {
+        beforeEach(async () => {
+          await closeWindow(w)
+          w = new BrowserWindow({
+            show: false,
+            width: 400,
+            height: 400,
+            webPreferences: {
+              contextIsolation: contextIsolationEnabled,
+              sandbox: sandboxEnabled
+            }
+          })
         })
-      })
 
-      afterEach(() => {
-        Object.keys(BrowserWindow.getExtensions()).map(extName => {
-          BrowserWindow.removeExtension(extName)
+        afterEach(() => {
+          removeAllExtensions()
+          return closeWindow(w).then(() => { w = null })
         })
-        return closeWindow(w).then(() => { w = null })
-      })
 
-      const addExtension = (name) => {
-        const extensionPath = path.join(__dirname, 'fixtures', 'extensions', name)
-        BrowserWindow.addExtension(extensionPath)
-      }
+        it('should run content script at document_start', (done) => {
+          addExtension('content-script-document-start')
+          w.webContents.once('dom-ready', () => {
+            w.webContents.executeJavaScript('document.documentElement.style.backgroundColor', (result) => {
+              expect(result).to.equal('red')
+              done()
+            })
+          })
+          w.loadURL('about:blank')
+        })
 
-      it('should run content script at document_start', (done) => {
-        addExtension('content-script-document-start')
-        w.webContents.once('dom-ready', () => {
-          w.webContents.executeJavaScript('document.documentElement.style.backgroundColor', (result) => {
+        it('should run content script at document_idle', (done) => {
+          addExtension('content-script-document-idle')
+          w.loadURL('about:blank')
+          w.webContents.executeJavaScript('document.body.style.backgroundColor', (result) => {
             expect(result).to.equal('red')
             done()
           })
         })
-        w.loadURL('about:blank')
-      })
 
-      it('should run content script at document_idle', (done) => {
-        addExtension('content-script-document-idle')
-        w.loadURL('about:blank')
-        w.webContents.executeJavaScript('document.body.style.backgroundColor', (result) => {
-          expect(result).to.equal('red')
-          done()
+        it('should run content script at document_end', (done) => {
+          addExtension('content-script-document-end')
+          w.webContents.once('did-finish-load', () => {
+            w.webContents.executeJavaScript('document.documentElement.style.backgroundColor', (result) => {
+              expect(result).to.equal('red')
+              done()
+            })
+          })
+          w.loadURL('about:blank')
         })
       })
 
-      it('should run content script at document_end', (done) => {
-        addExtension('content-script-document-end')
-        w.webContents.once('did-finish-load', () => {
-          w.webContents.executeJavaScript('document.documentElement.style.backgroundColor', (result) => {
-            expect(result).to.equal('red')
-            done()
+      describe('supports "all_frames" option', () => {
+        const contentScript = path.resolve(fixtures, 'extensions/content-script')
+
+        // Computed style values
+        const COLOR_RED = `rgb(255, 0, 0)`
+        const COLOR_BLUE = `rgb(0, 0, 255)`
+        const COLOR_TRANSPARENT = `rgba(0, 0, 0, 0)`
+
+        before(() => {
+          BrowserWindow.addExtension(contentScript)
+        })
+
+        after(() => {
+          BrowserWindow.removeExtension('content-script-test')
+        })
+
+        beforeEach(() => {
+          w = new BrowserWindow({
+            show: false,
+            webPreferences: {
+              // enable content script injection in subframes
+              nodeIntegrationInSubFrames: true,
+              preload: path.join(contentScript, 'all_frames-preload.js')
+            }
+          })
+        })
+
+        afterEach(() =>
+          closeWindow(w).then(() => {
+            w = null
           })
+        )
+
+        it('applies matching rules in subframes', async () => {
+          const detailsPromise = emittedNTimes(w.webContents, 'did-frame-finish-load', 2)
+          w.loadFile(path.join(contentScript, 'frame-with-frame.html'))
+          const frameEvents = await detailsPromise
+          await Promise.all(
+            frameEvents.map(async frameEvent => {
+              const [, isMainFrame, , frameRoutingId] = frameEvent
+              const result = await executeJavaScriptInFrame(
+                w.webContents,
+                frameRoutingId,
+                `(() => {
+                  const a = document.getElementById('all_frames_enabled')
+                  const b = document.getElementById('all_frames_disabled')
+                  return {
+                    enabledColor: getComputedStyle(a).backgroundColor,
+                    disabledColor: getComputedStyle(b).backgroundColor
+                  }
+                })()`
+              )
+              expect(result.enabledColor).to.equal(COLOR_RED)
+              if (isMainFrame) {
+                expect(result.disabledColor).to.equal(COLOR_BLUE)
+              } else {
+                expect(result.disabledColor).to.equal(COLOR_TRANSPARENT) // null color
+              }
+            })
+          )
         })
-        w.loadURL('about:blank')
       })
     })
   }

+ 3 - 0
spec/fixtures/extensions/content-script/all_frames-disabled.css

@@ -0,0 +1,3 @@
+#all_frames_disabled {
+    background: blue;
+}

+ 3 - 0
spec/fixtures/extensions/content-script/all_frames-enabled.css

@@ -0,0 +1,3 @@
+#all_frames_enabled {
+    background: red;
+}

+ 14 - 0
spec/fixtures/extensions/content-script/all_frames-preload.js

@@ -0,0 +1,14 @@
+const { ipcRenderer, webFrame } = require('electron')
+
+if (process.isMainFrame) {
+  // https://github.com/electron/electron/issues/17252
+  ipcRenderer.on('executeJavaScriptInFrame', (event, frameRoutingId, code, responseId) => {
+    const frame = webFrame.findFrameByRoutingId(frameRoutingId)
+    if (!frame) {
+      throw new Error(`Can't find frame for routing ID ${frameRoutingId}`)
+    }
+    frame.executeJavaScript(code, false, result => {
+      event.sender.send(`executeJavaScriptInFrame_${responseId}`, result)
+    })
+  })
+}

+ 15 - 0
spec/fixtures/extensions/content-script/frame-with-frame.html

@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <meta http-equiv="X-UA-Compatible" content="ie=edge">
+  <title>Document</title>
+</head>
+<body>
+  This is a frame, is has one child
+  <iframe src="./frame.html"></iframe>
+  <div id="all_frames_enabled"></div>
+  <div id="all_frames_disabled"></div>
+</body>
+</html>

+ 12 - 0
spec/fixtures/extensions/content-script/frame.html

@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <title>Document</title>
+</head>
+<body>
+  This is a frame, it has no children
+  <div id="all_frames_enabled"></div>
+  <div id="all_frames_disabled"></div>
+</body>
+</html>

+ 19 - 0
spec/fixtures/extensions/content-script/manifest.json

@@ -0,0 +1,19 @@
+{
+  "name": "content-script-test",
+  "version": "1.0",
+  "content_scripts": [
+    {
+      "matches": ["<all_urls>"],
+      "css": ["all_frames-enabled.css"],
+      "run_at": "document_start",
+      "all_frames": true
+    },
+    {
+      "matches": ["<all_urls>"],
+      "css": ["all_frames-disabled.css"],
+      "run_at": "document_start",
+      "all_frames": false
+    }
+  ],
+  "manifest_version": 2
+}

+ 5 - 0
typings/internal-electron.d.ts

@@ -39,6 +39,11 @@ declare namespace Electron {
     matches: {
       some: (input: (pattern: string) => boolean | RegExpMatchArray | null) => boolean;
     }
+    /**
+     * Whether to match all frames, or only the top one.
+     * https://developer.chrome.com/extensions/content_scripts#frames
+     */
+    allFrames: boolean
   }
 
   interface RendererProcessPreference {