From 0bfbcefaf6ca288b691aae8df7e09e50832587d6 Mon Sep 17 00:00:00 2001 From: Alex Kontos Date: Tue, 5 Aug 2025 14:29:20 +0100 Subject: [PATCH] refactor: addon stores --- .../{AddonStores.jsm => AddonStores.sys.mjs} | 28 +- .../components/addonstores/StoreHandler.jsm | 547 ----------------- .../addonstores/StoreHandler.sys.mjs | 577 ++++++++++++++++++ .../components/addonstores/components.conf | 2 +- .../addonstores/extension/background.js | 15 +- .../components/addonstores/extension/cws.js | 152 ++--- .../addonstores/extension/experiments/api.js | 20 +- .../addonstores/extension/manifest.json | 6 +- .../components/addonstores/extension/ows.js | 4 +- .../browser/components/addonstores/moz.build | 4 +- 10 files changed, 661 insertions(+), 694 deletions(-) rename waterfox/browser/components/addonstores/{AddonStores.jsm => AddonStores.sys.mjs} (61%) delete mode 100644 waterfox/browser/components/addonstores/StoreHandler.jsm create mode 100644 waterfox/browser/components/addonstores/StoreHandler.sys.mjs diff --git a/waterfox/browser/components/addonstores/AddonStores.jsm b/waterfox/browser/components/addonstores/AddonStores.sys.mjs similarity index 61% rename from waterfox/browser/components/addonstores/AddonStores.jsm rename to waterfox/browser/components/addonstores/AddonStores.sys.mjs index d2fa13eebea4..5d9aae67a9ab 100644 --- a/waterfox/browser/components/addonstores/AddonStores.jsm +++ b/waterfox/browser/components/addonstores/AddonStores.sys.mjs @@ -2,19 +2,15 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -"use strict"; - const CRX_CONTENT_TYPE = "application/x-chrome-extension"; -const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const lazy = {}; -ChromeUtils.defineModuleGetter( - this, - "StoreHandler", - "resource:///modules/StoreHandler.jsm" -); +ChromeUtils.defineESModuleGetters(lazy, { + StoreHandler: "resource:///modules/StoreHandler.sys.mjs", +}); -function ExtensionCompatibilityHandler() {} +export function ExtensionCompatibilityHandler() {} ExtensionCompatibilityHandler.prototype = { /** @@ -27,12 +23,12 @@ ExtensionCompatibilityHandler.prototype = { * @param aRequest * The nsIRequest dealing with the content */ - async handleContent(aMimetype, aContext, aRequest) { - let uri = aRequest.URI; - if (aMimetype == CRX_CONTENT_TYPE) { + async handleContent(aMimetype, _aContext, aRequest) { + const uri = aRequest.URI; + if (aMimetype === CRX_CONTENT_TYPE) { // attempt install try { - return new StoreHandler().attemptInstall(uri); + return new lazy.StoreHandler().attemptInstall(uri); } catch (ex) { this.log(ex); } @@ -44,10 +40,8 @@ ExtensionCompatibilityHandler.prototype = { QueryInterface: ChromeUtils.generateQI([Ci.nsIContentHandler]), log(aMsg) { - let msg = "addon_stores.js: " + (aMsg.join ? aMsg.join("") : aMsg); + const msg = `addon_stores.js: ${aMsg.join ? aMsg.join("") : aMsg}`; Services.console.logStringMessage(msg); - dump(msg + "\n"); + dump(`${msg}\n`); }, }; - -var EXPORTED_SYMBOLS = ["ExtensionCompatibilityHandler"]; diff --git a/waterfox/browser/components/addonstores/StoreHandler.jsm b/waterfox/browser/components/addonstores/StoreHandler.jsm deleted file mode 100644 index b6a68d664680..000000000000 --- a/waterfox/browser/components/addonstores/StoreHandler.jsm +++ /dev/null @@ -1,547 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -var EXPORTED_SYMBOLS = ["StoreHandler"]; - -const { XPCOMUtils } = ChromeUtils.import( - "resource://gre/modules/XPCOMUtils.jsm" -); - -const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); - -XPCOMUtils.defineLazyModuleGetters(this, { - AddonManager: "resource://gre/modules/AddonManager.jsm", - BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm", - FileUtils: "resource://gre/modules/FileUtils.jsm", - OS: "resource://gre/modules/osfile.jsm", - NetUtil: "resource://gre/modules/NetUtil.jsm", -}); - -XPCOMUtils.defineLazyGetter(this, "PopupNotifications", () => { - // eslint-disable-next-line no-shadow - let { PopupNotifications } = ChromeUtils.import( - "resource://gre/modules/PopupNotifications.jsm" - ); - try { - const win = BrowserWindowTracker.getTopWindow(); - const gBrowser = win.gBrowser; - const document = win.document; - const gURLBar = win.gURLBar; - let shouldSuppress = () => { - return ( - win.windowState == win.STATE_MINIMIZED || - (gURLBar.getAttribute("pageproxystate") != "valid" && - gURLBar.focused) || - gBrowser?.selectedBrowser.hasAttribute("tabmodalChromePromptShowing") || - gBrowser?.selectedBrowser.hasAttribute("tabDialogShowing") - ); - }; - return new PopupNotifications( - gBrowser, - document.getElementById("notification-popup"), - document.getElementById("notification-popup-box"), - { shouldSuppress } - ); - } catch (ex) { - Cu.reportError(ex); - return null; - } -}); - -const ZipReader = Components.Constructor( - "@mozilla.org/libjar/zip-reader;1", - "nsIZipReader", - "open" -); - -const zw = Cc["@mozilla.org/zipwriter;1"].createInstance(Ci.nsIZipWriter); - -const ReusableStreamInstance = Components.Constructor( - "@mozilla.org/scriptableinputstream;1", - "nsIScriptableInputStream", - "init" -); - -const uuidGenerator = Services.uuid; - -class StoreHandler { - // init vars - constructor() { - this.uuidString = this._getUUID().slice(1, -1); - this.xpiPath = OS.Path.join( - OS.Constants.Path.profileDir, - "extensions", - "tmp", - this.uuidString, - "extension.xpi" - ); - this.manifestPath = OS.Path.join( - OS.Constants.Path.profileDir, - "extensions", - "tmp", - this.uuidString, - "new_manifest.json" - ); - this.nsiFileXpi = this._getNsiFile(this.xpiPath); - this.nsiManifest = this._getNsiFile(this.manifestPath); - } - - /** - * Remove dir if it exists - * @param dir string absolute path to directory to remove - */ - flushDir(dir) { - return new Promise(resolve => { - const nsiDir = this._getNsiFile(dir); - if (nsiDir.exists()) { - // remove all files - nsiDir.remove(true); - } - resolve(); - }); - } - - /** - * Return extension UUID, set and return if not already set - */ - _getUUID() { - if (!this._extensionUUID) { - this._setUUID(); - } - return this._extensionUUID; - } - - /** - * Set extension UUID - */ - _setUUID() { - let uuid = uuidGenerator.generateUUID(); - let uuidString = uuid.toString(); - this._extensionUUID = uuidString; - } - - /** - * Reset extension UUID - */ - _resetUUID() { - return new Promise(resolve => { - this._extensionUUID = undefined; - resolve(); - }); - } - - /** - * Display prompt in event of failed installation - * @param msg string message to display - */ - _installFailedMsg( - msg = "Encountered an error during extension installation" - ) { - const anchorID = "addons-notification-icon"; - const win = BrowserWindowTracker.getTopWindow(); - const browser = win.gBrowser.selectedBrowser; - let action = { - label: "OK", - accessKey: "failed_accessKey", - callback: () => {}, - }; - var options = { - persistent: true, - hideClose: true, - }; - PopupNotifications.show( - browser, - "addon-install-failed", - msg, - anchorID, - action, - null, - options - ); - } - - /** - * Get an nsiFile object from a given path - * @param path string path to file - */ - _getNsiFile(path) { - let nsiFile = new FileUtils.File(path); - return nsiFile; - } - - /** - * Attempt to install a crx extension - * @param uri object uri of request - * @param retry bool is this a retry attempt or not - */ - attemptInstall(uri, retry = false) { - let channel = NetUtil.newChannel({ - uri: uri.spec, - loadUsingSystemPrincipal: true, - }); - NetUtil.asyncFetch(channel, (aInputStream, aResult) => { - // Check that we had success. - if (!Components.isSuccessCode(aResult)) { - if (!retry) { - this.attemptInstall(uri, true); - return false; - } - this._installFailedMsg( - "The add-on could not be downloaded because of a connection failure." - ); - return false; - } - // write nsiInputStream to nsiOutputStream - // this was originally in a separate function but had error - // passing input stream between funcs - let aOutputStream = FileUtils.openAtomicFileOutputStream(this.nsiFileXpi); - NetUtil.asyncCopy(aInputStream, aOutputStream, async aResultInner => { - // Check that we had success. - if (!Components.isSuccessCode(aResultInner)) { - // delete any tmp files - this._cleanup(this.nsiFileXpi); - this._installFailedMsg( - "This add-on could not be installed because of a filesystem error." - ); - return false; - } - try { - await this._removeChromeHeaders(this.xpiPath); - let manifest = this._amendManifest(this.nsiFileXpi); - // Notify tests - Services.obs.notifyObservers(null, "waterfox-test-stores"); - if (manifest instanceof Array) { - this._cleanup(this.nsiFileXpi); - this._installFailedMsg( - "This add-on could not be installed because not all of its features are supported." - ); - Services.console.logStringMessage( - "CRX: Unsupported APIs: " + manifest.join(",") - ); - return false; - } - this._writeTmpManifest(this.nsiManifest, manifest); - this._replaceManifestInXpi(this.nsiFileXpi, this.nsiManifest); - await this._installXpi(this.nsiFileXpi); - // this._cleanup(this.nsiFileXpi); - this._resetUUID(); - } catch (e) { - // delete any tmp files - this._cleanup(this.nsiFileXpi); - this._installFailedMsg( - "There was an issue while attempting to install the add-on." - ); - Services.console.logStringMessage( - "CRX: Error installing add-on: " + e - ); - return false; - } - }); - }); - } - - /** - * Remove Chrome headers from crx addon - * @param path string path to downloaded extension file - */ - async _removeChromeHeaders(path) { - try { - // read using OS.File to enable data manipulation - let arrayBuffer = await OS.File.read(path); - // determine Chrome ext headers - let locOfPk = arrayBuffer.slice(0, 3000); - for (var i = 0; i < locOfPk.length; i++) { - if ( - locOfPk[i] == 80 && - locOfPk[i + 1] == 75 && - locOfPk[i + 2] == 3 && - locOfPk[i + 3] == 4 - ) { - locOfPk = null; - break; - } - } - if (i == 3000) { - Services.console.logStringMessage("CRX: Magic not found"); - return false; - } - // remove Chrome ext headers - let zipBuffer = arrayBuffer.slice(i); - // overwrite .zip with headers removed as ZipReader only compatible with nsiFile type, not Uint8Array - await OS.File.writeAtomic(path, zipBuffer); - return true; - } catch (e) { - Services.console.logStringMessage("CRX: Error removing Chrome headers"); - return false; - } - } - - /** - * Check API compatibility and maybe add id and remove update_url from manifest - * @param file nsiFile tmp extension file - */ - _amendManifest(file) { - try { - // unzip nsiFile object - let zr = new ZipReader(file); - let manifest = this._parseManifest(zr); - // only manifest version 2 currently supported - if (manifest.manifest_version != 2 || !manifest.manifest_version) { - this._installFailedMsg( - "Manifest version not supported, must be manifest_version: 2" - ); - return false; - } - // ensure locale properties set correctly - manifest = this._localeCheck(manifest, zr); - // check API compatibility - let unsupportedApis = this._manifestCompatCheck(manifest); - if (unsupportedApis.length) { - return unsupportedApis; - } - manifest.applications = { - gecko: { - id: this._getUUID(), - }, - }; - // cannot allow auto update of crx extensions - delete manifest.update_url; - manifest = JSON.stringify(manifest); - // close zipReader - zr.close(); - return manifest; - } catch (e) { - Services.console.logStringMessage("CRX: Error updating manifest: " + e); - return false; - } - } - - /** - * Parse manifest file into JS Object - * @param zr nsiZipReader ZipReader object - */ - _parseManifest(zr) { - let entryPointer = "manifest.json"; - let manifest; - if (zr.hasEntry(entryPointer)) { - let entry = zr.getEntry(entryPointer); - let inputStream = zr.getInputStream(entryPointer); - let rsi = new ReusableStreamInstance(inputStream); - let fileContents = rsi.read(entry.realSize); - manifest = JSON.parse(fileContents); - } - return manifest; - } - - /** - * Check support for APIs in manifest - * @param manifest Object manifest to compatibility check - */ - _manifestCompatCheck(manifest) { - let unsupported = { - externally_connectable: "", - storage: "", - chrome_settings_overrides: { - search_provider: { - alternate_urls: "", - image_url: "", - image_url_post_params: "", - instant_url: "", - instant_url_post_params: "", - prepopulated_id: "", - }, - startup_pages: "", - }, - chrome_url_overrides: { - bookmarks: "", - history: "", - }, - commands: { - global: "", - }, - incognito: "split", - offline_enabled: "", - optional_permissions: [ - "background", - "contentSettings", - "contextMenus", - "debugger", - "pageCapture", - "tabCapture", - ], - options_page: "", - permissions: [ - "background", - "contentSettings", - "debugger", - "pageCapture", - "tabCapture", - ], - version_name: "", - }; - var unsupportedInManifest = []; - Object.entries(manifest).forEach(arr => { - if ( - Object.keys(unsupported).includes(arr[0]) && - unsupported[arr[0]] == "" - ) { - // if manifest key is in unsupported list and - // no value associated with unsupported key - // we know it's unsupported in it's entirety - unsupportedInManifest.push(arr[0]); - } else if ( - Object.keys(unsupported).includes(arr[0]) && - typeof unsupported[arr[0]] == "string" && - unsupported[arr[0]] == arr[1] - ) { - // if key is unsupported and value matches - // value in unsupported, we know the kv pair - // only is unsupported - unsupportedInManifest.push(arr[0] + ": " + arr[1]); - } else if ( - Object.keys(unsupported).includes(arr[0]) && - Object.prototype.toString.call(unsupported[arr[0]]) == - "[object Array]" && - Object.prototype.toString.call(arr[1]) == "[object Array]" - ) { - // if value in unsupported is an array, we know - // key is permissions related so we need to check - // each permission against the unsupported array - var permissionArr = []; - arr[1].forEach(value => { - if (unsupported[arr[0]].includes(value)) { - permissionArr.push(arr[0] + "." + value); - } - }); - if (permissionArr.length) { - unsupportedInManifest.push(...permissionArr); - } - } else if ( - Object.keys(unsupported).includes(arr[0]) && - typeof unsupported[arr[0]] == "object" && - typeof arr[1] == "object" - ) { - // if value in unsupported is object we need to - // identify if this is the final layer or if there - // is another object for one of the keys here - Object.keys(arr[1]).forEach(key => { - if ( - Object.keys(unsupported[arr[0]]).includes(key) && - typeof unsupported[arr[0]][key] == "string" - ) { - // if object value in unsupported is string we know that - // it is unsupported in it's entirety - unsupportedInManifest.push(arr[0] + "." + key); - // TODO: need to rewrite this to be recursive so we don't have to go down the nesting - } else if ( - Object.keys(unsupported[arr[0]]).includes(key) && - typeof unsupported[arr[0]][key] == "object" && - typeof arr[1][key] == "object" - ) { - // if object value in unsupported is another object - // we have to dig through the extra layer - Object.keys(arr[1][key]).forEach(value => { - if (Object.keys(unsupported[arr[0]][key]).includes(value)) { - unsupportedInManifest.push(arr[0] + "." + key + "." + value); - } - }); - } - }); - } - }); - return unsupportedInManifest; - } - - /** - * Ensure manifest compliance based on extension contents - * @param manifest - * @param zr - */ - _localeCheck(manifest, zr) { - let entryPointer = "_locales/"; - if (zr.hasEntry(entryPointer)) { - if (!manifest.default_locale) { - zr.hasEntry("_locales/en/") - ? (manifest.default_locale = "en") - : (manifest.default_locale = "en-US"); - } - } else if (manifest.default_locale) { - delete manifest.default_locale; - } - return manifest; - } - - /** - * Write amended manifest to temporary manifest.json - * @param file nsiFile tmp manifest.json - * @param manifest string JSON string of amended manifest - */ - _writeTmpManifest(file, manifest) { - let manifestOutputStream = FileUtils.openAtomicFileOutputStream(file); - manifestOutputStream.write(manifest, manifest.length); - } - - /** - * Replace the manifest in the tmp extension file with the amended version - * @param xpiFile nsiFile tmp extension file - * @param manifestFile nsiFile tmp manifest.json - */ - _replaceManifestInXpi(xpiFile, manifestFile) { - try { - let pr = { - PR_RDONLY: 0x01, - PR_WRONLY: 0x02, - PR_RDWR: 0x04, - PR_CREATE_FILE: 0x08, - PR_APPEND: 0x10, - PR_TRUNCATE: 0x20, - PR_SYNC: 0x40, - PR_EXCL: 0x80, - }; - zw.open(xpiFile, pr.PR_RDWR); - zw.removeEntry("manifest.json", false); - zw.addEntryFile( - "manifest.json", - Ci.nsIZipWriter.COMPRESSION_NONE, - manifestFile, - false - ); - zw.close(); - return true; - } catch (e) { - Services.console.logStringMessage("CRX: Error replacing manifest"); - return false; - } - } - - /** - * Silently install extension - * @param xpiFile nsiFile tmp extension file to install - */ - async _installXpi(xpiFile) { - let install = await AddonManager.getInstallForFile(xpiFile); - const win = BrowserWindowTracker.getTopWindow(); - const browser = win.gBrowser.selectedBrowser; - const document = win.document; - await AddonManager.installAddonFromAOM( - browser, - document.documentURI, - install - ); - } - - /** - * Remove tmp files - * @param zipFile nsiFile tmp extension file - */ - _cleanup(zipFile) { - return new Promise(resolve => { - let parent = zipFile.parent; - parent.remove(true); - resolve(); - }); - } -} diff --git a/waterfox/browser/components/addonstores/StoreHandler.sys.mjs b/waterfox/browser/components/addonstores/StoreHandler.sys.mjs new file mode 100644 index 000000000000..58235ae6206b --- /dev/null +++ b/waterfox/browser/components/addonstores/StoreHandler.sys.mjs @@ -0,0 +1,577 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + AddonManager: "resource://gre/modules/AddonManager.sys.mjs", + BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", + FileUtils: "resource://gre/modules/FileUtils.sys.mjs", + NetUtil: "resource://gre/modules/NetUtil.sys.mjs", +}); + +ChromeUtils.defineLazyGetter(lazy, "PopupNotifications", () => { + // eslint-disable-next-line no-shadow + const { PopupNotifications } = ChromeUtils.importESModule( + "resource://gre/modules/PopupNotifications.sys.mjs" + ); + try { + const win = lazy.BrowserWindowTracker.getTopWindow(); + const gBrowser = win.gBrowser; + const document = win.document; + const gURLBar = win.gURLBar; + const shouldSuppress = () => { + return ( + win.windowState === win.STATE_MINIMIZED || + (gURLBar.getAttribute("pageproxystate") !== "valid" && + gURLBar.focused) || + gBrowser?.selectedBrowser.hasAttribute("tabmodalChromePromptShowing") || + gBrowser?.selectedBrowser.hasAttribute("tabDialogShowing") + ); + }; + return new PopupNotifications( + gBrowser, + document.getElementById("notification-popup"), + document.getElementById("notification-popup-box"), + { shouldSuppress } + ); + } catch (ex) { + console.error(ex); + return null; + } +}); + +const ZipReader = Components.Constructor( + "@mozilla.org/libjar/zip-reader;1", + "nsIZipReader", + "open" +); + +const zw = Cc["@mozilla.org/zipwriter;1"].createInstance(Ci.nsIZipWriter); + +const ReusableStreamInstance = Components.Constructor( + "@mozilla.org/scriptableinputstream;1", + "nsIScriptableInputStream", + "init" +); + +const uuidGenerator = Services.uuid; + +export class StoreHandler { + // init vars + constructor() { + this.uuidString = this._getUUID().slice(1, -1); + const profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile).path; + this.xpiPath = PathUtils.join( + profileDir, + "extensions", + "tmp", + this.uuidString, + "extension.xpi" + ); + this.manifestPath = PathUtils.join( + profileDir, + "extensions", + "tmp", + this.uuidString, + "new_manifest.json" + ); + this.nsiFileXpi = this._getNsiFile(this.xpiPath); + this.nsiManifest = this._getNsiFile(this.manifestPath); + } + + /** + * Remove dir if it exists + * + * @param dir string absolute path to directory to remove + */ + flushDir(dir) { + return new Promise((resolve) => { + const nsiDir = this._getNsiFile(dir); + if (nsiDir.exists()) { + // remove all files + nsiDir.remove(true); + } + resolve(); + }); + } + + /** + * Return extension UUID, set and return if not already set + */ + _getUUID() { + if (!this._extensionUUID) { + this._setUUID(); + } + return this._extensionUUID; + } + + /** + * Set extension UUID + */ + _setUUID() { + const uuid = uuidGenerator.generateUUID(); + const uuidString = uuid.toString(); + this._extensionUUID = uuidString; + } + + /** + * Reset extension UUID + */ + _resetUUID() { + return new Promise((resolve) => { + this._extensionUUID = undefined; + resolve(); + }); + } + + /** + * Display prompt in event of failed installation + * + * @param msg string message to display + */ + _installFailedMsg( + msg = "Encountered an error during extension installation" + ) { + const anchorID = "addons-notification-icon"; + const win = lazy.BrowserWindowTracker.getTopWindow(); + const browser = win.gBrowser.selectedBrowser; + const action = { + label: "OK", + accessKey: "failed_accessKey", + callback: () => {}, + }; + const options = { + persistent: true, + hideClose: true, + }; + lazy.PopupNotifications.show( + browser, + "addon-install-failed", + msg, + anchorID, + action, + null, + options + ); + } + + /** + * Get an nsiFile object from a given path + * + * @param path string path to file + */ + _getNsiFile(path) { + const nsiFile = new lazy.FileUtils.File(path); + return nsiFile; + } + + /** + * Attempt to install a crx extension + * + * @param uri object uri of request + * @param retry bool is this a retry attempt or not + */ + attemptInstall(uri, retry = false) { + const channel = lazy.NetUtil.newChannel({ + uri: uri.spec, + loadUsingSystemPrincipal: true, + }); + lazy.NetUtil.asyncFetch(channel, (aInputStream, aResult) => { + // Check that we had success. + if (!Components.isSuccessCode(aResult)) { + if (!retry) { + this.attemptInstall(uri, true); + return false; + } + this._installFailedMsg( + "The add-on could not be downloaded because of a connection failure." + ); + return false; + } + // write nsiInputStream to nsiOutputStream + // this was originally in a separate function but had error + // passing input stream between funcs + const aOutputStream = lazy.FileUtils.openAtomicFileOutputStream( + this.nsiFileXpi + ); + lazy.NetUtil.asyncCopy( + aInputStream, + aOutputStream, + async (aResultInner) => { + // Check that we had success. + if (!Components.isSuccessCode(aResultInner)) { + // delete any tmp files + this._cleanup(this.nsiFileXpi); + this._installFailedMsg( + "This add-on could not be installed because of a filesystem error." + ); + return false; + } + try { + await this._removeChromeHeaders(this.xpiPath); + const manifest = this._amendManifest(this.nsiFileXpi); + // Notify tests + Services.obs.notifyObservers(null, "waterfox-test-stores"); + if (Array.isArray(manifest)) { + this._cleanup(this.nsiFileXpi); + this._installFailedMsg( + "This add-on could not be installed because not all of its features are supported." + ); + Services.console.logStringMessage( + `CRX: Unsupported APIs: ${manifest.join(",")}` + ); + return false; + } + this._writeTmpManifest(this.nsiManifest, manifest); + this._replaceManifestInXpi(this.nsiFileXpi, this.nsiManifest); + await this._installXpi(this.nsiFileXpi); + // this._cleanup(this.nsiFileXpi); + this._resetUUID(); + } catch (e) { + // delete any tmp files + this._cleanup(this.nsiFileXpi); + this._installFailedMsg( + "There was an issue while attempting to install the add-on." + ); + Services.console.logStringMessage( + `CRX: Error installing add-on: ${e}` + ); + return false; + } + } + ); + }); + } + + /** + * Remove Chrome headers from crx addon + * + * @param path string path to downloaded extension file + */ + async _removeChromeHeaders(path) { + let i; // Declare i at the root of the function scope to be used in the loop and after. + try { + // read using IOUtils to enable data manipulation + const arrayBuffer = await IOUtils.read(path); + // determine Chrome ext headers + let locOfPk = arrayBuffer.slice(0, 3000); + for (i = 0; i < locOfPk.length; i++) { // Initialize and use the function-scoped i + if ( + locOfPk[i] === 80 && + locOfPk[i + 1] === 75 && + locOfPk[i + 2] === 3 && + locOfPk[i + 3] === 4 + ) { + locOfPk = null; + break; + } + } + if (i === 3000) { + Services.console.logStringMessage("CRX: Magic not found"); + return false; + } + // remove Chrome ext headers + const zipBuffer = arrayBuffer.slice(i); + // overwrite .zip with headers removed as ZipReader only compatible with nsiFile type, not Uint8Array + await IOUtils.write(path, zipBuffer); + return true; + } catch (_e) { + Services.console.logStringMessage("CRX: Error removing Chrome headers"); + return false; + } + } + + /** + * Check API compatibility and maybe add id and remove update_url from manifest + * + * @param file nsiFile tmp extension file + */ + _amendManifest(file) { + try { + // unzip nsiFile object + const zr = new ZipReader(file); + let manifest = this._parseManifest(zr); + // only manifest version 2 currently supported + if (manifest.manifest_version !== 2 || !manifest.manifest_version) { + this._installFailedMsg( + "Manifest version not supported, must be manifest_version: 2" + ); + return false; + } + // ensure locale properties set correctly + manifest = this._localeCheck(manifest, zr); + // check API compatibility + const unsupportedApis = this._manifestCompatCheck(manifest); + if (unsupportedApis.length) { + return unsupportedApis; + } + manifest.applications = { + gecko: { + id: this._getUUID(), + }, + }; + // cannot allow auto update of crx extensions + manifest.update_url = undefined; + manifest = JSON.stringify(manifest); + // close zipReader + zr.close(); + return manifest; + } catch (e) { + Services.console.logStringMessage(`CRX: Error updating manifest: ${e}`); + return false; + } + } + + /** + * Parse manifest file into JS Object + * + * @param zr nsiZipReader ZipReader object + */ + _parseManifest(zr) { + const entryPointer = "manifest.json"; + let manifest; + if (zr.hasEntry(entryPointer)) { + const entry = zr.getEntry(entryPointer); + const inputStream = zr.getInputStream(entryPointer); + const rsi = new ReusableStreamInstance(inputStream); + const fileContents = rsi.read(entry.realSize); + manifest = JSON.parse(fileContents); + } + return manifest; + } + + /** + * Check support for APIs in manifest + * + * @param manifest Object manifest to compatibility check + */ + _manifestCompatCheck(manifest) { + const unsupported = { + externally_connectable: "", + storage: "", + chrome_settings_overrides: { + search_provider: { + alternate_urls: "", + image_url: "", + image_url_post_params: "", + instant_url: "", + instant_url_post_params: "", + prepopulated_id: "", + }, + startup_pages: "", + }, + chrome_url_overrides: { + bookmarks: "", + history: "", + }, + commands: { + global: "", + }, + incognito: "split", + offline_enabled: "", + optional_permissions: [ + "background", + "contentSettings", + "contextMenus", + "debugger", + "pageCapture", + "tabCapture", + ], + options_page: "", + permissions: [ + "background", + "contentSettings", + "debugger", + "pageCapture", + "tabCapture", + ], + version_name: "", + }; + const unsupportedInManifest = []; + + for (const arr of Object.entries(manifest)) { + const manifestKey = arr[0]; + const manifestValue = arr[1]; + + if ( + Object.keys(unsupported).includes(manifestKey) && + unsupported[manifestKey] === "" + ) { + // if manifest key is in unsupported list and + // no value associated with unsupported key + // we know it's unsupported in it's entirety + unsupportedInManifest.push(manifestKey); + } else if ( + Object.keys(unsupported).includes(manifestKey) && + typeof unsupported[manifestKey] === "string" && + unsupported[manifestKey] === manifestValue + ) { + // if key is unsupported and value matches + // value in unsupported, we know the kv pair + // only is unsupported + unsupportedInManifest.push(`${manifestKey}: ${manifestValue}`); + } else if ( + Object.keys(unsupported).includes(manifestKey) && + Array.isArray(unsupported[manifestKey]) && + Array.isArray(manifestValue) + ) { + // if value in unsupported is an array, we know + // key is permissions related so we need to check + // each permission against the unsupported array + const permissionArr = []; // Changed var to let, scoped within this block + for (const value of manifestValue) { + if (unsupported[manifestKey].includes(value)) { + permissionArr.push(`${manifestKey}.${value}`); + } + } + if (permissionArr.length) { + unsupportedInManifest.push(...permissionArr); + } + } else if ( + Object.keys(unsupported).includes(manifestKey) && + typeof unsupported[manifestKey] === "object" && + unsupported[manifestKey] !== null && + !Array.isArray(unsupported[manifestKey]) && + typeof manifestValue === "object" && + manifestValue !== null && + !Array.isArray(manifestValue) + ) { + // if value in unsupported is object we need to + // identify if this is the final layer or if there + // is another object for one of the keys here + for (const key of Object.keys(manifestValue)) { + if ( + Object.keys(unsupported[manifestKey]).includes(key) && + typeof unsupported[manifestKey][key] === "string" + ) { + // if object value in unsupported is string we know that + // it is unsupported in it's entirety + unsupportedInManifest.push(`${manifestKey}.${key}`); + // TODO: need to rewrite this to be recursive so we don't have to go down the nesting + } else if ( + Object.keys(unsupported[manifestKey]).includes(key) && + typeof unsupported[manifestKey][key] === "object" && + unsupported[manifestKey][key] !== null && + !Array.isArray(unsupported[manifestKey][key]) && + typeof manifestValue[key] === "object" && + manifestValue[key] !== null && + !Array.isArray(manifestValue[key]) + ) { + // if object value in unsupported is another object + // we have to dig through the extra layer + for (const value of Object.keys(manifestValue[key])) { + if ( + Object.keys(unsupported[manifestKey][key]).includes(value) + ) { + unsupportedInManifest.push( + `${manifestKey}.${key}.${value}` + ); + } + } + } + } + } + } + return unsupportedInManifest; + } + + /** + * Ensure manifest compliance based on extension contents + * + * @param manifest + * @param zr + */ + _localeCheck(manifest, zr) { + const entryPointer = "_locales/"; + if (zr.hasEntry(entryPointer)) { + if (!manifest.default_locale) { + if (zr.hasEntry("_locales/en/")) { + manifest.default_locale = "en"; + } else { + manifest.default_locale = "en-US"; + } + } + } else if (manifest.default_locale) { + manifest.default_locale = undefined; + } + return manifest; + } + + /** + * Write amended manifest to temporary manifest.json + * + * @param file nsiFile tmp manifest.json + * @param manifest string JSON string of amended manifest + */ + _writeTmpManifest(file, manifest) { + const manifestOutputStream = + lazy.FileUtils.openAtomicFileOutputStream(file); + manifestOutputStream.write(manifest, manifest.length); + } + + /** + * Replace the manifest in the tmp extension file with the amended version + * + * @param xpiFile nsiFile tmp extension file + * @param manifestFile nsiFile tmp manifest.json + */ + _replaceManifestInXpi(xpiFile, manifestFile) { + try { + const pr = { + PR_RDONLY: 0x01, + PR_WRONLY: 0x02, + PR_RDWR: 0x04, + PR_CREATE_FILE: 0x08, + PR_APPEND: 0x10, + PR_TRUNCATE: 0x20, + PR_SYNC: 0x40, + PR_EXCL: 0x80, + }; + zw.open(xpiFile, pr.PR_RDWR); + zw.removeEntry("manifest.json", false); + zw.addEntryFile( + "manifest.json", + Ci.nsIZipWriter.COMPRESSION_NONE, + manifestFile, + false + ); + zw.close(); + return true; + } catch (_e) { + Services.console.logStringMessage("CRX: Error replacing manifest"); + return false; + } + } + + /** + * Silently install extension + * + * @param xpiFile nsiFile tmp extension file to install + */ + async _installXpi(xpiFile) { + const install = await lazy.AddonManager.getInstallForFile(xpiFile); + const win = lazy.BrowserWindowTracker.getTopWindow(); + const browser = win.gBrowser.selectedBrowser; + const document = win.document; + await lazy.AddonManager.installAddonFromAOM( + browser, + document.documentURI, + install + ); + } + + /** + * Remove tmp files + * + * @param zipFile nsiFile tmp extension file + */ + _cleanup(zipFile) { + return new Promise((resolve) => { + const parent = zipFile.parent; + parent.remove(true); + resolve(); + }); + } +} diff --git a/waterfox/browser/components/addonstores/components.conf b/waterfox/browser/components/addonstores/components.conf index 1129d83422d6..071ad7cad6b3 100644 --- a/waterfox/browser/components/addonstores/components.conf +++ b/waterfox/browser/components/addonstores/components.conf @@ -8,7 +8,7 @@ Classes = [ { 'cid': '{478ebd10-5998-11eb-be34-0800200c9a66}', 'contract_ids': ['@mozilla.org/uriloader/content-handler;1?type=application/x-chrome-extension'], - 'jsm': 'resource:///modules/AddonStores.jsm', + 'esModule': 'resource:///modules/AddonStores.sys.mjs', 'constructor': 'ExtensionCompatibilityHandler', }, ] \ No newline at end of file diff --git a/waterfox/browser/components/addonstores/extension/background.js b/waterfox/browser/components/addonstores/extension/background.js index f6ce941bfa25..7f424338694e 100644 --- a/waterfox/browser/components/addonstores/extension/background.js +++ b/waterfox/browser/components/addonstores/extension/background.js @@ -1,26 +1,19 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -/* globals browser */ - -"use strict"; - // Handle install request from Chrome Web store button click -function handleMessage(request, sender, sendResponse) { +function handleMessage(request, _sender, _sendResponse) { browser.wf.attemptInstallChromeExtension(request.downloadURL); } browser.runtime.onMessage.addListener(handleMessage); // Send message to content script to add new element to indicate crx install attempt succeeded -browser.wf.onCrxInstall.addListener(data => { +browser.wf.onCrxInstall.addListener((_data) => { browser.tabs .query({ currentWindow: true, active: true, }) - .then(tabs => { - for (let tab of tabs) { + .then((tabs) => { + for (const tab of tabs) { browser.tabs.sendMessage(tab.id, { update: true }); } }); diff --git a/waterfox/browser/components/addonstores/extension/cws.js b/waterfox/browser/components/addonstores/extension/cws.js index 74c657852512..57289148926a 100644 --- a/waterfox/browser/components/addonstores/extension/cws.js +++ b/waterfox/browser/components/addonstores/extension/cws.js @@ -3,20 +3,17 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* globals browser */ -"use-strict"; - -function uninit() { - removeStyleSheet(); - removeInstallClickHandlers(document.body); +function _uninit() { + updateInstallClickHandlers(document.body, false); unwatchAddingInstallHandlers(); } function init() { - addStyleSheet(); (function initInstallHandlers() { if (document.body) { - addInstallClickHandlers(document.body); + updateInstallClickHandlers(document.body, true); watchForAddingInstallHandlers(); + replaceButtonText(); return; } window.requestAnimationFrame(initInstallHandlers); @@ -26,54 +23,47 @@ function init() { let gObserver; init(); -function addStyleSheet() { - const styleSheet = document.createElement("style"); - styleSheet.setAttribute("id", "wf-addons-store-style"); - styleSheet.textContent = ` - div[role=dialog][aria-labelledby="promo-header"] - { - visibility: hidden; - } - div[role=button][aria-label*="CHROME"], - div[role=button][aria-label*="Chrome"] - { - background-color: rgb(124, 191, 54); - background-image: linear-gradient(to bottom, rgb(124, 191, 54), rgb(101, 173, 40)); - border-color:rgb(78, 155, 25); - } - div[role=button][aria-label*="CHROME"] .webstore-test-button-label, - div[role=button][aria-label*="Chrome"] .webstore-test-button-label - { - font-size: 0; - } - div[role=button][aria-label*="CHROME"] .webstore-test-button-label::before, - div[role=button][aria-label*="Chrome"] .webstore-test-button-label::before - { - display: flex; - content: "Add To Waterfox"; - justify-content: center; - align-items: center; - font-size: 14px; - } - /* targeting download div */ - body > div:last-of-type > div:nth-of-type(2), - /* alt target download div */ - .h-Yb-wa.Yb-wa - { - display: none; - } - `; - - document.documentElement.insertBefore( - styleSheet, - document.documentElement.firstChild +function hideElements() { + const elementsToHide = Array.from( + document.querySelectorAll( + '[aria-labelledby="promo-header"], [aria-label="info"]' + ) ); + + for (const element of elementsToHide) { + element.style.display = "none"; + } } -function removeStyleSheet() { - const styleSheet = document.getElementById("wf-addons-store-style"); - if (styleSheet) { - styleSheet.remove(styleSheet); +function replaceButtonText() { + const buttons = Array.from(document.querySelectorAll("button")).filter( + (button) => button.textContent.includes("Add to Chrome") + ); + + for (const button of buttons) { + button.textContent = button.textContent.replace( + "Add to Chrome", + "Add to Waterfox" + ); + button.style.color = "white"; // Add this line + } +} + +function updateInstallClickHandlers(node, addHandlers) { + if (node.nodeType === Node.ELEMENT_NODE) { + const buttons = Array.from(node.querySelectorAll("button")).filter( + (button) => button.textContent.includes("Add to Chrome") + ); + + for (const button of buttons) { + if (addHandlers) { + button.removeAttribute("disabled"); + button.addEventListener("click", handleInstall, true); + } else { + button.setAttribute("disabled", ""); + button.removeEventListener("click", handleInstall, true); + } + } } } @@ -81,7 +71,7 @@ function removeStyleSheet() { * If return is truthy, the return value is returned. * */ -function parentNodeUntil(node, maxDepth, predicate) { +function _parentNodeUntil(node, maxDepth, predicate) { let curNode = node; let rez; let depth = 0; @@ -98,61 +88,29 @@ function handleInstall(e) { e.preventDefault(); e.stopPropagation(); - // start figure out id - // Thanks to @Rob--W the id is accurately obtained: "It is the first 32 characters of the public key's sha256 hash, with the 0-9a-f replaced with a-p" - const extIdPatt = /[^a-p]([a-p]{32})[^a-p]/i; - const extId = parentNodeUntil(e.target, 100, node => { - if (node.nodeType === Node.ELEMENT_NODE) { - const [, extId] = extIdPatt.exec(node.innerHTML) || []; - console.log("extId:", extId); - return extId; - } - }); + // Extract the extension ID from the URL of the page + const urlParts = window.location.pathname.split("/"); + const extId = urlParts[urlParts.length - 1]; + if (!extId) { alert( - "Addon Stores Compatibility enecountered an error. Failed to determine extension ID." + "Addon Stores Compatibility encountered an error. Failed to determine extension ID." ); } else { - let downloadURL = buildDownloadURL(extId); - // Send downloadURL to background script + const downloadURL = buildDownloadURL(extId); browser.runtime.sendMessage({ downloadURL, }); } } -function addInstallClickHandlers(node) { - if (node.nodeType === Node.ELEMENT_NODE) { - const buttons = [ - ...node.querySelectorAll('div[role=button][aria-label*="Chrome"]'), - ...node.querySelectorAll('div[role=button][aria-label*="CHROME"]'), - ]; - - for (const button of buttons) { - button.addEventListener("click", handleInstall, true); - } - } -} - -function removeInstallClickHandlers(node) { - if (node.nodeType === Node.ELEMENT_NODE) { - const buttons = [ - ...node.querySelectorAll('div[role=button][aria-label*="Chrome"]'), - ...node.querySelectorAll('div[role=button][aria-label*="CHROME"]'), - ]; - - for (const button of buttons) { - button.removeEventListener("click", handleInstall, true); - } - } -} - function watchForAddingInstallHandlers() { - gObserver = new MutationObserver(mutations => { + gObserver = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === "childList") { for (const node of mutation.addedNodes) { - addInstallClickHandlers(node); + updateInstallClickHandlers(node, true); + hideElements(); } } } @@ -169,15 +127,15 @@ function unwatchAddingInstallHandlers() { } function buildDownloadURL(extId) { - let baseUrl = + const baseUrl = "https://clients2.google.com/service/update2/crx?response=redirect&prodversion=49.0&acceptformat=crx3&x=id%3D***%26installsource%3Dondemand%26uc"; return baseUrl.replace("***", extId); } -browser.runtime.onMessage.addListener(request => { +browser.runtime.onMessage.addListener((_request) => { const ID = "waterfox-extension-test"; if (!document.getElementById(ID)) { - let el = document.createElement("div"); + const el = document.createElement("div"); el.setAttribute("id", ID); document.body.appendChild(el); } diff --git a/waterfox/browser/components/addonstores/extension/experiments/api.js b/waterfox/browser/components/addonstores/extension/experiments/api.js index c0156d655c68..4dc66acc93db 100644 --- a/waterfox/browser/components/addonstores/extension/experiments/api.js +++ b/waterfox/browser/components/addonstores/extension/experiments/api.js @@ -1,26 +1,18 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -/* globals browser, AppConstants, Services, ExtensionAPI, ExtensionCommon */ - -"use strict"; - -const { StoreHandler } = ChromeUtils.import( - "resource:///modules/StoreHandler.jsm" +const { StoreHandler } = ChromeUtils.importESModule( + "resource:///modules/StoreHandler.sys.mjs" ); this.total = class extends ExtensionAPI { getAPI(context) { - let EventManager = ExtensionCommon.EventManager; + const EventManager = ExtensionCommon.EventManager; return { wf: { onCrxInstall: new EventManager({ context, name: "wf.onCrxInstall", - register: fire => { - let observer = (subject, topic, data) => { + register: (fire) => { + const observer = (_subject, _topic, data) => { fire.sync(data); }; Services.obs.addObserver(observer, "waterfox-test-stores"); @@ -34,7 +26,7 @@ this.total = class extends ExtensionAPI { try { new StoreHandler().attemptInstall({ spec: uri }); } catch (ex) { - Cu.reportError(ex); + console.error(ex); } }, }, diff --git a/waterfox/browser/components/addonstores/extension/manifest.json b/waterfox/browser/components/addonstores/extension/manifest.json index 4db0b7f3c780..c167e017e230 100644 --- a/waterfox/browser/components/addonstores/extension/manifest.json +++ b/waterfox/browser/components/addonstores/extension/manifest.json @@ -5,7 +5,7 @@ "version": "1.0.0", "hidden": true, - "applications": { + "browser_specific_settings": { "gecko": { "id": "addonstores@waterfox.net", "strict_min_version": "72.0a1" @@ -15,8 +15,8 @@ "content_scripts": [ { "matches": [ - "http://chrome.google.com/webstore*", - "https://chrome.google.com/webstore*" + "http://chromewebstore.google.com/*", + "https://chromewebstore.google.com/*" ], "js": ["cws.js"], "run_at": "document_start", diff --git a/waterfox/browser/components/addonstores/extension/ows.js b/waterfox/browser/components/addonstores/extension/ows.js index d0d6c901e960..4e8c3bf1141e 100644 --- a/waterfox/browser/components/addonstores/extension/ows.js +++ b/waterfox/browser/components/addonstores/extension/ows.js @@ -41,8 +41,8 @@ function init() { ); } -function uninit() { - var style = document.getElementById("wf-addons-store-style"); +function _uninit() { + const style = document.getElementById("wf-addons-store-style"); if (style) { style.remove(style); } diff --git a/waterfox/browser/components/addonstores/moz.build b/waterfox/browser/components/addonstores/moz.build index d88cfafa06a6..1c2e4fa9e372 100644 --- a/waterfox/browser/components/addonstores/moz.build +++ b/waterfox/browser/components/addonstores/moz.build @@ -5,8 +5,8 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. EXTRA_JS_MODULES += [ - "AddonStores.jsm", - "StoreHandler.jsm", + "AddonStores.sys.mjs", + "StoreHandler.sys.mjs", ] JAR_MANIFESTS += ["jar.mn"]