Skip to content

Commit 610dfad

Browse files
committed
feat: extension updater
1 parent 420d08f commit 610dfad

File tree

7 files changed

+776
-476
lines changed

7 files changed

+776
-476
lines changed
Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
import * as fs from 'fs'
2+
import * as path from 'path'
3+
import { app, ipcMain } from 'electron'
4+
5+
import {
6+
ExtensionInstallStatus,
7+
MV2DeprecationStatus,
8+
Result,
9+
WebGlStatus,
10+
} from '../common/constants'
11+
import { downloadExtension } from './installer'
12+
13+
const d = require('debug')('electron-chrome-web-store:api')
14+
15+
const WEBSTORE_URL = 'https://chromewebstore.google.com'
16+
17+
function getExtensionInfo(ext: Electron.Extension) {
18+
const manifest: chrome.runtime.Manifest = ext.manifest
19+
return {
20+
description: manifest.description || '',
21+
enabled: !manifest.disabled,
22+
homepageUrl: manifest.homepage_url || '',
23+
hostPermissions: manifest.host_permissions || [],
24+
icons: Object.entries(manifest?.icons || {}).map(([size, url]) => ({
25+
size: parseInt(size),
26+
url: `chrome://extension-icon/${ext.id}/${size}/0`,
27+
})),
28+
id: ext.id,
29+
installType: 'normal',
30+
isApp: !!manifest.app,
31+
mayDisable: true,
32+
name: manifest.name,
33+
offlineEnabled: !!manifest.offline_enabled,
34+
optionsUrl: manifest.options_page
35+
? `chrome-extension://${ext.id}/${manifest.options_page}`
36+
: '',
37+
permissions: manifest.permissions || [],
38+
shortName: manifest.short_name || manifest.name,
39+
type: manifest.app ? 'app' : 'extension',
40+
updateUrl: manifest.update_url || '',
41+
version: manifest.version,
42+
}
43+
}
44+
45+
function getExtensionInstallStatus(
46+
state: WebStoreState,
47+
extensionId: ExtensionId,
48+
manifest?: chrome.runtime.Manifest,
49+
) {
50+
if (state.denylist?.has(extensionId)) {
51+
return ExtensionInstallStatus.BLOCKED_BY_POLICY
52+
}
53+
54+
if (state.allowlist && !state.allowlist.has(extensionId)) {
55+
return ExtensionInstallStatus.BLOCKED_BY_POLICY
56+
}
57+
58+
if (manifest) {
59+
if (manifest.manifest_version < 2) {
60+
return ExtensionInstallStatus.DEPRECATED_MANIFEST_VERSION
61+
}
62+
}
63+
64+
const extensions = state.session.getAllExtensions()
65+
const extension = extensions.find((ext) => ext.id === extensionId)
66+
67+
if (!extension) {
68+
return ExtensionInstallStatus.INSTALLABLE
69+
}
70+
71+
if (extension.manifest.disabled) {
72+
return ExtensionInstallStatus.DISABLED
73+
}
74+
75+
return ExtensionInstallStatus.ENABLED
76+
}
77+
78+
async function uninstallExtension(
79+
{ session, extensionsPath }: WebStoreState,
80+
extensionId: ExtensionId,
81+
) {
82+
const extensions = session.getAllExtensions()
83+
const existingExt = extensions.find((ext) => ext.id === extensionId)
84+
if (existingExt) {
85+
await session.removeExtension(extensionId)
86+
}
87+
88+
const extensionDir = path.join(extensionsPath, extensionId)
89+
try {
90+
const stat = await fs.promises.stat(extensionDir)
91+
if (stat.isDirectory()) {
92+
await fs.promises.rm(extensionDir, { recursive: true, force: true })
93+
}
94+
} catch (error: any) {
95+
if (error?.code !== 'ENOENT') {
96+
console.error(error)
97+
}
98+
}
99+
}
100+
101+
interface InstallDetails {
102+
id: string
103+
manifest: string
104+
localizedName: string
105+
esbAllowlist: boolean
106+
iconUrl: string
107+
}
108+
109+
async function beginInstall(state: WebStoreState, details: InstallDetails) {
110+
const extensionId = details.id
111+
112+
try {
113+
if (state.installing.has(extensionId)) {
114+
return { result: Result.INSTALL_IN_PROGRESS }
115+
}
116+
117+
let manifest: chrome.runtime.Manifest
118+
try {
119+
manifest = JSON.parse(details.manifest)
120+
} catch {
121+
return { result: Result.MANIFEST_ERROR }
122+
}
123+
124+
const installStatus = getExtensionInstallStatus(state, extensionId, manifest)
125+
switch (installStatus) {
126+
case ExtensionInstallStatus.INSTALLABLE:
127+
break // good to go
128+
case ExtensionInstallStatus.BLOCKED_BY_POLICY:
129+
return { result: Result.BLOCKED_BY_POLICY }
130+
default: {
131+
d('unable to install extension %s with status "%s"', extensionId, installStatus)
132+
return { result: Result.UNKNOWN_ERROR }
133+
}
134+
}
135+
136+
state.installing.add(extensionId)
137+
138+
// Check if extension is already loaded in session and remove it
139+
await uninstallExtension(state, extensionId)
140+
141+
// Create extension directory
142+
const installVersion = manifest.version
143+
const unpackedDir = path.join(state.extensionsPath, extensionId, `${installVersion}_0`)
144+
await fs.promises.mkdir(unpackedDir, { recursive: true })
145+
146+
await downloadExtension(extensionId, unpackedDir)
147+
148+
// Load extension into session
149+
await state.session.loadExtension(unpackedDir)
150+
151+
return { result: Result.SUCCESS }
152+
} catch (error) {
153+
console.error('Extension installation failed:', error)
154+
return {
155+
result: Result.INSTALL_ERROR,
156+
message: error instanceof Error ? error.message : String(error),
157+
}
158+
} finally {
159+
state.installing.delete(extensionId)
160+
}
161+
}
162+
163+
export function registerWebStoreApi(webStoreState: WebStoreState) {
164+
/** Handle IPCs from the Chrome Web Store. */
165+
const handle = (
166+
channel: string,
167+
handle: (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any,
168+
) => {
169+
ipcMain.handle(channel, async function handleWebStoreIpc(event, ...args) {
170+
d('received %s', channel)
171+
172+
const senderOrigin = event.senderFrame?.origin
173+
if (!senderOrigin || !senderOrigin.startsWith(WEBSTORE_URL)) {
174+
d('ignoring webstore request from %s', senderOrigin)
175+
return
176+
}
177+
178+
const result = await handle(event, ...args)
179+
d('%s result', channel, result)
180+
return result
181+
})
182+
}
183+
184+
handle('chromeWebstore.beginInstall', async (event, details: InstallDetails) => {
185+
const { senderFrame } = event
186+
187+
d('beginInstall', details)
188+
189+
const result = await beginInstall(webStoreState, details)
190+
191+
if (result.result === Result.SUCCESS) {
192+
queueMicrotask(() => {
193+
const ext = webStoreState.session.getExtension(details.id)
194+
if (ext) {
195+
// TODO: use WebFrameMain.isDestroyed
196+
try {
197+
senderFrame.send('chrome.management.onInstalled', getExtensionInfo(ext))
198+
} catch (error) {
199+
console.error(error)
200+
}
201+
}
202+
})
203+
}
204+
205+
return result
206+
})
207+
208+
handle('chromeWebstore.completeInstall', async (event, id) => {
209+
// TODO: Implement completion of extension installation
210+
return Result.SUCCESS
211+
})
212+
213+
handle('chromeWebstore.enableAppLauncher', async (event, enable) => {
214+
// TODO: Implement app launcher enable/disable
215+
return true
216+
})
217+
218+
handle('chromeWebstore.getBrowserLogin', async () => {
219+
// TODO: Implement getting browser login
220+
return ''
221+
})
222+
handle('chromeWebstore.getExtensionStatus', async (_event, id, manifestJson) => {
223+
const manifest = JSON.parse(manifestJson)
224+
return getExtensionInstallStatus(webStoreState, id, manifest)
225+
})
226+
227+
handle('chromeWebstore.getFullChromeVersion', async () => {
228+
return {
229+
version_number: process.versions.chrome,
230+
app_name: app.getName(),
231+
}
232+
})
233+
234+
handle('chromeWebstore.getIsLauncherEnabled', async () => {
235+
// TODO: Implement checking if launcher is enabled
236+
return true
237+
})
238+
239+
handle('chromeWebstore.getMV2DeprecationStatus', async () => {
240+
return MV2DeprecationStatus.INACTIVE
241+
})
242+
243+
handle('chromeWebstore.getReferrerChain', async () => {
244+
// TODO: Implement getting referrer chain
245+
return 'EgIIAA=='
246+
})
247+
248+
handle('chromeWebstore.getStoreLogin', async () => {
249+
// TODO: Implement getting store login
250+
return ''
251+
})
252+
253+
handle('chromeWebstore.getWebGLStatus', async () => {
254+
await app.getGPUInfo('basic')
255+
const features = app.getGPUFeatureStatus()
256+
return features.webgl.startsWith('enabled')
257+
? WebGlStatus.WEBGL_ALLOWED
258+
: WebGlStatus.WEBGL_BLOCKED
259+
})
260+
261+
handle('chromeWebstore.install', async (event, id, silentInstall) => {
262+
// TODO: Implement extension installation
263+
return Result.SUCCESS
264+
})
265+
266+
handle('chromeWebstore.isInIncognitoMode', async () => {
267+
// TODO: Implement incognito mode check
268+
return false
269+
})
270+
271+
handle('chromeWebstore.isPendingCustodianApproval', async (event, id) => {
272+
// TODO: Implement custodian approval check
273+
return false
274+
})
275+
276+
handle('chromeWebstore.setStoreLogin', async (event, login) => {
277+
// TODO: Implement setting store login
278+
return true
279+
})
280+
281+
handle('chrome.runtime.getManifest', async () => {
282+
// TODO: Implement getting extension manifest
283+
return {}
284+
})
285+
286+
handle('chrome.management.getAll', async (event) => {
287+
const extensions = webStoreState.session.getAllExtensions()
288+
return extensions.map(getExtensionInfo)
289+
})
290+
291+
handle('chrome.management.setEnabled', async (event, id, enabled) => {
292+
// TODO: Implement enabling/disabling extension
293+
return true
294+
})
295+
296+
handle(
297+
'chrome.management.uninstall',
298+
async (event, id, options: { showConfirmDialog: boolean }) => {
299+
if (options?.showConfirmDialog) {
300+
// TODO: confirmation dialog
301+
}
302+
303+
try {
304+
await uninstallExtension(webStoreState, id)
305+
queueMicrotask(() => {
306+
event.sender.send('chrome.management.onUninstalled', id)
307+
})
308+
return Result.SUCCESS
309+
} catch (error) {
310+
console.error(error)
311+
return Result.UNKNOWN_ERROR
312+
}
313+
},
314+
)
315+
}

0 commit comments

Comments
 (0)