diff --git a/package.json b/package.json index 2daafe82..492d63a2 100644 --- a/package.json +++ b/package.json @@ -6,10 +6,12 @@ "workspaces": [ "packages/shell", "packages/electron-chrome-extensions", - "packages/electron-chrome-context-menu" + "packages/electron-chrome-context-menu", + "packages/electron-chrome-web-store" ], "scripts": { - "build": "yarn run build:context-menu && yarn run build:extensions && yarn run build:shell", + "build": "yarn run build:context-menu && yarn run build:chrome-web-store && yarn run build:extensions && yarn run build:shell", + "build:chrome-web-store": "yarn --cwd ./packages/electron-chrome-web-store build", "build:context-menu": "yarn --cwd ./packages/electron-chrome-context-menu build", "build:extensions": "yarn --cwd ./packages/electron-chrome-extensions build", "build:shell": "yarn --cwd ./packages/shell build", diff --git a/packages/electron-chrome-web-store/.gitignore b/packages/electron-chrome-web-store/.gitignore new file mode 100644 index 00000000..53c37a16 --- /dev/null +++ b/packages/electron-chrome-web-store/.gitignore @@ -0,0 +1 @@ +dist \ No newline at end of file diff --git a/packages/electron-chrome-web-store/package.json b/packages/electron-chrome-web-store/package.json new file mode 100644 index 00000000..7e47cae5 --- /dev/null +++ b/packages/electron-chrome-web-store/package.json @@ -0,0 +1,25 @@ +{ + "name": "electron-chrome-web-store", + "version": "0.0.1", + "description": "Download extensions from the Chrome Web Store in Electron", + "main": "dist/browser/index.js", + "scripts": { + "build": "tsc", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "chrome", + "web", + "store", + "webstore", + "extensions" + ], + "author": "Samuel Maddock", + "license": "ISC", + "devDependencies": { + "typescript": "^5.6.3" + }, + "dependencies": { + "adm-zip": "^0.5.16" + } +} diff --git a/packages/electron-chrome-web-store/src/browser/index.ts b/packages/electron-chrome-web-store/src/browser/index.ts new file mode 100644 index 00000000..b373b3cc --- /dev/null +++ b/packages/electron-chrome-web-store/src/browser/index.ts @@ -0,0 +1,309 @@ +import { app, ipcMain, net, BrowserWindow, Session } from 'electron' +import * as path from 'path' +import * as fs from 'fs' +import { Readable } from 'stream' + +const AdmZip = require('adm-zip') + +const ExtensionInstallStatus = { + BLACKLISTED: 'blacklisted', + BLOCKED_BY_POLICY: 'blocked_by_policy', + CAN_REQUEST: 'can_request', + CORRUPTED: 'corrupted', + CUSTODIAN_APPROVAL_REQUIRED: 'custodian_approval_required', + CUSTODIAN_APPROVAL_REQUIRED_FOR_INSTALLATION: 'custodian_approval_required_for_installation', + DEPRECATED_MANIFEST_VERSION: 'deprecated_manifest_version', + DISABLED: 'disabled', + ENABLED: 'enabled', + FORCE_INSTALLED: 'force_installed', + INSTALLABLE: 'installable', + REQUEST_PENDING: 'request_pending', + TERMINATED: 'terminated', +} + +const MV2DeprecationStatus = { + INACTIVE: 'inactive', + SOFT_DISABLE: 'soft_disable', + WARNING: 'warning', +} + +const Result = { + ALREADY_INSTALLED: 'already_installed', + BLACKLISTED: 'blacklisted', + BLOCKED_BY_POLICY: 'blocked_by_policy', + BLOCKED_FOR_CHILD_ACCOUNT: 'blocked_for_child_account', + FEATURE_DISABLED: 'feature_disabled', + ICON_ERROR: 'icon_error', + INSTALL_ERROR: 'install_error', + INSTALL_IN_PROGRESS: 'install_in_progress', + INVALID_ICON_URL: 'invalid_icon_url', + INVALID_ID: 'invalid_id', + LAUNCH_IN_PROGRESS: 'launch_in_progress', + MANIFEST_ERROR: 'manifest_error', + MISSING_DEPENDENCIES: 'missing_dependencies', + SUCCESS: 'success', + UNKNOWN_ERROR: 'unknown_error', + UNSUPPORTED_EXTENSION_TYPE: 'unsupported_extension_type', + USER_CANCELLED: 'user_cancelled', + USER_GESTURE_REQUIRED: 'user_gesture_required', +} + +const WebGlStatus = { + WEBGL_ALLOWED: 'webgl_allowed', + WEBGL_BLOCKED: 'webgl_blocked', +} + +export function setupChromeWebStore(session: Session, modulePath: string = __dirname) { + const preloadPath = path.join(modulePath, 'dist/renderer/web-store-api.js') + + // Add preload script to session + session.setPreloads([...session.getPreloads(), preloadPath]) + interface InstallDetails { + id: string + manifest: string + localizedName: string + esbAllowlist: boolean + iconUrl: string + } + + ipcMain.handle('chromeWebstore.beginInstall', async (event, details: InstallDetails) => { + try { + const manifest: chrome.runtime.Manifest = JSON.parse(details.manifest) + const installVersion = manifest.version; + + // Check if extension is already loaded in session and remove it + const extensions = session.getAllExtensions() + const existingExt = extensions.find(ext => ext.id === details.id) + if (existingExt) { + await session.removeExtension(details.id) + } + + // Get user data directory and ensure extensions folder exists + const userDataPath = app.getPath('userData') + const extensionsPath = path.join(userDataPath, 'Extensions') + await fs.promises.mkdir(extensionsPath, { recursive: true }) + + // Create extension directory + const extensionDir = path.join(extensionsPath, details.id) + + // Remove existing directory if it exists + await fs.promises.rm(extensionDir, { recursive: true, force: true }) + await fs.promises.mkdir(extensionDir, { recursive: true }) + + // Download extension from Chrome Web Store + const chromeVersion = process.versions.chrome; + const response = await net.fetch( + `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${details.id}%26uc&prodversion=${chromeVersion}` + ) + + if (!response.ok) { + throw new Error('Failed to download extension') + } + + // Save extension file + const extensionFile = path.join(extensionDir, 'extension.crx') + const fileStream = fs.createWriteStream(extensionFile) + + // Convert ReadableStream to Node stream and pipe to file + const readableStream = Readable.fromWeb(response.body as any) + await new Promise((resolve, reject) => { + readableStream.pipe(fileStream) + readableStream.on('error', reject) + fileStream.on('finish', resolve) + }) + + // Unpack extension + const unpackedDir = path.join(extensionDir, installVersion) + await fs.promises.mkdir(unpackedDir, { recursive: true }) + // Use crx-parser to extract contents + const crxBuffer = await fs.promises.readFile(extensionFile) + + interface CrxInfo { + version: number; + header: Buffer; + contents: Buffer; + } + + // Parse CRX header and extract contents + function parseCrx(buffer: Buffer): CrxInfo { + // CRX3 magic number: 'Cr24' + const magicNumber = buffer.toString('utf8', 0, 4) + if (magicNumber !== 'Cr24') { + throw new Error('Invalid CRX format') + } + + // CRX3 format has version = 3 and header size at bytes 8-12 + const version = buffer.readUInt32LE(4) + const headerSize = buffer.readUInt32LE(8) + + // Extract header and contents + const header = buffer.subarray(16, 16 + headerSize) + const contents = buffer.subarray(16 + headerSize) + + return { + version, + header, + contents + } + } + + // Extract CRX contents to directory + async function extractCrx(crx: CrxInfo, destPath: string) { + // Create zip file from contents + const zip = new AdmZip(crx.contents) + + // Extract zip to destination + zip.extractAllTo(destPath, true) + } + + const crx = await parseCrx(crxBuffer) + console.log('crx', crx) + await extractCrx(crx, unpackedDir) + + // Load extension into session + await session.loadExtension(unpackedDir) + + return Result.SUCCESS + } catch (error) { + console.error('Extension installation failed:', error) + return Result.INSTALL_ERROR + } + }) + + ipcMain.handle('chromeWebstore.completeInstall', async (event, id) => { + // TODO: Implement completion of extension installation + return Result.SUCCESS + }) + + ipcMain.handle('chromeWebstore.enableAppLauncher', async (event, enable) => { + // TODO: Implement app launcher enable/disable + return true + }) + + ipcMain.handle('chromeWebstore.getBrowserLogin', async () => { + // TODO: Implement getting browser login + return '' + }) + ipcMain.handle('chromeWebstore.getExtensionStatus', async (event, id, manifestJson) => { + console.log('webstorePrivate.getExtensionStatus', JSON.stringify({ id })) + const extensions = session.getAllExtensions() + const extension = extensions.find((ext) => ext.id === id) + + if (!extension) { + console.log(extensions) + console.log('webstorePrivate.getExtensionStatus result:', id, ExtensionInstallStatus.INSTALLABLE) + return ExtensionInstallStatus.INSTALLABLE + } + + if (extension.manifest.disabled) { + console.log('webstorePrivate.getExtensionStatus result:', id, ExtensionInstallStatus.DISABLED) + return ExtensionInstallStatus.DISABLED + } + + console.log('webstorePrivate.getExtensionStatus result:', id, ExtensionInstallStatus.ENABLED) + return ExtensionInstallStatus.ENABLED + }) + + ipcMain.handle('chromeWebstore.getFullChromeVersion', async () => { + return { version_number: process.versions.chrome } + }) + + ipcMain.handle('chromeWebstore.getIsLauncherEnabled', async () => { + // TODO: Implement checking if launcher is enabled + return true + }) + + ipcMain.handle('chromeWebstore.getMV2DeprecationStatus', async () => { + // TODO: Implement MV2 deprecation status check + return MV2DeprecationStatus.INACTIVE + }) + + ipcMain.handle('chromeWebstore.getReferrerChain', async () => { + // TODO: Implement getting referrer chain + return 'EgIIAA==' + }) + + ipcMain.handle('chromeWebstore.getStoreLogin', async () => { + // TODO: Implement getting store login + return '' + }) + + ipcMain.handle('chromeWebstore.getWebGLStatus', async () => { + // TODO: Implement WebGL status check + return WebGlStatus.WEBGL_ALLOWED + }) + + ipcMain.handle('chromeWebstore.install', async (event, id, silentInstall) => { + // TODO: Implement extension installation + return Result.SUCCESS + }) + + ipcMain.handle('chromeWebstore.isInIncognitoMode', async () => { + // TODO: Implement incognito mode check + return false + }) + + ipcMain.handle('chromeWebstore.isPendingCustodianApproval', async (event, id) => { + // TODO: Implement custodian approval check + return false + }) + + ipcMain.handle('chromeWebstore.setStoreLogin', async (event, login) => { + // TODO: Implement setting store login + return true + }) + + ipcMain.handle('chrome.runtime.getManifest', async () => { + // TODO: Implement getting extension manifest + return {} + }) + + ipcMain.handle('chrome.management.getAll', async (event) => { + const extensions = session.getAllExtensions() + + return extensions.map((ext) => { + const manifest: chrome.runtime.Manifest = ext.manifest + return { + description: manifest.description || '', + enabled: !manifest.disabled, + homepageUrl: manifest.homepage_url || '', + hostPermissions: manifest.host_permissions || [], + icons: Object.entries(manifest?.icons || {}).map(([size, url]) => ({ + size: parseInt(size), + url: `chrome://extension-icon/${ext.id}/${size}/0`, + })), + id: ext.id, + installType: 'normal', + isApp: !!manifest.app, + mayDisable: true, + name: manifest.name, + offlineEnabled: !!manifest.offline_enabled, + optionsUrl: manifest.options_page + ? `chrome-extension://${ext.id}/${manifest.options_page}` + : '', + permissions: manifest.permissions || [], + shortName: manifest.short_name || manifest.name, + type: manifest.app ? 'app' : 'extension', + updateUrl: manifest.update_url || '', + version: manifest.version, + } + }) + }) + + ipcMain.handle('chrome.management.setEnabled', async (event, id, enabled) => { + // TODO: Implement enabling/disabling extension + return true + }) + + ipcMain.handle('chrome.management.uninstall', async (event, id, options) => { + // TODO: Implement uninstalling extension + return true + }) + + // Handle extension install/uninstall events + function emitExtensionEvent(eventName: string) { + BrowserWindow.getAllWindows().forEach((window) => { + window.webContents.send(`chrome.management.${eventName}`) + }) + } +} diff --git a/packages/electron-chrome-web-store/src/renderer/usage.js b/packages/electron-chrome-web-store/src/renderer/usage.js new file mode 100644 index 00000000..e8aeb740 --- /dev/null +++ b/packages/electron-chrome-web-store/src/renderer/usage.js @@ -0,0 +1,139 @@ +r c = { + showConfirmDialog: !0 + }; + window.chrome && window.chrome.management && window.chrome.management.uninstall && window.chrome.management.uninstall(a, c, b) + } + ; + + b = b === void 0 ? null : b; + return new Promise(function(c) { + if (!window.chrome || !window.chrome.webstorePrivate || !window.chrome.webstorePrivate.getExtensionStatus) + throw Error("Yc"); + window.chrome.webstorePrivate.getExtensionStatus(a, b, function(d) { + c(d) + }) + + fqa = function() { + return new Promise(function(a) { + window.chrome && window.chrome.management && window.chrome.management.getAll || a([]); + window.chrome.management.getAll(function(b) { + a(b) +a, b)) + } + , Dw = function() { + if (!(window.chrome && window.chrome.runtime && window.chrome.runtime.getManifest && window.chrome.runtime.getManifest())) + throw Error("Ib"); + }; +; + this.Hc = a.service.YB; + this.j = a.service.Vn; + this.Aa = this.chrome.j; + this.oa = _.Hh(b); + this.o = null; +X.bind(this || null)); + this.Da = 0; + this.iJ = a.service.window; + a = this.chrome.window.get(); + a.history && a.history.scrollRestoration && (b = Object.getPrototypeOf(a.history), + b != null && (b + + z0 = function(a, b) { + var c = a.Hc.nh(); + (b.Zd().getMetadata() || {}).XSa || a.chrome.qb.j("Page loaded.", "assertive"); + a.Hc.j && g0(a.Hc); + d0(a.Hc, b, { +hrome, d, b)) { + c || this.Yi.tB(b); + b = a; + if (c = this.chrome.Ja(b)) { + for (d = 0; b && b !== c; ) + d += b.offse + + var P6a, Q6a, R6a; + _.R8 = function() { + return !!(window.chrome && window.chrome.management && window.chrome.webstorePrivate && window.chrome.webstorePrivate.beginInstallWithManifest3) + } + ; + + _.S8 = function() { + return new Promise(function(a) { + window.chrome && window.chrome.webstorePrivate && window.chrome.webstorePrivate.isInIncognitoMode || a(!1); + window.chrome.webstorePrivate.isInIncognitoMode(function(b) { + a(b) + + return _.H(function(a) { + return a.return(new Promise(function(b) { + window.chrome && window.chrome.webstorePrivate && window.chrome.webstorePrivate.getFullChromeVersion || b(""); + window.chrome.webstorePrivate.getFullChromeVersion(function(c) { + b(c.version_number) + + case "\u00010\u0001": + a.open("a", "J5jx0e"); + a.ua(WYa || (WYa = "class Z6CGhd href https://developer.chrome.com/docs/webstore/program-policies/limited-use/ target _blank".split(" "))); + a.qa(); + break; + + var x1a = function() { + return new Promise(function(a, b) { + window.chrome && window.chrome.webstorePrivate && window.chrome.webstorePrivate.getReferrerChain || b(""); + window.chrome.webstorePrivate.getReferrerChain(function(c) { + a(c) +b(0); + break; + case 2: + if (!window.chrome || !window.chrome.management || !window.chrome.management.setEnabled) + throw Error("Yc"); + d = window.chrome.management.setEnabled(a.itemId, !0); + return _.G(c, d, 4); + case 4: + + , d9 = function() { + var a = _.eb.apply(0, arguments); + if (!window.chrome || !window.chrome.webstorePrivate || !window.chrome.webstorePrivate.beginInstallWithManifest3) + throw Error("Yc"); + window.chrome.webstorePrivate.beginInstallWithManifest3.apply(window.chrome.webstorePrivate, _.wi(a)) + } + , C7a = function() { + var a = _.eb.apply(0, arguments); + if (!window.chrome || !window.chrome.webstorePrivate || !window.chrome.webstorePrivate.completeInstall) + throw Error("Yc"); + window.chrome.webstorePrivate.completeInstall.apply(window.chrome.webstorePrivate, _.wi(a)) + } + , e9 = function( + b.Ga && (_.lP(b.j), + b.Ga = !1) + }); + window.chrome && window.chrome.management && (chrome.management.onInstalled.addListener(this.Aa.bind(this)), + chrome.management.onUninstalled.addListener(this.Aa.bind(this)), + _.gj(this, function() { + chrome.management.onInstalled.removeListener(b.Aa.bind(b)); + chrome.management.onUninstalled.removeListener(b.Aa.bind(b)) + })); + this +ction() { + a.v(); + var b, c; + if (!((b = chrome.runtime) == null ? 0 : (c = b.lastError) == null ? 0 : c.message)) { + var d; + (b = (d = a.zg.data.Cb.j()) == null ? void 0 : d.getTitle()) && a.oa.j({ + label: b + " has been removed from Chrome.", + Va: "U9Cmxb" + }) +failed to install due to " + b : b + } + , i9 = function() { + return window.chrome && (chrome.extension && chrome.extension.lastError && chrome.extension.lastError.message || chrome.runtime && chrome.runtime.lastError && chrome.runtime.lastError.message) || void 0 + }; + _.Q(g9.prototype, "HN + = b || d.has("debugReviews"); + f.o = g; + try { + !a.o && window.chrome && window.chrome.management && (chrome.management.onInstalled.addListener(a.v), + chrome.management.onUninstalled.addListener(a.oa), + a + + ; + C7.prototype.Vf = function() { + this.o && window.chrome && window.chrome.management && (chrome.management.onInstalled.removeListener(this.v), + chrome.management.onUninstalled.removeListener(this.oa)) + } \ No newline at end of file diff --git a/packages/electron-chrome-web-store/src/renderer/web-store-api.ts b/packages/electron-chrome-web-store/src/renderer/web-store-api.ts new file mode 100644 index 00000000..9beb5904 --- /dev/null +++ b/packages/electron-chrome-web-store/src/renderer/web-store-api.ts @@ -0,0 +1,279 @@ +import { contextBridge, ipcRenderer, webFrame } from 'electron' + +const ExtensionInstallStatus = { + BLACKLISTED: 'blacklisted', + BLOCKED_BY_POLICY: 'blocked_by_policy', + CAN_REQUEST: 'can_request', + CORRUPTED: 'corrupted', + CUSTODIAN_APPROVAL_REQUIRED: 'custodian_approval_required', + CUSTODIAN_APPROVAL_REQUIRED_FOR_INSTALLATION: 'custodian_approval_required_for_installation', + DEPRECATED_MANIFEST_VERSION: 'deprecated_manifest_version', + DISABLED: 'disabled', + ENABLED: 'enabled', + FORCE_INSTALLED: 'force_installed', + INSTALLABLE: 'installable', + REQUEST_PENDING: 'request_pending', + TERMINATED: 'terminated', +} + +const MV2DeprecationStatus = { + INACTIVE: 'inactive', + SOFT_DISABLE: 'soft_disable', + WARNING: 'warning', +} + +const Result = { + ALREADY_INSTALLED: 'already_installed', + BLACKLISTED: 'blacklisted', + BLOCKED_BY_POLICY: 'blocked_by_policy', + BLOCKED_FOR_CHILD_ACCOUNT: 'blocked_for_child_account', + FEATURE_DISABLED: 'feature_disabled', + ICON_ERROR: 'icon_error', + INSTALL_ERROR: 'install_error', + INSTALL_IN_PROGRESS: 'install_in_progress', + INVALID_ICON_URL: 'invalid_icon_url', + INVALID_ID: 'invalid_id', + LAUNCH_IN_PROGRESS: 'launch_in_progress', + MANIFEST_ERROR: 'manifest_error', + MISSING_DEPENDENCIES: 'missing_dependencies', + SUCCESS: 'success', + UNKNOWN_ERROR: 'unknown_error', + UNSUPPORTED_EXTENSION_TYPE: 'unsupported_extension_type', + USER_CANCELLED: 'user_cancelled', + USER_GESTURE_REQUIRED: 'user_gesture_required', +} + +const WebGlStatus = { + WEBGL_ALLOWED: 'webgl_allowed', + WEBGL_BLOCKED: 'webgl_blocked', +} + +interface WebstorePrivate { + ExtensionInstallStatus: typeof ExtensionInstallStatus + MV2DeprecationStatus: typeof MV2DeprecationStatus + Result: typeof Result + WebGlStatus: typeof WebGlStatus + + beginInstallWithManifest3: (details: unknown, callback?: (result: string) => void) => Promise + completeInstall: (id: string, callback?: (result: string) => void) => Promise + enableAppLauncher: (enable: boolean, callback?: (result: boolean) => void) => Promise + getBrowserLogin: (callback?: (result: string) => void) => Promise + getExtensionStatus: ( + id: string, + manifestJson: string, + callback?: (status: string) => void + ) => Promise + getFullChromeVersion: (callback?: (result: string) => void) => Promise<{ version_number: string }> + getIsLauncherEnabled: (callback?: (result: boolean) => void) => Promise + getMV2DeprecationStatus: (callback?: (result: string) => void) => Promise + getReferrerChain: (callback?: (result: unknown[]) => void) => Promise + getStoreLogin: (callback?: (result: string) => void) => Promise + getWebGLStatus: (callback?: (result: string) => void) => Promise + install: (id: string, silentInstall: boolean, callback?: (result: string) => void) => Promise + isInIncognitoMode: (callback?: (result: boolean) => void) => Promise + isPendingCustodianApproval: (id: string, callback?: (result: boolean) => void) => Promise + setStoreLogin: (login: string, callback?: (result: boolean) => void) => Promise +} + +function setupChromeWebStoreApi() { + /** + * Implementation of Chrome's webstorePrivate for Electron. + */ + const electronWebstore: WebstorePrivate = { + ExtensionInstallStatus, + MV2DeprecationStatus, + Result, + WebGlStatus, + + beginInstallWithManifest3: async (details, callback) => { + console.log('webstorePrivate.beginInstallWithManifest3', details) + const result = await ipcRenderer.invoke('chromeWebstore.beginInstall', details) + console.log('webstorePrivate.beginInstallWithManifest3 result:', result) + if (callback) callback(result) + return result + }, + + completeInstall: async (id, callback) => { + console.log('webstorePrivate.completeInstall', id) + const result = await ipcRenderer.invoke('chromeWebstore.completeInstall', id) + console.log('webstorePrivate.completeInstall result:', result) + if (callback) callback(result) + return result + }, + + enableAppLauncher: async (enable, callback) => { + console.log('webstorePrivate.enableAppLauncher', enable) + const result = await ipcRenderer.invoke('chromeWebstore.enableAppLauncher', enable) + console.log('webstorePrivate.enableAppLauncher result:', result) + if (callback) callback(result) + return result + }, + + getBrowserLogin: async (callback) => { + console.log('webstorePrivate.getBrowserLogin called') + const result = await ipcRenderer.invoke('chromeWebstore.getBrowserLogin') + console.log('webstorePrivate.getBrowserLogin result:', result) + if (callback) callback(result) + return result + }, + + getExtensionStatus: async (id, manifestJson, callback) => { + console.log('webstorePrivate.getExtensionStatus', id, { id, manifestJson, callback }) + const result = await ipcRenderer.invoke('chromeWebstore.getExtensionStatus', id, manifestJson) + console.log('webstorePrivate.getExtensionStatus result:', id, result) + if (callback) callback(result) + return result + }, + + getFullChromeVersion: async (callback) => { + console.log('webstorePrivate.getFullChromeVersion called') + const result = await ipcRenderer.invoke('chromeWebstore.getFullChromeVersion') + console.log('webstorePrivate.getFullChromeVersion result:', result) + if (callback) callback(result) + return result + }, + + getIsLauncherEnabled: async (callback) => { + console.log('webstorePrivate.getIsLauncherEnabled called') + const result = await ipcRenderer.invoke('chromeWebstore.getIsLauncherEnabled') + console.log('webstorePrivate.getIsLauncherEnabled result:', result) + if (callback) callback(result) + return result + }, + + getMV2DeprecationStatus: async (callback) => { + console.log('webstorePrivate.getMV2DeprecationStatus called') + const result = await ipcRenderer.invoke('chromeWebstore.getMV2DeprecationStatus') + console.log('webstorePrivate.getMV2DeprecationStatus result:', result) + if (callback) callback(result) + return result + }, + + getReferrerChain: async (callback) => { + console.log('webstorePrivate.getReferrerChain called') + const result = await ipcRenderer.invoke('chromeWebstore.getReferrerChain') + console.log('webstorePrivate.getReferrerChain result:', result) + if (callback) callback(result) + return result + }, + + getStoreLogin: async (callback) => { + console.log('webstorePrivate.getStoreLogin called') + const result = await ipcRenderer.invoke('chromeWebstore.getStoreLogin') + console.log('webstorePrivate.getStoreLogin result:', result) + if (callback) callback(result) + return result + }, + + getWebGLStatus: async (callback) => { + console.log('webstorePrivate.getWebGLStatus called') + const result = await ipcRenderer.invoke('chromeWebstore.getWebGLStatus') + console.log('webstorePrivate.getWebGLStatus result:', result) + if (callback) callback(result) + return result + }, + + install: async (id, silentInstall, callback) => { + console.log('webstorePrivate.install', { id, silentInstall }) + const result = await ipcRenderer.invoke('chromeWebstore.install', id, silentInstall) + console.log('webstorePrivate.install result:', result) + if (callback) callback(result) + return result + }, + + isInIncognitoMode: async (callback) => { + console.log('webstorePrivate.isInIncognitoMode called') + const result = await ipcRenderer.invoke('chromeWebstore.isInIncognitoMode') + console.log('webstorePrivate.isInIncognitoMode result:', result) + if (callback) callback(result) + return result + }, + + isPendingCustodianApproval: async (id, callback) => { + console.log('webstorePrivate.isPendingCustodianApproval', id) + const result = await ipcRenderer.invoke('chromeWebstore.isPendingCustodianApproval', id) + console.log('webstorePrivate.isPendingCustodianApproval result:', result) + if (callback) callback(result) + return result + }, + + setStoreLogin: async (login, callback) => { + console.log('webstorePrivate.setStoreLogin', login) + const result = await ipcRenderer.invoke('chromeWebstore.setStoreLogin', login) + console.log('webstorePrivate.setStoreLogin result:', result) + if (callback) callback(result) + return result + }, + } + + // Expose webstorePrivate API + contextBridge.exposeInMainWorld('electronWebstore', electronWebstore) + // Expose chrome.runtime and chrome.management APIs + const runtime = { + lastError: null, + getManifest: async () => { + console.log('chrome.runtime.getManifest called') + const result = await ipcRenderer.invoke('chrome.runtime.getManifest') + console.log('chrome.runtime.getManifest result:', result) + return result + }, + } + + contextBridge.exposeInMainWorld('electronRuntime', runtime) + + const management = { + onInstalled: { + addListener: (callback: () => void) => { + console.log('chrome.management.onInstalled.addListener called') + ipcRenderer.on('chrome.management.onInstalled', callback) + }, + removeListener: (callback: () => void) => { + console.log('chrome.management.onInstalled.removeListener called') + ipcRenderer.removeListener('chrome.management.onInstalled', callback) + }, + }, + onUninstalled: { + addListener: (callback: () => void) => { + console.log('chrome.management.onUninstalled.addListener called') + ipcRenderer.on('chrome.management.onUninstalled', callback) + }, + removeListener: (callback: () => void) => { + console.log('chrome.management.onUninstalled.removeListener called') + ipcRenderer.removeListener('chrome.management.onUninstalled', callback) + }, + }, + getAll: (callback: (extensions: any[]) => void) => { + console.log('chrome.management.getAll called') + ipcRenderer.invoke('chrome.management.getAll').then((result) => { + console.log('chrome.management.getAll result:', result) + callback(result) + }) + }, + setEnabled: async (id: string, enabled: boolean) => { + console.log('chrome.management.setEnabled', { id, enabled }) + const result = await ipcRenderer.invoke('chrome.management.setEnabled', id, enabled) + console.log('chrome.management.setEnabled result:', result) + return result + }, + uninstall: (id: string, options: { showConfirmDialog: boolean }, callback?: () => void) => { + console.log('chrome.management.uninstall', { id, options }) + ipcRenderer.invoke('chrome.management.uninstall', id, options).then((result) => { + console.log('chrome.management.uninstall result:', result) + if (callback) callback() + }) + }, + } + + contextBridge.exposeInMainWorld('electronManagement', management) + + webFrame.executeJavaScript(` + chrome.webstorePrivate = globalThis.electronWebstore; + chrome.runtime = globalThis.electronRuntime; + chrome.management = globalThis.electronManagement; + `) +} + +if (location.href.startsWith('https://chromewebstore.google.com')) { + console.log('Injecting Chrome Web Store API') + setupChromeWebStoreApi() +} diff --git a/packages/electron-chrome-web-store/tsconfig.json b/packages/electron-chrome-web-store/tsconfig.json new file mode 100644 index 00000000..9ab8c381 --- /dev/null +++ b/packages/electron-chrome-web-store/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + + "compilerOptions": { + "moduleResolution": "node", + "outDir": "dist", + "declaration": true + }, + + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/packages/electron-chrome-web-store/yarn.lock b/packages/electron-chrome-web-store/yarn.lock new file mode 100644 index 00000000..b72816c2 --- /dev/null +++ b/packages/electron-chrome-web-store/yarn.lock @@ -0,0 +1,8 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +typescript@^5.6.3: + version "5.6.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.3.tgz#5f3449e31c9d94febb17de03cc081dd56d81db5b" + integrity sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw== diff --git a/packages/shell/browser/main.js b/packages/shell/browser/main.js index c3deb239..919b1174 100644 --- a/packages/shell/browser/main.js +++ b/packages/shell/browser/main.js @@ -6,6 +6,7 @@ const { Tabs } = require('./tabs') const { ElectronChromeExtensions } = require('electron-chrome-extensions') const { setupMenu } = require('./menu') const { buildChromeContextMenu } = require('electron-chrome-context-menu') +const { setupChromeWebStore } = require('electron-chrome-web-store') // https://www.electronforge.io/config/plugins/webpack#main-process-code const ROOT_DIR = path.join(__dirname, '../../../../'); @@ -230,6 +231,8 @@ class Browser { this.popup = popup }) + setupChromeWebStore(this.session, path.join(__dirname, 'electron-chrome-web-store')) + const webuiExtension = await this.session.loadExtension(PATHS.WEBUI) webuiExtensionId = webuiExtension.id diff --git a/packages/shell/browser/ui/new-tab.html b/packages/shell/browser/ui/new-tab.html index a24d32b6..02ebdec5 100644 --- a/packages/shell/browser/ui/new-tab.html +++ b/packages/shell/browser/ui/new-tab.html @@ -22,6 +22,7 @@

New Tab

>https://github.com/samuelmaddock/electron-browser-shell +
  • https://chromewebstore.google.com
  • https://permission.site
  • https://samuelmaddock.com
  • diff --git a/packages/shell/package.json b/packages/shell/package.json index 3888bc86..d8ae9fae 100644 --- a/packages/shell/package.json +++ b/packages/shell/package.json @@ -15,6 +15,7 @@ "private": true, "dependencies": { "electron-chrome-context-menu": "^1.0.1", + "electron-chrome-web-store": "*", "electron-chrome-extensions": "^4.0.0", "electron-squirrel-startup": "^1.0.0" }, diff --git a/packages/shell/webpack.main.config.js b/packages/shell/webpack.main.config.js index d29865fd..6c403101 100644 --- a/packages/shell/webpack.main.config.js +++ b/packages/shell/webpack.main.config.js @@ -20,6 +20,10 @@ module.exports = { from: path.resolve(__dirname, '../electron-chrome-extensions/dist'), to: path.resolve(__dirname, '.webpack/main/electron-chrome-extensions/dist'), }, + { + from: path.resolve(__dirname, '../electron-chrome-web-store/dist'), + to: path.resolve(__dirname, '.webpack/main/electron-chrome-web-store/dist'), + }, ], }), ], diff --git a/yarn.lock b/yarn.lock index c414c45c..d567921d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2104,6 +2104,11 @@ acorn@^8.7.1, acorn@^8.8.2: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b" integrity sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w== +adm-zip@^0.5.16: + version "0.5.16" + resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.5.16.tgz#0b5e4c779f07dedea5805cdccb1147071d94a909" + integrity sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ== + agent-base@6, agent-base@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" @@ -6422,6 +6427,11 @@ typescript@^4.1.3, typescript@^4.9.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.4.tgz#a2a3d2756c079abda241d75f149df9d561091e78" integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg== +typescript@^5.6.3: + version "5.6.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.3.tgz#5f3449e31c9d94febb17de03cc081dd56d81db5b" + integrity sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw== + undici-types@~6.19.2: version "6.19.8" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02"