diff --git a/shell/browser/electron_browser_client.cc b/shell/browser/electron_browser_client.cc index 5453bd08848f..1f4bdde6ddfa 100644 --- a/shell/browser/electron_browser_client.cc +++ b/shell/browser/electron_browser_client.cc @@ -186,6 +186,63 @@ const base::FilePath::StringPieceType kPathDelimiter = FILE_PATH_LITERAL(";"); const base::FilePath::StringPieceType kPathDelimiter = FILE_PATH_LITERAL(":"); #endif +#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS) +// Used by the GetPrivilegeRequiredByUrl() and GetProcessPrivilege() functions +// below. Extension, and isolated apps require different privileges to be +// granted to their RenderProcessHosts. This classification allows us to make +// sure URLs are served by hosts with the right set of privileges. +enum RenderProcessHostPrivilege { + PRIV_NORMAL, + PRIV_HOSTED, + PRIV_ISOLATED, + PRIV_EXTENSION, +}; + +RenderProcessHostPrivilege GetPrivilegeRequiredByUrl( + const GURL& url, + extensions::ExtensionRegistry* registry) { + // Default to a normal renderer cause it is lower privileged. This should only + // occur if the URL on a site instance is either malformed, or uninitialized. + // If it is malformed, then there is no need for better privileges anyways. + // If it is uninitialized, but eventually settles on being an a scheme other + // than normal webrenderer, the navigation logic will correct us out of band + // anyways. + if (!url.is_valid()) + return PRIV_NORMAL; + + if (!url.SchemeIs(extensions::kExtensionScheme)) + return PRIV_NORMAL; + + return PRIV_EXTENSION; +} + +RenderProcessHostPrivilege GetProcessPrivilege( + content::RenderProcessHost* process_host, + extensions::ProcessMap* process_map, + extensions::ExtensionRegistry* registry) { + std::set extension_ids = + process_map->GetExtensionsInProcess(process_host->GetID()); + if (extension_ids.empty()) + return PRIV_NORMAL; + + return PRIV_EXTENSION; +} + +const extensions::Extension* GetEnabledExtensionFromEffectiveURL( + content::BrowserContext* context, + const GURL& effective_url) { + if (!effective_url.SchemeIs(extensions::kExtensionScheme)) + return nullptr; + + extensions::ExtensionRegistry* registry = + extensions::ExtensionRegistry::Get(context); + if (!registry) + return nullptr; + + return registry->enabled_extensions().GetByID(effective_url.host()); +} +#endif // BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS) + } // namespace // static @@ -760,6 +817,40 @@ void ElectronBrowserClient::SiteInstanceGotProcess( #endif // BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS) } +bool ElectronBrowserClient::IsSuitableHost( + content::RenderProcessHost* process_host, + const GURL& site_url) { +#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS) + auto* browser_context = process_host->GetBrowserContext(); + extensions::ExtensionRegistry* registry = + extensions::ExtensionRegistry::Get(browser_context); + extensions::ProcessMap* process_map = + extensions::ProcessMap::Get(browser_context); + + // Otherwise, just make sure the process privilege matches the privilege + // required by the site. + RenderProcessHostPrivilege privilege_required = + GetPrivilegeRequiredByUrl(site_url, registry); + return GetProcessPrivilege(process_host, process_map, registry) == + privilege_required; +#else + return content::ContentBrowserClient::IsSuitableHost(process_host, site_url); +#endif +} + +bool ElectronBrowserClient::ShouldUseProcessPerSite( + content::BrowserContext* browser_context, + const GURL& effective_url) { +#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS) + const extensions::Extension* extension = + GetEnabledExtensionFromEffectiveURL(browser_context, effective_url); + return extension != nullptr; +#else + return content::ContentBrowserClient::ShouldUseProcessPerSite(browser_context, + effective_url); +#endif +} + void ElectronBrowserClient::SiteInstanceDeleting( content::SiteInstance* site_instance) { // We are storing weak_ptr, is it fundamental to maintain the map up-to-date diff --git a/shell/browser/electron_browser_client.h b/shell/browser/electron_browser_client.h index e3a557ef1aed..6e0b744b2ed2 100644 --- a/shell/browser/electron_browser_client.h +++ b/shell/browser/electron_browser_client.h @@ -221,6 +221,10 @@ class ElectronBrowserClient : public content::ContentBrowserClient, bool first_auth_attempt, LoginAuthRequiredCallback auth_required_callback) override; void SiteInstanceGotProcess(content::SiteInstance* site_instance) override; + bool IsSuitableHost(content::RenderProcessHost* process_host, + const GURL& site_url) override; + bool ShouldUseProcessPerSite(content::BrowserContext* browser_context, + const GURL& effective_url) override; // content::RenderProcessHostObserver: void RenderProcessHostDestroyed(content::RenderProcessHost* host) override; diff --git a/shell/common/extensions/api/_api_features.json b/shell/common/extensions/api/_api_features.json index 95f36816368f..5714a4af9182 100644 --- a/shell/common/extensions/api/_api_features.json +++ b/shell/common/extensions/api/_api_features.json @@ -9,6 +9,10 @@ "extension_types": ["extension"], "contexts": ["blessed_extension"] }, + "extension.getBackgroundPage": { + "contexts": ["blessed_extension"], + "disallow_for_service_workers": true + }, "extension.getURL": { "contexts": ["blessed_extension", "unblessed_extension", "content_script"] } diff --git a/shell/common/extensions/api/extension.json b/shell/common/extensions/api/extension.json index 77ed19d02d81..4122a36b4017 100644 --- a/shell/common/extensions/api/extension.json +++ b/shell/common/extensions/api/extension.json @@ -12,6 +12,20 @@ "properties": { }, "functions": [ + { + "name": "getBackgroundPage", + "nocompile": true, + "type": "function", + "description": "Returns the JavaScript 'window' object for the background page running inside the current extension. Returns null if the extension has no background page.", + "parameters": [], + "returns": { + "type": "object", + "optional": true, + "name": "backgroundPageGlobal", + "isInstanceOf": "Window", + "additionalProperties": { "type": "any" } + } + }, { "name": "getURL", "deprecated": "Please use $(ref:runtime.getURL).", diff --git a/shell/common/extensions/api/tabs.json b/shell/common/extensions/api/tabs.json index 15865bf83625..de26d4370afc 100644 --- a/shell/common/extensions/api/tabs.json +++ b/shell/common/extensions/api/tabs.json @@ -1,6 +1,7 @@ [ { "namespace": "tabs", + "description": "Use the chrome.tabs API to interact with the browser's tab system. You can use this API to create, modify, and rearrange tabs in the browser.", "functions": [ { "name": "executeScript", diff --git a/spec-main/extensions-spec.ts b/spec-main/extensions-spec.ts index 0ebf9ffa8ed4..e1ff0268bed4 100644 --- a/spec-main/extensions-spec.ts +++ b/spec-main/extensions-spec.ts @@ -31,8 +31,8 @@ ifdescribe(process.electronBinding('features').isExtensionsEnabled())('chrome ex // extension registry is redirected to the main session. so installing an // extension in an in-memory session results in it being installed in the // default session. - const customSession = session.fromPartition(`persist:${require('uuid').v4()}`); - (customSession as any).loadExtension(path.join(fixtures, 'extensions', 'red-bg')) + const customSession = session.fromPartition(`persist:${require('uuid').v4()}`) + await customSession.loadExtension(path.join(fixtures, 'extensions', 'red-bg')) const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } }) await w.loadURL(url) const bg = await w.webContents.executeJavaScript('document.documentElement.style.backgroundColor') @@ -41,14 +41,14 @@ ifdescribe(process.electronBinding('features').isExtensionsEnabled())('chrome ex it('removes an extension', async () => { const customSession = session.fromPartition(`persist:${require('uuid').v4()}`) - const { id } = await (customSession as any).loadExtension(path.join(fixtures, 'extensions', 'red-bg')) + const { id } = await customSession.loadExtension(path.join(fixtures, 'extensions', 'red-bg')) { const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } }) await w.loadURL(url) const bg = await w.webContents.executeJavaScript('document.documentElement.style.backgroundColor') expect(bg).to.equal('red') } - (customSession as any).removeExtension(id) + customSession.removeExtension(id) { const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } }) await w.loadURL(url) @@ -59,21 +59,21 @@ ifdescribe(process.electronBinding('features').isExtensionsEnabled())('chrome ex it('lists loaded extensions in getAllExtensions', async () => { const customSession = session.fromPartition(`persist:${require('uuid').v4()}`) - const e = await (customSession as any).loadExtension(path.join(fixtures, 'extensions', 'red-bg')) - expect((customSession as any).getAllExtensions()).to.deep.equal([e]); - (customSession as any).removeExtension(e.id) - expect((customSession as any).getAllExtensions()).to.deep.equal([]) + const e = await customSession.loadExtension(path.join(fixtures, 'extensions', 'red-bg')) + expect(customSession.getAllExtensions()).to.deep.equal([e]) + customSession.removeExtension(e.id) + expect(customSession.getAllExtensions()).to.deep.equal([]) }) it('gets an extension by id', async () => { const customSession = session.fromPartition(`persist:${require('uuid').v4()}`) - const e = await (customSession as any).loadExtension(path.join(fixtures, 'extensions', 'red-bg')) - expect((customSession as any).getExtension(e.id)).to.deep.equal(e) + const e = await customSession.loadExtension(path.join(fixtures, 'extensions', 'red-bg')) + expect(customSession.getExtension(e.id)).to.deep.equal(e) }) it('confines an extension to the session it was loaded in', async () => { - const customSession = session.fromPartition(`persist:${require('uuid').v4()}`); - (customSession as any).loadExtension(path.join(fixtures, 'extensions', 'red-bg')) + const customSession = session.fromPartition(`persist:${require('uuid').v4()}`) + customSession.loadExtension(path.join(fixtures, 'extensions', 'red-bg')) const w = new BrowserWindow({ show: false }) // not in the session await w.loadURL(url) const bg = await w.webContents.executeJavaScript('document.documentElement.style.backgroundColor') @@ -83,8 +83,8 @@ ifdescribe(process.electronBinding('features').isExtensionsEnabled())('chrome ex describe('chrome.runtime', () => { let content: any before(async () => { - const customSession = session.fromPartition(`persist:${require('uuid').v4()}`); - (customSession as any).loadExtension(path.join(fixtures, 'extensions', 'chrome-runtime')) + const customSession = session.fromPartition(`persist:${require('uuid').v4()}`) + customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-runtime')) const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } }) try { await w.loadURL(url) @@ -107,8 +107,8 @@ ifdescribe(process.electronBinding('features').isExtensionsEnabled())('chrome ex describe('chrome.storage', () => { it('stores and retrieves a key', async () => { - const customSession = session.fromPartition(`persist:${require('uuid').v4()}`); - (customSession as any).loadExtension(path.join(fixtures, 'extensions', 'chrome-storage')) + const customSession = session.fromPartition(`persist:${require('uuid').v4()}`) + await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-storage')) const w = new BrowserWindow({ show: false, webPreferences: { session: customSession, nodeIntegration: true } }) try { const p = emittedOnce(ipcMain, 'storage-success') @@ -124,7 +124,7 @@ ifdescribe(process.electronBinding('features').isExtensionsEnabled())('chrome ex describe('chrome.tabs', () => { it('executeScript', async () => { const customSession = session.fromPartition(`persist:${require('uuid').v4()}`) - ;(customSession as any).loadExtension(path.join(fixtures, 'extensions', 'chrome-api')) + await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-api')) const w = new BrowserWindow({ show: false, webPreferences: { session: customSession, nodeIntegration: true } }) await w.loadURL(url) @@ -139,7 +139,7 @@ ifdescribe(process.electronBinding('features').isExtensionsEnabled())('chrome ex it('sendMessage receives the response', async function () { const customSession = session.fromPartition(`persist:${require('uuid').v4()}`) - ;(customSession as any).loadExtension(path.join(fixtures, 'extensions', 'chrome-api')) + await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-api')) const w = new BrowserWindow({ show: false, webPreferences: { session: customSession, nodeIntegration: true } }) await w.loadURL(url) @@ -157,7 +157,7 @@ ifdescribe(process.electronBinding('features').isExtensionsEnabled())('chrome ex describe('background pages', () => { it('loads a lazy background page when sending a message', async () => { const customSession = session.fromPartition(`persist:${require('uuid').v4()}`) - ;(customSession as any).loadExtension(path.join(fixtures, 'extensions', 'lazy-background-page')) + await customSession.loadExtension(path.join(fixtures, 'extensions', 'lazy-background-page')) const w = new BrowserWindow({ show: false, webPreferences: { session: customSession, nodeIntegration: true } }) try { w.loadURL(url) @@ -170,6 +170,33 @@ ifdescribe(process.electronBinding('features').isExtensionsEnabled())('chrome ex w.destroy() } }) + + it('can use extension.getBackgroundPage from a ui page', async () => { + const customSession = session.fromPartition(`persist:${require('uuid').v4()}`) + const { id } = await customSession.loadExtension(path.join(fixtures, 'extensions', 'lazy-background-page')) + const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } }) + await w.loadURL(`chrome-extension://${id}/page-get-background.html`) + const receivedMessage = await w.webContents.executeJavaScript(`window.completionPromise`) + expect(receivedMessage).to.deep.equal({ some: 'message' }) + }) + + it('can use extension.getBackgroundPage from a ui page', async () => { + const customSession = session.fromPartition(`persist:${require('uuid').v4()}`) + const { id } = await customSession.loadExtension(path.join(fixtures, 'extensions', 'lazy-background-page')) + const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } }) + await w.loadURL(`chrome-extension://${id}/page-get-background.html`) + const receivedMessage = await w.webContents.executeJavaScript(`window.completionPromise`) + expect(receivedMessage).to.deep.equal({ some: 'message' }) + }) + + it('can use runtime.getBackgroundPage from a ui page', async () => { + const customSession = session.fromPartition(`persist:${require('uuid').v4()}`) + const { id } = await customSession.loadExtension(path.join(fixtures, 'extensions', 'lazy-background-page')) + const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } }) + await w.loadURL(`chrome-extension://${id}/page-runtime-get-background.html`) + const receivedMessage = await w.webContents.executeJavaScript(`window.completionPromise`) + expect(receivedMessage).to.deep.equal({ some: 'message' }) + }) }) describe('devtools extensions', () => { @@ -201,8 +228,8 @@ ifdescribe(process.electronBinding('features').isExtensionsEnabled())('chrome ex } it('loads a devtools extension', async () => { - const customSession = session.fromPartition(`persist:${require('uuid').v4()}`); - (customSession as any).loadExtension(path.join(fixtures, 'extensions', 'devtools-extension')) + const customSession = session.fromPartition(`persist:${require('uuid').v4()}`) + customSession.loadExtension(path.join(fixtures, 'extensions', 'devtools-extension')) const w = new BrowserWindow({ show: true, webPreferences: { session: customSession, nodeIntegration: true } }) await w.loadURL('data:text/html,hello') w.webContents.openDevTools() @@ -213,8 +240,8 @@ ifdescribe(process.electronBinding('features').isExtensionsEnabled())('chrome ex describe('deprecation shims', () => { afterEach(() => { - (session.defaultSession as any).getAllExtensions().forEach((e: any) => { - (session.defaultSession as any).removeExtension(e.id) + session.defaultSession.getAllExtensions().forEach((e: any) => { + session.defaultSession.removeExtension(e.id) }) }) @@ -387,6 +414,30 @@ ifdescribe(process.electronBinding('features').isExtensionsEnabled())('chrome ex generateTests(true, false) generateTests(true, true) }) + + describe('extension ui pages', () => { + afterEach(() => { + session.defaultSession.getAllExtensions().forEach(e => { + session.defaultSession.removeExtension(e.id) + }) + }) + + it('loads a ui page of an extension', async () => { + const { id } = await session.defaultSession.loadExtension(path.join(fixtures, 'extensions', 'ui-page')) + const w = new BrowserWindow({ show: false }) + await w.loadURL(`chrome-extension://${id}/bare-page.html`) + const textContent = await w.webContents.executeJavaScript(`document.body.textContent`) + expect(textContent).to.equal('ui page loaded ok\n') + }) + + it('can load resources', async () => { + const { id } = await session.defaultSession.loadExtension(path.join(fixtures, 'extensions', 'ui-page')) + const w = new BrowserWindow({ show: false }) + await w.loadURL(`chrome-extension://${id}/page-script-load.html`) + const textContent = await w.webContents.executeJavaScript(`document.body.textContent`) + expect(textContent).to.equal('script loaded ok\n') + }) + }) }) ifdescribe(!process.electronBinding('features').isExtensionsEnabled())('chrome extensions', () => { diff --git a/spec-main/fixtures/extensions/lazy-background-page/background.js b/spec-main/fixtures/extensions/lazy-background-page/background.js index a4a75971791b..13bf8248795e 100644 --- a/spec-main/fixtures/extensions/lazy-background-page/background.js +++ b/spec-main/fixtures/extensions/lazy-background-page/background.js @@ -1,4 +1,5 @@ /* eslint-disable no-undef */ chrome.runtime.onMessage.addListener((message, sender, reply) => { + window.receivedMessage = message reply({ message, sender }) }) diff --git a/spec-main/fixtures/extensions/lazy-background-page/get-background-page.js b/spec-main/fixtures/extensions/lazy-background-page/get-background-page.js new file mode 100644 index 000000000000..52b4df957f70 --- /dev/null +++ b/spec-main/fixtures/extensions/lazy-background-page/get-background-page.js @@ -0,0 +1,7 @@ +/* global chrome */ +window.completionPromise = new Promise((resolve) => { + window.completionPromiseResolve = resolve +}) +chrome.runtime.sendMessage({ some: 'message' }, (response) => { + window.completionPromiseResolve(chrome.extension.getBackgroundPage().receivedMessage) +}) diff --git a/spec-main/fixtures/extensions/lazy-background-page/page-get-background.html b/spec-main/fixtures/extensions/lazy-background-page/page-get-background.html new file mode 100644 index 000000000000..ab983bfd34c2 --- /dev/null +++ b/spec-main/fixtures/extensions/lazy-background-page/page-get-background.html @@ -0,0 +1 @@ + diff --git a/spec-main/fixtures/extensions/lazy-background-page/page-runtime-get-background.html b/spec-main/fixtures/extensions/lazy-background-page/page-runtime-get-background.html new file mode 100644 index 000000000000..eee3ba694e8d --- /dev/null +++ b/spec-main/fixtures/extensions/lazy-background-page/page-runtime-get-background.html @@ -0,0 +1 @@ + diff --git a/spec-main/fixtures/extensions/lazy-background-page/runtime-get-background-page.js b/spec-main/fixtures/extensions/lazy-background-page/runtime-get-background-page.js new file mode 100644 index 000000000000..d0a7cf7ddab3 --- /dev/null +++ b/spec-main/fixtures/extensions/lazy-background-page/runtime-get-background-page.js @@ -0,0 +1,9 @@ +/* global chrome */ +window.completionPromise = new Promise((resolve) => { + window.completionPromiseResolve = resolve +}) +chrome.runtime.sendMessage({ some: 'message' }, (response) => { + chrome.runtime.getBackgroundPage((bgPage) => { + window.completionPromiseResolve(bgPage.receivedMessage) + }) +}) diff --git a/spec-main/fixtures/extensions/ui-page/bare-page.html b/spec-main/fixtures/extensions/ui-page/bare-page.html new file mode 100644 index 000000000000..25735dccac8b --- /dev/null +++ b/spec-main/fixtures/extensions/ui-page/bare-page.html @@ -0,0 +1,2 @@ + +ui page loaded ok diff --git a/spec-main/fixtures/extensions/ui-page/manifest.json b/spec-main/fixtures/extensions/ui-page/manifest.json new file mode 100644 index 000000000000..9a0dac25e476 --- /dev/null +++ b/spec-main/fixtures/extensions/ui-page/manifest.json @@ -0,0 +1,5 @@ +{ + "name": "ui-page", + "version": "1.0", + "manifest_version": 2 +} diff --git a/spec-main/fixtures/extensions/ui-page/page-get-background.html b/spec-main/fixtures/extensions/ui-page/page-get-background.html new file mode 100644 index 000000000000..ab983bfd34c2 --- /dev/null +++ b/spec-main/fixtures/extensions/ui-page/page-get-background.html @@ -0,0 +1 @@ + diff --git a/spec-main/fixtures/extensions/ui-page/page-script-load.html b/spec-main/fixtures/extensions/ui-page/page-script-load.html new file mode 100644 index 000000000000..02a37b4bc0bc --- /dev/null +++ b/spec-main/fixtures/extensions/ui-page/page-script-load.html @@ -0,0 +1 @@ + diff --git a/spec-main/fixtures/extensions/ui-page/script.js b/spec-main/fixtures/extensions/ui-page/script.js new file mode 100644 index 000000000000..b7e2e07cf753 --- /dev/null +++ b/spec-main/fixtures/extensions/ui-page/script.js @@ -0,0 +1 @@ +document.write('script loaded ok')