diff --git a/lib/browser/chrome-extension.js b/lib/browser/chrome-extension.js index 9b1420b9976..7110c8b6ea6 100644 --- a/lib/browser/chrome-extension.js +++ b/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 } } diff --git a/lib/renderer/content-scripts-injector.ts b/lib/renderer/content-scripts-injector.ts index b02d5603c00..2bd4129840e 100644 --- a/lib/renderer/content-scripts-injector.ts +++ b/lib/renderer/content-scripts-injector.ts @@ -69,6 +69,7 @@ const runAllStylesheet = function (css: Array) { // 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) { diff --git a/lib/renderer/init.ts b/lib/renderer/init.ts index 07a339adccf..08e6b27ed73 100644 --- a/lib/renderer/init.ts +++ b/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) } } diff --git a/spec/content-script-spec.js b/spec/content-script-spec.js index bbfb0c978ca..a5c2ce85eff 100644 --- a/spec/content-script-spec.js +++ b/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') + } }) }) - 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 + } + }) + ) + }) }) }) } diff --git a/spec/fixtures/extensions/content-script/all_frames-disabled.css b/spec/fixtures/extensions/content-script/all_frames-disabled.css new file mode 100644 index 00000000000..669863938cb --- /dev/null +++ b/spec/fixtures/extensions/content-script/all_frames-disabled.css @@ -0,0 +1,3 @@ +#all_frames_disabled { + background: blue; +} diff --git a/spec/fixtures/extensions/content-script/all_frames-enabled.css b/spec/fixtures/extensions/content-script/all_frames-enabled.css new file mode 100644 index 00000000000..73863ca994e --- /dev/null +++ b/spec/fixtures/extensions/content-script/all_frames-enabled.css @@ -0,0 +1,3 @@ +#all_frames_enabled { + background: red; +} diff --git a/spec/fixtures/extensions/content-script/all_frames-preload.js b/spec/fixtures/extensions/content-script/all_frames-preload.js new file mode 100644 index 00000000000..2ff8c3a1d60 --- /dev/null +++ b/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) + }) + }) +} diff --git a/spec/fixtures/extensions/content-script/frame-with-frame.html b/spec/fixtures/extensions/content-script/frame-with-frame.html new file mode 100644 index 00000000000..5045216c984 --- /dev/null +++ b/spec/fixtures/extensions/content-script/frame-with-frame.html @@ -0,0 +1,15 @@ + + + + + + + Document + + + This is a frame, is has one child + +
+
+ + \ No newline at end of file diff --git a/spec/fixtures/extensions/content-script/frame.html b/spec/fixtures/extensions/content-script/frame.html new file mode 100644 index 00000000000..9660e503d66 --- /dev/null +++ b/spec/fixtures/extensions/content-script/frame.html @@ -0,0 +1,12 @@ + + + + + Document + + + This is a frame, it has no children +
+
+ + \ No newline at end of file diff --git a/spec/fixtures/extensions/content-script/manifest.json b/spec/fixtures/extensions/content-script/manifest.json new file mode 100644 index 00000000000..e2862b5390a --- /dev/null +++ b/spec/fixtures/extensions/content-script/manifest.json @@ -0,0 +1,19 @@ +{ + "name": "content-script-test", + "version": "1.0", + "content_scripts": [ + { + "matches": [""], + "css": ["all_frames-enabled.css"], + "run_at": "document_start", + "all_frames": true + }, + { + "matches": [""], + "css": ["all_frames-disabled.css"], + "run_at": "document_start", + "all_frames": false + } + ], + "manifest_version": 2 +} diff --git a/typings/internal-electron.d.ts b/typings/internal-electron.d.ts index 6a73f6d8cca..0e24770c2b7 100644 --- a/typings/internal-electron.d.ts +++ b/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 {