refactor: addon stores
This commit is contained in:
@@ -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"];
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
577
waterfox/browser/components/addonstores/StoreHandler.sys.mjs
Normal file
577
waterfox/browser/components/addonstores/StoreHandler.sys.mjs
Normal file
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
]
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user