Browse Source

feat: [extensions] support extension.getBackgroundPage (#21951)

* feat: [extensions] support extension.getBackgroundPage

* cleanup

* how does c++

* tests

* test for runtime.getBackgroundPage too
Jeremy Apthorp 5 years ago
parent
commit
9107157073

+ 91 - 0
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<std::string> 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

+ 4 - 0
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;

+ 4 - 0
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"]
   }

+ 14 - 0
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).",

+ 1 - 0
shell/common/extensions/api/tabs.json

@@ -1,6 +1,7 @@
 [
   {
     "namespace": "tabs",
+    "description": "Use the <code>chrome.tabs</code> 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",

+ 74 - 23
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', () => {

+ 1 - 0
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 })
 })

+ 7 - 0
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)
+})

+ 1 - 0
spec-main/fixtures/extensions/lazy-background-page/page-get-background.html

@@ -0,0 +1 @@
+<script src="get-background-page.js"></script>

+ 1 - 0
spec-main/fixtures/extensions/lazy-background-page/page-runtime-get-background.html

@@ -0,0 +1 @@
+<script src="runtime-get-background-page.js"></script>

+ 9 - 0
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)
+  })
+})

+ 2 - 0
spec-main/fixtures/extensions/ui-page/bare-page.html

@@ -0,0 +1,2 @@
+<!doctype html>
+<body>ui page loaded ok</body>

+ 5 - 0
spec-main/fixtures/extensions/ui-page/manifest.json

@@ -0,0 +1,5 @@
+{
+  "name": "ui-page",
+  "version": "1.0",
+  "manifest_version": 2
+}

+ 1 - 0
spec-main/fixtures/extensions/ui-page/page-get-background.html

@@ -0,0 +1 @@
+<script src="get-background-page.js"></script>

+ 1 - 0
spec-main/fixtures/extensions/ui-page/page-script-load.html

@@ -0,0 +1 @@
+<script src="script.js"></script>

+ 1 - 0
spec-main/fixtures/extensions/ui-page/script.js

@@ -0,0 +1 @@
+document.write('script loaded ok')