Skip to content

Commit 58abfc9

Browse files
committed
fix: crx protocol requests from remote sessions
1 parent c7cad03 commit 58abfc9

File tree

6 files changed

+109
-22
lines changed

6 files changed

+109
-22
lines changed

packages/electron-chrome-extensions/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,22 @@ Add the `<browser-action-list>` element with attributes appropriate for your app
280280
<browser-action-list alignment="top right"></browser-action-list>
281281
```
282282

283+
##### Main process
284+
285+
For extension icons to appear in the list, the `crx://` protocol needs to be handled in the Session
286+
where it's intended to be displayed.
287+
288+
```js
289+
import { app, session } from 'electron'
290+
import { ElectronChromeExtensions } from 'electron-chrome-extensions'
291+
292+
app.whenReady().then(() => {
293+
// Provide the session where your app will display <browser-action-list>
294+
const appSession = session.defaultSession
295+
ElectronChromeExtensions.handleCRXProtocol(appSession)
296+
})
297+
```
298+
283299
##### Custom CSS
284300

285301
The `<browser-action-list>` element is a [Web Component](https://developer.mozilla.org/en-US/docs/Web/Web_Components). Its styles are encapsulated within a [Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM). However, it's still possible to customize its appearance using the [CSS shadow parts](https://developer.mozilla.org/en-US/docs/Web/CSS/::part) selector `::part(name)`.

packages/electron-chrome-extensions/spec/chrome-browserAction-spec.ts

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { emittedOnce } from './events-helpers'
66
import { uuid } from './spec-helpers'
77
import { useExtensionBrowser, useServer } from './hooks'
88
import { createCrxRemoteWindow } from './crx-helpers'
9-
import { once } from 'node:events'
9+
import { ElectronChromeExtensions } from '../'
1010

1111
describe('chrome.browserAction', () => {
1212
const server = useServer()
@@ -237,9 +237,39 @@ describe('chrome.browserAction', () => {
237237
extensionName: 'chrome-browserAction-popup',
238238
})
239239

240+
it('supports same-session requests', async () => {
241+
ElectronChromeExtensions.handleCRXProtocol(browser.session)
242+
243+
// Load again now that crx protocol is handled
244+
await browser.webContents.loadURL(server.getUrl())
245+
246+
const result = await browser.webContents.executeJavaScript(
247+
`(${function (extensionId: any, tabId: any) {
248+
const img = document.createElement('img')
249+
const params = new URLSearchParams({
250+
tabId: `${tabId}`,
251+
t: `${Date.now()}`,
252+
})
253+
const src = `crx://extension-icon/${extensionId}/32/2?${params.toString()}`
254+
return new Promise((resolve, reject) => {
255+
img.onload = () => resolve('success')
256+
img.onerror = () => {
257+
reject(new Error('error loading img, check devtools console' + src))
258+
}
259+
img.src = src
260+
})
261+
}})(${[browser.extension.id, browser.webContents.id]
262+
.map((v) => JSON.stringify(v))
263+
.join(', ')});`,
264+
)
265+
266+
expect(result).to.equal('success')
267+
})
268+
240269
it('supports cross-session requests', async () => {
270+
const extensionsPartition = browser.partition
241271
const otherSession = session.fromPartition(`persist:crx-${uuid()}`)
242-
browser.extensions.handleCRXProtocol(otherSession)
272+
ElectronChromeExtensions.handleCRXProtocol(otherSession)
243273

244274
browser.session.getPreloadScripts().forEach((script) => {
245275
otherSession.registerPreloadScript(script)
@@ -252,17 +282,24 @@ describe('chrome.browserAction', () => {
252282
await view.webContents.loadURL(server.getUrl())
253283

254284
const result = await view.webContents.executeJavaScript(
255-
`(${function (extensionId: any, tabId: any) {
285+
`(${function (extensionId: any, tabId: any, partition: any) {
256286
const img = document.createElement('img')
257-
const src = `crx://extension-icon/${extensionId}/32/2?tabId=-1&t=${Date.now()}`
287+
const params = new URLSearchParams({
288+
tabId: `${tabId}`,
289+
partition,
290+
t: `${Date.now()}`,
291+
})
292+
const src = `crx://extension-icon/${extensionId}/32/2?${params.toString()}`
258293
return new Promise((resolve, reject) => {
259294
img.onload = () => resolve('success')
260295
img.onerror = () => {
261296
reject(new Error('error loading img, check devtools console'))
262297
}
263298
img.src = src
264299
})
265-
}})(${JSON.stringify(browser.extension.id)}, ${browser.webContents.id});`,
300+
}})(${[browser.extension.id, browser.webContents.id, extensionsPartition]
301+
.map((v) => JSON.stringify(v))
302+
.join(', ')});`,
266303
)
267304

268305
expect(result).to.equal('success')

packages/electron-chrome-extensions/src/browser-action.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,8 +205,14 @@ export const injectBrowserAction = () => {
205205
private updateIcon(info: any) {
206206
const iconSize = 32
207207
const resizeType = 2
208-
const timeParam = info.iconModified ? `&t=${info.iconModified}` : ''
209-
const iconUrl = `crx://extension-icon/${this.id}/${iconSize}/${resizeType}?tabId=${this.tab}${timeParam}`
208+
const searchParams = new URLSearchParams({
209+
tabId: `${this.tab}`,
210+
partition: `${this.partition || DEFAULT_PARTITION}`,
211+
})
212+
if (info.iconModified) {
213+
searchParams.append('t', info.iconModified)
214+
}
215+
const iconUrl = `crx://extension-icon/${this.id}/${iconSize}/${resizeType}?${searchParams.toString()}`
210216
const bgImage = `url(${iconUrl})`
211217

212218
if (this.pendingIcon) {

packages/electron-chrome-extensions/src/browser/api/browser-action.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -209,17 +209,9 @@ export class BrowserActionAPI {
209209
session.on('extension-unloaded', (event, extension) => {
210210
this.removeActions(extension.id)
211211
})
212-
213-
this.handleCRXProtocol(session)
214-
}
215-
216-
handleCRXProtocol(session: Electron.Session) {
217-
if (!session.protocol.isProtocolHandled('crx')) {
218-
session.protocol.handle('crx', this.handleCrxRequest)
219-
}
220212
}
221213

222-
private handleCrxRequest = (request: GlobalRequest): GlobalResponse => {
214+
handleCRXRequest(request: GlobalRequest): GlobalResponse {
223215
d('%s', request.url)
224216

225217
try {

packages/electron-chrome-extensions/src/browser/index.ts

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,37 @@ export class ElectronChromeExtensions extends EventEmitter {
8787
return sessionMap.get(session)
8888
}
8989

90+
/**
91+
* Handles the 'crx://' protocol in the session.
92+
*
93+
* This is required to display <browser-action-list> extension icons.
94+
*/
95+
static handleCRXProtocol(session: Electron.Session) {
96+
if (session.protocol.isProtocolHandled('crx')) {
97+
session.protocol.unhandle('crx')
98+
}
99+
session.protocol.handle('crx', function handleCRXRequest(request) {
100+
let url
101+
try {
102+
url = new URL(request.url)
103+
} catch {
104+
return new Response('Invalid URL', { status: 404 })
105+
}
106+
107+
const partition = url?.searchParams.get('partition') || '_self'
108+
const remoteSession =
109+
partition === '_self' ? session : electronSession.fromPartition(partition)
110+
const extensions = ElectronChromeExtensions.fromSession(remoteSession)
111+
if (!extensions) {
112+
return new Response(`ElectronChromeExtensions not found for "${partition}"`, {
113+
status: 404,
114+
})
115+
}
116+
117+
return extensions.api.browserAction.handleCRXRequest(request)
118+
})
119+
}
120+
90121
private ctx: ExtensionContext
91122

92123
private api: {
@@ -141,11 +172,6 @@ export class ElectronChromeExtensions extends EventEmitter {
141172

142173
this.listenForExtensions()
143174
this.prependPreload(opts.modulePath)
144-
145-
// Register crx:// protocol in default session for convenience
146-
if (this.ctx.session !== electronSession.defaultSession) {
147-
this.handleCRXProtocol(electronSession.defaultSession)
148-
}
149175
}
150176

151177
private listenForExtensions() {
@@ -247,9 +273,16 @@ export class ElectronChromeExtensions extends EventEmitter {
247273

248274
/**
249275
* Handles the 'crx://' protocol in the session.
276+
*
277+
* @deprecated Call `ElectronChromeExtensions.handleCRXProtocol(session)`
278+
* instead. The CRX protocol is no longer one-to-one with
279+
* ElectronChromeExtensions instances. Instead, it should now be handled only
280+
* on the sessions where <browser-action-list> extension icons will be shown.
250281
*/
251282
handleCRXProtocol(session: Electron.Session) {
252-
this.api.browserAction.handleCRXProtocol(session)
283+
throw new Error(
284+
'extensions.handleCRXProtocol(session) is deprecated, call ElectronChromeExtensions.handleCRXProtocol(session) instead.',
285+
)
253286
}
254287

255288
/**

packages/shell/browser/main.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,9 @@ class Browser {
197197
},
198198
})
199199

200+
// Display <browser-action-list> extension icons.
201+
ElectronChromeExtensions.handleCRXProtocol(this.session)
202+
200203
this.extensions.on('browser-action-popup-created', (popup) => {
201204
this.popup = popup
202205
})

0 commit comments

Comments
 (0)