diff --git a/waterfox/browser/components/WaterfoxGlue.jsm b/waterfox/browser/components/WaterfoxGlue.jsm index 4c70a72052c9..026b042805fd 100644 --- a/waterfox/browser/components/WaterfoxGlue.jsm +++ b/waterfox/browser/components/WaterfoxGlue.jsm @@ -10,6 +10,10 @@ const { XPCOMUtils } = ChromeUtils.import( "resource://gre/modules/XPCOMUtils.jsm" ); +const { AddonManager } = ChromeUtils.import( + "resource://gre/modules/AddonManager.jsm" +); + const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); XPCOMUtils.defineLazyModuleGetters(this, { @@ -25,6 +29,12 @@ XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]); const WaterfoxGlue = { async init() { + AddonManager.maybeInstallBuiltinAddon( + "addonstores@waterfox.net", + "1.0.0", + "resource://builtin-addons/addonstores/" + ); + // Parse chrome.manifest this.startupManifest = await this.getChromeManifest("startup"); this.privateManifest = await this.getChromeManifest("private"); diff --git a/waterfox/browser/components/addonstores/AddonStores.jsm b/waterfox/browser/components/addonstores/AddonStores.jsm new file mode 100644 index 000000000000..d2fa13eebea4 --- /dev/null +++ b/waterfox/browser/components/addonstores/AddonStores.jsm @@ -0,0 +1,53 @@ +/* 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"; + +const CRX_CONTENT_TYPE = "application/x-chrome-extension"; + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +ChromeUtils.defineModuleGetter( + this, + "StoreHandler", + "resource:///modules/StoreHandler.jsm" +); + +function ExtensionCompatibilityHandler() {} + +ExtensionCompatibilityHandler.prototype = { + /** + * Handles a new request for an application/x-xpinstall file. + * + * @param aMimetype + * The mimetype of the file + * @param aContext + * The context passed to nsIChannel.asyncOpen + * @param aRequest + * The nsIRequest dealing with the content + */ + async handleContent(aMimetype, aContext, aRequest) { + let uri = aRequest.URI; + if (aMimetype == CRX_CONTENT_TYPE) { + // attempt install + try { + return new StoreHandler().attemptInstall(uri); + } catch (ex) { + this.log(ex); + } + } + return undefined; + }, + + classID: Components.ID("{478ebd10-5998-11eb-be34-0800200c9a66}"), + QueryInterface: ChromeUtils.generateQI([Ci.nsIContentHandler]), + + log(aMsg) { + let msg = "addon_stores.js: " + (aMsg.join ? aMsg.join("") : aMsg); + Services.console.logStringMessage(msg); + dump(msg + "\n"); + }, +}; + +var EXPORTED_SYMBOLS = ["ExtensionCompatibilityHandler"]; diff --git a/waterfox/browser/components/addonstores/StoreHandler.jsm b/waterfox/browser/components/addonstores/StoreHandler.jsm new file mode 100644 index 000000000000..b6a68d664680 --- /dev/null +++ b/waterfox/browser/components/addonstores/StoreHandler.jsm @@ -0,0 +1,547 @@ +/* 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/components.conf b/waterfox/browser/components/addonstores/components.conf new file mode 100644 index 000000000000..1129d83422d6 --- /dev/null +++ b/waterfox/browser/components/addonstores/components.conf @@ -0,0 +1,14 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +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', + '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 new file mode 100644 index 000000000000..f6ce941bfa25 --- /dev/null +++ b/waterfox/browser/components/addonstores/extension/background.js @@ -0,0 +1,27 @@ +/* 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) { + 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.tabs + .query({ + currentWindow: true, + active: true, + }) + .then(tabs => { + for (let 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 new file mode 100644 index 000000000000..74c657852512 --- /dev/null +++ b/waterfox/browser/components/addonstores/extension/cws.js @@ -0,0 +1,184 @@ +/* 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"; + +function uninit() { + removeStyleSheet(); + removeInstallClickHandlers(document.body); + unwatchAddingInstallHandlers(); +} + +function init() { + addStyleSheet(); + (function initInstallHandlers() { + if (document.body) { + addInstallClickHandlers(document.body); + watchForAddingInstallHandlers(); + return; + } + window.requestAnimationFrame(initInstallHandlers); + })(); +} + +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 removeStyleSheet() { + const styleSheet = document.getElementById("wf-addons-store-style"); + if (styleSheet) { + styleSheet.remove(styleSheet); + } +} + +/** + * If return is truthy, the return value is returned. + * + */ +function parentNodeUntil(node, maxDepth, predicate) { + let curNode = node; + let rez; + let depth = 0; + while (!rez && depth++ < maxDepth) { + rez = predicate(curNode); + if (!rez) { + curNode = curNode.parentNode; + } + } + return rez; +} + +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; + } + }); + if (!extId) { + alert( + "Addon Stores Compatibility enecountered an error. Failed to determine extension ID." + ); + } else { + let downloadURL = buildDownloadURL(extId); + // Send downloadURL to background script + 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 => { + for (const mutation of mutations) { + if (mutation.type === "childList") { + for (const node of mutation.addedNodes) { + addInstallClickHandlers(node); + } + } + } + }); + + gObserver.observe(document.body, { + childList: true, + subtree: true, + }); +} + +function unwatchAddingInstallHandlers() { + gObserver.disconnect(); +} + +function buildDownloadURL(extId) { + let 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 => { + const ID = "waterfox-extension-test"; + if (!document.getElementById(ID)) { + let 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 new file mode 100644 index 000000000000..c0156d655c68 --- /dev/null +++ b/waterfox/browser/components/addonstores/extension/experiments/api.js @@ -0,0 +1,43 @@ +/* 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" +); + +this.total = class extends ExtensionAPI { + getAPI(context) { + let EventManager = ExtensionCommon.EventManager; + + return { + wf: { + onCrxInstall: new EventManager({ + context, + name: "wf.onCrxInstall", + register: fire => { + let observer = (subject, topic, data) => { + fire.sync(data); + }; + Services.obs.addObserver(observer, "waterfox-test-stores"); + return () => { + Services.obs.removeObserver(observer, "waterfox-test-stores"); + }; + }, + }).api(), + + attemptInstallChromeExtension(uri) { + try { + new StoreHandler().attemptInstall({ spec: uri }); + } catch (ex) { + Cu.reportError(ex); + } + }, + }, + }; + } +}; diff --git a/waterfox/browser/components/addonstores/extension/experiments/schema.json b/waterfox/browser/components/addonstores/extension/experiments/schema.json new file mode 100644 index 000000000000..fef49d12123d --- /dev/null +++ b/waterfox/browser/components/addonstores/extension/experiments/schema.json @@ -0,0 +1,34 @@ +[ + { + "namespace": "wf", + "description": "Helper functions for Waterfox extensions.", + "events": [ + { + "name": "onCrxInstall", + "type": "function", + "async": true, + "description": "Fired when the waterfox-test-stores observer is notified.", + "parameters": [ + { + "name": "data", + "type": "object" + } + ] + } + ], + "functions": [ + { + "name": "attemptInstallChromeExtension", + "type": "function", + "async": true, + "description": "Attempt to install the Chrome extension at the given URI.", + "parameters": [ + { + "name": "uri", + "type": "string" + } + ] + } + ] + } +] diff --git a/waterfox/browser/components/addonstores/extension/manifest.json b/waterfox/browser/components/addonstores/extension/manifest.json new file mode 100644 index 000000000000..4db0b7f3c780 --- /dev/null +++ b/waterfox/browser/components/addonstores/extension/manifest.json @@ -0,0 +1,49 @@ +{ + "manifest_version": 2, + "name": "Addon Store Compatibility", + "description": "Allow installing addons from certain other browser web stores.", + "version": "1.0.0", + "hidden": true, + + "applications": { + "gecko": { + "id": "addonstores@waterfox.net", + "strict_min_version": "72.0a1" + } + }, + + "content_scripts": [ + { + "matches": [ + "http://chrome.google.com/webstore*", + "https://chrome.google.com/webstore*" + ], + "js": ["cws.js"], + "run_at": "document_start", + "all_frames": true + }, + { + "matches": ["http://addons.opera.com/*", "https://addons.opera.com/*"], + "js": ["ows.js"], + "run_at": "document_start", + "all_frames": true + } + ], + + "background": { + "scripts": ["background.js"] + }, + + "permissions": ["tabs", "menus"], + + "experiment_apis": { + "total": { + "schema": "experiments/schema.json", + "parent": { + "scopes": ["addon_parent"], + "script": "experiments/api.js", + "paths": [["wf"]] + } + } + } +} diff --git a/waterfox/browser/components/addonstores/extension/ows.js b/waterfox/browser/components/addonstores/extension/ows.js new file mode 100644 index 000000000000..d0d6c901e960 --- /dev/null +++ b/waterfox/browser/components/addonstores/extension/ows.js @@ -0,0 +1,51 @@ +/* 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"; + +function init() { + const style = document.createElement("style"); + style.setAttribute("id", "wf-addons-store-style"); + style.textContent = ` + a.btn-install.btn-with-plus + { + border-radius: 2px; + box-shadow: 1px 1px 5px rgba(0,0,0,0.3); + background: linear-gradient(top,#28bd00 0%,#21a100 100%); + width: 150px; + /* to get "Add to Waterfox" to be over "Add to Opera" */ + display: flex; + color: transparent; + } + a.btn-install.btn-with-plus::before { + border-right: 1px solid #71BD4C; /* override btn-gray border-right */ + color: white; /* override color:transparent i set above */ + } + a.btn-install.btn-with-plus::after + { + display: block; + content: "Add To Waterfox"; + color: white; /* override color:transparent i set above */ + position: absolute; /* so it overlaps Add to opera */ + } + .site-message.site-message--top + { + display: none; + } + `; + + document.documentElement.insertBefore( + style, + document.documentElement.firstChild + ); +} + +function uninit() { + var style = document.getElementById("wf-addons-store-style"); + if (style) { + style.remove(style); + } +} + +init(); diff --git a/waterfox/browser/components/addonstores/jar.mn b/waterfox/browser/components/addonstores/jar.mn new file mode 100644 index 000000000000..9eddf29e8ba2 --- /dev/null +++ b/waterfox/browser/components/addonstores/jar.mn @@ -0,0 +1,3 @@ +browser.jar: +% resource builtin-addons %builtin-addons/ contentaccessible=yes + builtin-addons/addonstores/ (extension/**) \ No newline at end of file diff --git a/waterfox/browser/components/addonstores/moz.build b/waterfox/browser/components/addonstores/moz.build new file mode 100644 index 000000000000..d88cfafa06a6 --- /dev/null +++ b/waterfox/browser/components/addonstores/moz.build @@ -0,0 +1,16 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +EXTRA_JS_MODULES += [ + "AddonStores.jsm", + "StoreHandler.jsm", +] + +JAR_MANIFESTS += ["jar.mn"] + +XPCOM_MANIFESTS += [ + "components.conf", +] diff --git a/waterfox/browser/components/moz.build b/waterfox/browser/components/moz.build index 1d328cb2916c..406efd82410c 100644 --- a/waterfox/browser/components/moz.build +++ b/waterfox/browser/components/moz.build @@ -5,6 +5,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. DIRS += [ + "addonstores", "preferences", "privatetab", "statusbar",