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
This commit is contained in:
Samuel Maddock 2019-03-08 18:53:25 -05:00 committed by Samuel Attard
parent b7fc50b7ca
commit 8ee153dae1
11 changed files with 196 additions and 45 deletions

View file

@ -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
}
}

View file

@ -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) {

View file

@ -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)
}
}

View file

@ -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')
}
})
})
w.loadURL('about:blank')
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
}
})
)
})
})
})
}

View file

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

View file

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

View file

@ -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)
})
})
}

View file

@ -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>

View file

@ -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>

View file

@ -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
}

View file

@ -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 {