Skip to content

Commit 3b27e1c

Browse files
committed
initial electron-chrome-web-store
1 parent 25de153 commit 3b27e1c

File tree

13 files changed

+796
-2
lines changed

13 files changed

+796
-2
lines changed

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
"workspaces": [
77
"packages/shell",
88
"packages/electron-chrome-extensions",
9-
"packages/electron-chrome-context-menu"
9+
"packages/electron-chrome-context-menu",
10+
"packages/electron-chrome-web-store"
1011
],
1112
"scripts": {
12-
"build": "yarn run build:context-menu && yarn run build:extensions && yarn run build:shell",
13+
"build": "yarn run build:context-menu && yarn run build:chrome-web-store && yarn run build:extensions && yarn run build:shell",
14+
"build:chrome-web-store": "yarn --cwd ./packages/electron-chrome-web-store build",
1315
"build:context-menu": "yarn --cwd ./packages/electron-chrome-context-menu build",
1416
"build:extensions": "yarn --cwd ./packages/electron-chrome-extensions build",
1517
"build:shell": "yarn --cwd ./packages/shell build",
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dist
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "electron-chrome-web-store",
3+
"version": "0.0.1",
4+
"description": "Download extensions from the Chrome Web Store in Electron",
5+
"main": "dist/browser/index.js",
6+
"scripts": {
7+
"build": "tsc",
8+
"test": "echo \"Error: no test specified\" && exit 1"
9+
},
10+
"keywords": [
11+
"chrome",
12+
"web",
13+
"store",
14+
"webstore",
15+
"extensions"
16+
],
17+
"author": "Samuel Maddock",
18+
"license": "ISC",
19+
"devDependencies": {
20+
"typescript": "^5.6.3"
21+
},
22+
"dependencies": {
23+
"adm-zip": "^0.5.16"
24+
}
25+
}
Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
import { app, ipcMain, net, BrowserWindow, Session } from 'electron'
2+
import * as path from 'path'
3+
import * as fs from 'fs'
4+
import { Readable } from 'stream'
5+
6+
const AdmZip = require('adm-zip')
7+
8+
const ExtensionInstallStatus = {
9+
BLACKLISTED: 'blacklisted',
10+
BLOCKED_BY_POLICY: 'blocked_by_policy',
11+
CAN_REQUEST: 'can_request',
12+
CORRUPTED: 'corrupted',
13+
CUSTODIAN_APPROVAL_REQUIRED: 'custodian_approval_required',
14+
CUSTODIAN_APPROVAL_REQUIRED_FOR_INSTALLATION: 'custodian_approval_required_for_installation',
15+
DEPRECATED_MANIFEST_VERSION: 'deprecated_manifest_version',
16+
DISABLED: 'disabled',
17+
ENABLED: 'enabled',
18+
FORCE_INSTALLED: 'force_installed',
19+
INSTALLABLE: 'installable',
20+
REQUEST_PENDING: 'request_pending',
21+
TERMINATED: 'terminated',
22+
}
23+
24+
const MV2DeprecationStatus = {
25+
INACTIVE: 'inactive',
26+
SOFT_DISABLE: 'soft_disable',
27+
WARNING: 'warning',
28+
}
29+
30+
const Result = {
31+
ALREADY_INSTALLED: 'already_installed',
32+
BLACKLISTED: 'blacklisted',
33+
BLOCKED_BY_POLICY: 'blocked_by_policy',
34+
BLOCKED_FOR_CHILD_ACCOUNT: 'blocked_for_child_account',
35+
FEATURE_DISABLED: 'feature_disabled',
36+
ICON_ERROR: 'icon_error',
37+
INSTALL_ERROR: 'install_error',
38+
INSTALL_IN_PROGRESS: 'install_in_progress',
39+
INVALID_ICON_URL: 'invalid_icon_url',
40+
INVALID_ID: 'invalid_id',
41+
LAUNCH_IN_PROGRESS: 'launch_in_progress',
42+
MANIFEST_ERROR: 'manifest_error',
43+
MISSING_DEPENDENCIES: 'missing_dependencies',
44+
SUCCESS: 'success',
45+
UNKNOWN_ERROR: 'unknown_error',
46+
UNSUPPORTED_EXTENSION_TYPE: 'unsupported_extension_type',
47+
USER_CANCELLED: 'user_cancelled',
48+
USER_GESTURE_REQUIRED: 'user_gesture_required',
49+
}
50+
51+
const WebGlStatus = {
52+
WEBGL_ALLOWED: 'webgl_allowed',
53+
WEBGL_BLOCKED: 'webgl_blocked',
54+
}
55+
56+
export function setupChromeWebStore(session: Session, modulePath: string = __dirname) {
57+
const preloadPath = path.join(modulePath, 'dist/renderer/web-store-api.js')
58+
59+
// Add preload script to session
60+
session.setPreloads([...session.getPreloads(), preloadPath])
61+
interface InstallDetails {
62+
id: string
63+
manifest: string
64+
localizedName: string
65+
esbAllowlist: boolean
66+
iconUrl: string
67+
}
68+
69+
ipcMain.handle('chromeWebstore.beginInstall', async (event, details: InstallDetails) => {
70+
try {
71+
const manifest: chrome.runtime.Manifest = JSON.parse(details.manifest)
72+
const installVersion = manifest.version;
73+
74+
// Check if extension is already loaded in session and remove it
75+
const extensions = session.getAllExtensions()
76+
const existingExt = extensions.find(ext => ext.id === details.id)
77+
if (existingExt) {
78+
await session.removeExtension(details.id)
79+
}
80+
81+
// Get user data directory and ensure extensions folder exists
82+
const userDataPath = app.getPath('userData')
83+
const extensionsPath = path.join(userDataPath, 'Extensions')
84+
await fs.promises.mkdir(extensionsPath, { recursive: true })
85+
86+
// Create extension directory
87+
const extensionDir = path.join(extensionsPath, details.id)
88+
89+
// Remove existing directory if it exists
90+
await fs.promises.rm(extensionDir, { recursive: true, force: true })
91+
await fs.promises.mkdir(extensionDir, { recursive: true })
92+
93+
// Download extension from Chrome Web Store
94+
const chromeVersion = process.versions.chrome;
95+
const response = await net.fetch(
96+
`https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${details.id}%26uc&prodversion=${chromeVersion}`
97+
)
98+
99+
if (!response.ok) {
100+
throw new Error('Failed to download extension')
101+
}
102+
103+
// Save extension file
104+
const extensionFile = path.join(extensionDir, 'extension.crx')
105+
const fileStream = fs.createWriteStream(extensionFile)
106+
107+
// Convert ReadableStream to Node stream and pipe to file
108+
const readableStream = Readable.fromWeb(response.body as any)
109+
await new Promise((resolve, reject) => {
110+
readableStream.pipe(fileStream)
111+
readableStream.on('error', reject)
112+
fileStream.on('finish', resolve)
113+
})
114+
115+
// Unpack extension
116+
const unpackedDir = path.join(extensionDir, installVersion)
117+
await fs.promises.mkdir(unpackedDir, { recursive: true })
118+
// Use crx-parser to extract contents
119+
const crxBuffer = await fs.promises.readFile(extensionFile)
120+
121+
interface CrxInfo {
122+
version: number;
123+
header: Buffer;
124+
contents: Buffer;
125+
}
126+
127+
// Parse CRX header and extract contents
128+
function parseCrx(buffer: Buffer): CrxInfo {
129+
// CRX3 magic number: 'Cr24'
130+
const magicNumber = buffer.toString('utf8', 0, 4)
131+
if (magicNumber !== 'Cr24') {
132+
throw new Error('Invalid CRX format')
133+
}
134+
135+
// CRX3 format has version = 3 and header size at bytes 8-12
136+
const version = buffer.readUInt32LE(4)
137+
const headerSize = buffer.readUInt32LE(8)
138+
139+
// Extract header and contents
140+
const header = buffer.subarray(16, 16 + headerSize)
141+
const contents = buffer.subarray(16 + headerSize)
142+
143+
return {
144+
version,
145+
header,
146+
contents
147+
}
148+
}
149+
150+
// Extract CRX contents to directory
151+
async function extractCrx(crx: CrxInfo, destPath: string) {
152+
// Create zip file from contents
153+
const zip = new AdmZip(crx.contents)
154+
155+
// Extract zip to destination
156+
zip.extractAllTo(destPath, true)
157+
}
158+
159+
const crx = await parseCrx(crxBuffer)
160+
console.log('crx', crx)
161+
await extractCrx(crx, unpackedDir)
162+
163+
// Load extension into session
164+
await session.loadExtension(unpackedDir)
165+
166+
return Result.SUCCESS
167+
} catch (error) {
168+
console.error('Extension installation failed:', error)
169+
return Result.INSTALL_ERROR
170+
}
171+
})
172+
173+
ipcMain.handle('chromeWebstore.completeInstall', async (event, id) => {
174+
// TODO: Implement completion of extension installation
175+
return Result.SUCCESS
176+
})
177+
178+
ipcMain.handle('chromeWebstore.enableAppLauncher', async (event, enable) => {
179+
// TODO: Implement app launcher enable/disable
180+
return true
181+
})
182+
183+
ipcMain.handle('chromeWebstore.getBrowserLogin', async () => {
184+
// TODO: Implement getting browser login
185+
return ''
186+
})
187+
ipcMain.handle('chromeWebstore.getExtensionStatus', async (event, id, manifestJson) => {
188+
console.log('webstorePrivate.getExtensionStatus', JSON.stringify({ id }))
189+
const extensions = session.getAllExtensions()
190+
const extension = extensions.find((ext) => ext.id === id)
191+
192+
if (!extension) {
193+
console.log(extensions)
194+
console.log('webstorePrivate.getExtensionStatus result:', id, ExtensionInstallStatus.INSTALLABLE)
195+
return ExtensionInstallStatus.INSTALLABLE
196+
}
197+
198+
if (extension.manifest.disabled) {
199+
console.log('webstorePrivate.getExtensionStatus result:', id, ExtensionInstallStatus.DISABLED)
200+
return ExtensionInstallStatus.DISABLED
201+
}
202+
203+
console.log('webstorePrivate.getExtensionStatus result:', id, ExtensionInstallStatus.ENABLED)
204+
return ExtensionInstallStatus.ENABLED
205+
})
206+
207+
ipcMain.handle('chromeWebstore.getFullChromeVersion', async () => {
208+
return { version_number: process.versions.chrome }
209+
})
210+
211+
ipcMain.handle('chromeWebstore.getIsLauncherEnabled', async () => {
212+
// TODO: Implement checking if launcher is enabled
213+
return true
214+
})
215+
216+
ipcMain.handle('chromeWebstore.getMV2DeprecationStatus', async () => {
217+
// TODO: Implement MV2 deprecation status check
218+
return MV2DeprecationStatus.INACTIVE
219+
})
220+
221+
ipcMain.handle('chromeWebstore.getReferrerChain', async () => {
222+
// TODO: Implement getting referrer chain
223+
return 'EgIIAA=='
224+
})
225+
226+
ipcMain.handle('chromeWebstore.getStoreLogin', async () => {
227+
// TODO: Implement getting store login
228+
return ''
229+
})
230+
231+
ipcMain.handle('chromeWebstore.getWebGLStatus', async () => {
232+
// TODO: Implement WebGL status check
233+
return WebGlStatus.WEBGL_ALLOWED
234+
})
235+
236+
ipcMain.handle('chromeWebstore.install', async (event, id, silentInstall) => {
237+
// TODO: Implement extension installation
238+
return Result.SUCCESS
239+
})
240+
241+
ipcMain.handle('chromeWebstore.isInIncognitoMode', async () => {
242+
// TODO: Implement incognito mode check
243+
return false
244+
})
245+
246+
ipcMain.handle('chromeWebstore.isPendingCustodianApproval', async (event, id) => {
247+
// TODO: Implement custodian approval check
248+
return false
249+
})
250+
251+
ipcMain.handle('chromeWebstore.setStoreLogin', async (event, login) => {
252+
// TODO: Implement setting store login
253+
return true
254+
})
255+
256+
ipcMain.handle('chrome.runtime.getManifest', async () => {
257+
// TODO: Implement getting extension manifest
258+
return {}
259+
})
260+
261+
ipcMain.handle('chrome.management.getAll', async (event) => {
262+
const extensions = session.getAllExtensions()
263+
264+
return extensions.map((ext) => {
265+
const manifest: chrome.runtime.Manifest = ext.manifest
266+
return {
267+
description: manifest.description || '',
268+
enabled: !manifest.disabled,
269+
homepageUrl: manifest.homepage_url || '',
270+
hostPermissions: manifest.host_permissions || [],
271+
icons: Object.entries(manifest?.icons || {}).map(([size, url]) => ({
272+
size: parseInt(size),
273+
url: `chrome://extension-icon/${ext.id}/${size}/0`,
274+
})),
275+
id: ext.id,
276+
installType: 'normal',
277+
isApp: !!manifest.app,
278+
mayDisable: true,
279+
name: manifest.name,
280+
offlineEnabled: !!manifest.offline_enabled,
281+
optionsUrl: manifest.options_page
282+
? `chrome-extension://${ext.id}/${manifest.options_page}`
283+
: '',
284+
permissions: manifest.permissions || [],
285+
shortName: manifest.short_name || manifest.name,
286+
type: manifest.app ? 'app' : 'extension',
287+
updateUrl: manifest.update_url || '',
288+
version: manifest.version,
289+
}
290+
})
291+
})
292+
293+
ipcMain.handle('chrome.management.setEnabled', async (event, id, enabled) => {
294+
// TODO: Implement enabling/disabling extension
295+
return true
296+
})
297+
298+
ipcMain.handle('chrome.management.uninstall', async (event, id, options) => {
299+
// TODO: Implement uninstalling extension
300+
return true
301+
})
302+
303+
// Handle extension install/uninstall events
304+
function emitExtensionEvent(eventName: string) {
305+
BrowserWindow.getAllWindows().forEach((window) => {
306+
window.webContents.send(`chrome.management.${eventName}`)
307+
})
308+
}
309+
}

0 commit comments

Comments
 (0)