feat: Add addon store compatibility

(cherry picked from commit 2ebe2f51f3f53503c0e7f8a9465b80ab679f96d2)
This commit is contained in:
adamp01
2022-07-26 11:33:01 +01:00
committed by Alex Kontos
parent e0e648b801
commit 7d8877f6ab
13 changed files with 1032 additions and 0 deletions

View File

@@ -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");

View File

@@ -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"];

View File

@@ -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();
});
}
}

View File

@@ -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',
},
]

View File

@@ -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 });
}
});
});

View File

@@ -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);
}
});

View File

@@ -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);
}
},
},
};
}
};

View File

@@ -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"
}
]
}
]
}
]

View File

@@ -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"]]
}
}
}
}

View File

@@ -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();

View File

@@ -0,0 +1,3 @@
browser.jar:
% resource builtin-addons %builtin-addons/ contentaccessible=yes
builtin-addons/addonstores/ (extension/**)

View File

@@ -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",
]

View File

@@ -5,6 +5,7 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DIRS += [
"addonstores",
"preferences",
"privatetab",
"statusbar",