Source code

Revision control

Copy as Markdown

Other Tools

/* 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/. */
import { Log } from "resource://gre/modules/Log.sys.mjs";
import { Svc } from "resource://services-sync/util.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
AddonRepository: "resource://gre/modules/addons/AddonRepository.sys.mjs",
});
function AddonUtilsInternal() {
this._log = Log.repository.getLogger("Sync.AddonUtils");
this._log.Level =
Log.Level[Svc.PrefBranch.getStringPref("log.logger.addonutils", null)];
}
AddonUtilsInternal.prototype = {
/**
* Obtain an AddonInstall object from an AddonSearchResult instance.
*
* The returned promise will be an AddonInstall on success or null (failure or
* addon not found)
*
* @param addon
* AddonSearchResult to obtain install from.
*/
getInstallFromSearchResult(addon) {
this._log.debug("Obtaining install for " + addon.id);
// We should theoretically be able to obtain (and use) addon.install if
// it is available. However, the addon.sourceURI rewriting won't be
// reflected in the AddonInstall, so we can't use it. If we ever get rid
// of sourceURI rewriting, we can avoid having to reconstruct the
// AddonInstall.
return lazy.AddonManager.getInstallForURL(addon.sourceURI.spec, {
name: addon.name,
icons: addon.iconURL,
version: addon.version,
telemetryInfo: { source: "sync" },
});
},
/**
* Installs an add-on from an AddonSearchResult instance.
*
* The options argument defines extra options to control the install.
* Recognized keys in this map are:
*
* syncGUID - Sync GUID to use for the new add-on.
* enabled - Boolean indicating whether the add-on should be enabled upon
* install.
*
* The result object has the following keys:
*
* id ID of add-on that was installed.
* install AddonInstall that was installed.
* addon Addon that was installed.
*
* @param addon
* AddonSearchResult to install add-on from.
* @param options
* Object with additional metadata describing how to install add-on.
*/
async installAddonFromSearchResult(addon, options) {
this._log.info("Trying to install add-on from search result: " + addon.id);
const install = await this.getInstallFromSearchResult(addon);
if (!install) {
throw new Error("AddonInstall not available: " + addon.id);
}
try {
this._log.info("Installing " + addon.id);
let log = this._log;
return new Promise((res, rej) => {
let listener = {
onInstallStarted: function onInstallStarted(install) {
if (!options) {
return;
}
if (options.syncGUID) {
log.info(
"Setting syncGUID of " + install.name + ": " + options.syncGUID
);
install.addon.syncGUID = options.syncGUID;
}
// We only need to change userDisabled if it is disabled because
// enabled is the default.
if ("enabled" in options && !options.enabled) {
log.info(
"Marking add-on as disabled for install: " + install.name
);
install.addon.disable();
}
},
onInstallEnded(install, addon) {
install.removeListener(listener);
res({ id: addon.id, install, addon });
},
onInstallFailed(install) {
install.removeListener(listener);
rej(new Error("Install failed: " + install.error));
},
onDownloadFailed(install) {
install.removeListener(listener);
rej(new Error("Download failed: " + install.error));
},
};
install.addListener(listener);
install.install();
});
} catch (ex) {
this._log.error("Error installing add-on", ex);
throw ex;
}
},
/**
* Uninstalls the addon instance.
*
* @param addon
* Addon instance to uninstall.
*/
async uninstallAddon(addon) {
return new Promise(res => {
let listener = {
onUninstalling(uninstalling, needsRestart) {
if (addon.id != uninstalling.id) {
return;
}
// We assume restartless add-ons will send the onUninstalled event
// soon.
if (!needsRestart) {
return;
}
// For non-restartless add-ons, we issue the callback on uninstalling
// because we will likely never see the uninstalled event.
lazy.AddonManager.removeAddonListener(listener);
res(addon);
},
onUninstalled(uninstalled) {
if (addon.id != uninstalled.id) {
return;
}
lazy.AddonManager.removeAddonListener(listener);
res(addon);
},
};
lazy.AddonManager.addAddonListener(listener);
addon.uninstall();
});
},
/**
* Installs multiple add-ons specified by metadata.
*
* The first argument is an array of objects. Each object must have the
* following keys:
*
* id - public ID of the add-on to install.
* syncGUID - syncGUID for new add-on.
* enabled - boolean indicating whether the add-on should be enabled.
* requireSecureURI - Boolean indicating whether to require a secure
* URI when installing from a remote location. This defaults to
* true.
*
* The callback will be called when activity on all add-ons is complete. The
* callback receives 2 arguments, error and result.
*
* If error is truthy, it contains a string describing the overall error.
*
* The 2nd argument to the callback is always an object with details on the
* overall execution state. It contains the following keys:
*
* installedIDs Array of add-on IDs that were installed.
* installs Array of AddonInstall instances that were installed.
* addons Array of Addon instances that were installed.
* errors Array of errors encountered. Only has elements if error is
* truthy.
*
* @param installs
* Array of objects describing add-ons to install.
*/
async installAddons(installs) {
let ids = [];
for (let addon of installs) {
ids.push(addon.id);
}
let addons = await lazy.AddonRepository.getAddonsByIDs(ids);
this._log.info(
`Found ${addons.length} / ${ids.length}` +
" add-ons during repository search."
);
let ourResult = {
installedIDs: [],
installs: [],
addons: [],
skipped: [],
errors: [],
};
let toInstall = [];
// Rewrite the "src" query string parameter of the source URI to note
// that the add-on was installed by Sync and not something else so
// server-side metrics aren't skewed (bug 708134). The server should
// ideally send proper URLs, but this solution was deemed too
// complicated at the time the functionality was implemented.
for (let addon of addons) {
// Find the specified options for this addon.
let options;
for (let install of installs) {
if (install.id == addon.id) {
options = install;
break;
}
}
if (!this.canInstallAddon(addon, options)) {
ourResult.skipped.push(addon.id);
continue;
}
// We can go ahead and attempt to install it.
toInstall.push(addon);
// We should always be able to QI the nsIURI to nsIURL. If not, we
// still try to install the add-on, but we don't rewrite the URL,
// potentially skewing metrics.
try {
addon.sourceURI.QueryInterface(Ci.nsIURL);
} catch (ex) {
this._log.warn(
"Unable to QI sourceURI to nsIURL: " + addon.sourceURI.spec
);
continue;
}
let params = addon.sourceURI.query
.split("&")
.map(function rewrite(param) {
if (param.indexOf("src=") == 0) {
return "src=sync";
}
return param;
});
addon.sourceURI = addon.sourceURI
.mutate()
.setQuery(params.join("&"))
.finalize();
}
if (!toInstall.length) {
return ourResult;
}
const installPromises = [];
// Start all the installs asynchronously. They will report back to us
// as they finish, eventually triggering the global callback.
for (let addon of toInstall) {
let options = {};
for (let install of installs) {
if (install.id == addon.id) {
options = install;
break;
}
}
installPromises.push(
(async () => {
try {
const result = await this.installAddonFromSearchResult(
addon,
options
);
ourResult.installedIDs.push(result.id);
ourResult.installs.push(result.install);
ourResult.addons.push(result.addon);
} catch (error) {
ourResult.errors.push(error);
}
})()
);
}
await Promise.all(installPromises);
if (ourResult.errors.length) {
throw new Error("1 or more add-ons failed to install");
}
return ourResult;
},
/**
* Returns true if we are able to install the specified addon, false
* otherwise. It is expected that this will log the reason if it returns
* false.
*
* @param addon
* (Addon) Add-on instance to check.
* @param options
* (object) The options specified for this addon. See installAddons()
* for the valid elements.
*/
canInstallAddon(addon, options) {
// sourceURI presence isn't enforced by AddonRepository. So, we skip
// add-ons without a sourceURI.
if (!addon.sourceURI) {
this._log.info(
"Skipping install of add-on because missing sourceURI: " + addon.id
);
return false;
}
// Verify that the source URI uses TLS. We don't allow installs from
// insecure sources for security reasons. The Addon Manager ensures
// that cert validation etc is performed.
// (We should also consider just dropping this entirely and calling
// XPIProvider.isInstallAllowed, but that has additional semantics we might
// need to think through...)
let requireSecureURI = true;
if (options && options.requireSecureURI !== undefined) {
requireSecureURI = options.requireSecureURI;
}
if (requireSecureURI) {
let scheme = addon.sourceURI.scheme;
if (scheme != "https") {
this._log.info(
`Skipping install of add-on "${addon.id}" because sourceURI's scheme of "${scheme}" is not trusted`
);
return false;
}
}
// Policy prevents either installing this addon or any addon
if (
Services.policies &&
(!Services.policies.mayInstallAddon(addon) ||
!Services.policies.isAllowed("xpinstall"))
) {
this._log.info(
`Skipping install of "${addon.id}" due to enterprise policy`
);
return false;
}
this._log.info(`Add-on "${addon.id}" is able to be installed`);
return true;
},
/**
* Update the user disabled flag for an add-on.
*
* If the new flag matches the existing or if the add-on
* isn't currently active, the function will return immediately.
*
* @param addon
* (Addon) Add-on instance to operate on.
* @param value
* (bool) New value for add-on's userDisabled property.
*/
updateUserDisabled(addon, value) {
if (addon.userDisabled == value) {
return;
}
this._log.info("Updating userDisabled flag: " + addon.id + " -> " + value);
if (value) {
addon.disable();
} else {
addon.enable();
}
},
};
export const AddonUtils = new AddonUtilsInternal();