2012-07-30 17:05:33 -07:00
|
|
|
/* 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";
|
|
|
|
|
2012-10-31 09:13:28 -07:00
|
|
|
this.EXPORTED_SYMBOLS = ["AddonUtils"];
|
2012-07-30 17:05:33 -07:00
|
|
|
|
|
|
|
const {interfaces: Ci, utils: Cu} = Components;
|
|
|
|
|
|
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
|
|
Cu.import("resource://services-common/log4moz.js");
|
2012-08-01 12:02:48 -07:00
|
|
|
Cu.import("resource://services-sync/util.js");
|
2012-07-30 17:05:33 -07:00
|
|
|
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
|
|
|
|
"resource://gre/modules/AddonManager.jsm");
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository",
|
|
|
|
"resource://gre/modules/AddonRepository.jsm");
|
|
|
|
|
|
|
|
function AddonUtilsInternal() {
|
|
|
|
this._log = Log4Moz.repository.getLogger("Sync.AddonUtils");
|
2012-08-01 12:02:48 -07:00
|
|
|
this._log.Level = Log4Moz.Level[Svc.Prefs.get("log.logger.addonutils")];
|
2012-07-30 17:05:33 -07:00
|
|
|
}
|
|
|
|
AddonUtilsInternal.prototype = {
|
|
|
|
/**
|
|
|
|
* Obtain an AddonInstall object from an AddonSearchResult instance.
|
|
|
|
*
|
|
|
|
* The callback will be invoked with the result of the operation. The
|
|
|
|
* callback receives 2 arguments, error and result. Error will be falsy
|
|
|
|
* on success or some kind of error value otherwise. The result argument
|
|
|
|
* will be an AddonInstall on success or null on failure. It is possible
|
|
|
|
* for the error to be falsy but result to be null. This could happen if
|
|
|
|
* an install was not found.
|
|
|
|
*
|
|
|
|
* @param addon
|
|
|
|
* AddonSearchResult to obtain install from.
|
|
|
|
* @param cb
|
|
|
|
* Function to be called with result of operation.
|
|
|
|
*/
|
|
|
|
getInstallFromSearchResult:
|
|
|
|
function getInstallFromSearchResult(addon, cb, requireSecureURI=true) {
|
|
|
|
|
|
|
|
this._log.debug("Obtaining install for " + addon.id);
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
if (requireSecureURI) {
|
|
|
|
let scheme = addon.sourceURI.scheme;
|
|
|
|
if (scheme != "https") {
|
|
|
|
cb(new Error("Insecure source URI scheme: " + scheme), addon.install);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
AddonManager.getInstallForURL(
|
|
|
|
addon.sourceURI.spec,
|
|
|
|
function handleInstall(install) {
|
|
|
|
cb(null, install);
|
|
|
|
},
|
|
|
|
"application/x-xpinstall",
|
|
|
|
undefined,
|
|
|
|
addon.name,
|
|
|
|
addon.iconURL,
|
|
|
|
addon.version
|
|
|
|
);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
* requireSecureURI - Boolean indicating whether to require a secure
|
|
|
|
* URI to install from. This defaults to true.
|
|
|
|
*
|
|
|
|
* When complete it calls a callback with 2 arguments, error and result.
|
|
|
|
*
|
|
|
|
* If error is falsy, result is an object. If error is truthy, result is
|
|
|
|
* null.
|
|
|
|
*
|
|
|
|
* 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.
|
|
|
|
* @param cb
|
|
|
|
* Function to be invoked with result of operation.
|
|
|
|
*/
|
|
|
|
installAddonFromSearchResult:
|
|
|
|
function installAddonFromSearchResult(addon, options, cb) {
|
|
|
|
this._log.info("Trying to install add-on from search result: " + addon.id);
|
|
|
|
|
|
|
|
if (options.requireSecureURI === undefined) {
|
|
|
|
options.requireSecureURI = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.getInstallFromSearchResult(addon, function onResult(error, install) {
|
|
|
|
if (error) {
|
|
|
|
cb(error, null);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!install) {
|
|
|
|
cb(new Error("AddonInstall not available: " + addon.id), null);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
this._log.info("Installing " + addon.id);
|
|
|
|
let log = this._log;
|
|
|
|
|
|
|
|
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.userDisabled = true;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
onInstallEnded: function(install, addon) {
|
|
|
|
install.removeListener(listener);
|
|
|
|
|
|
|
|
cb(null, {id: addon.id, install: install, addon: addon});
|
|
|
|
},
|
|
|
|
onInstallFailed: function(install) {
|
|
|
|
install.removeListener(listener);
|
|
|
|
|
|
|
|
cb(new Error("Install failed: " + install.error), null);
|
|
|
|
},
|
|
|
|
onDownloadFailed: function(install) {
|
|
|
|
install.removeListener(listener);
|
|
|
|
|
|
|
|
cb(new Error("Download failed: " + install.error), null);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
install.addListener(listener);
|
|
|
|
install.install();
|
|
|
|
}
|
|
|
|
catch (ex) {
|
|
|
|
this._log.error("Error installing add-on: " + Utils.exceptionstr(ex));
|
|
|
|
cb(ex, null);
|
|
|
|
}
|
|
|
|
}.bind(this), options.requireSecureURI);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Uninstalls the Addon instance and invoke a callback when it is done.
|
|
|
|
*
|
|
|
|
* @param addon
|
|
|
|
* Addon instance to uninstall.
|
|
|
|
* @param cb
|
|
|
|
* Function to be invoked when uninstall has finished. It receives a
|
|
|
|
* truthy value signifying error and the add-on which was uninstalled.
|
|
|
|
*/
|
|
|
|
uninstallAddon: function uninstallAddon(addon, cb) {
|
|
|
|
let listener = {
|
|
|
|
onUninstalling: function(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.
|
|
|
|
AddonManager.removeAddonListener(listener);
|
|
|
|
cb(null, addon);
|
|
|
|
},
|
|
|
|
onUninstalled: function(uninstalled) {
|
|
|
|
if (addon.id != uninstalled.id) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
AddonManager.removeAddonListener(listener);
|
|
|
|
cb(null, addon);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
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.
|
|
|
|
* @param cb
|
|
|
|
* Function to be called when all actions are complete.
|
|
|
|
*/
|
|
|
|
installAddons: function installAddons(installs, cb) {
|
|
|
|
if (!cb) {
|
|
|
|
throw new Error("Invalid argument: cb is not defined.");
|
|
|
|
}
|
|
|
|
|
|
|
|
let ids = [];
|
|
|
|
for each (let addon in installs) {
|
|
|
|
ids.push(addon.id);
|
|
|
|
}
|
|
|
|
|
|
|
|
AddonRepository.getAddonsByIDs(ids, {
|
|
|
|
searchSucceeded: function searchSucceeded(addons, addonsLength, total) {
|
|
|
|
this._log.info("Found " + addonsLength + "/" + ids.length +
|
|
|
|
" add-ons during repository search.");
|
|
|
|
|
|
|
|
let ourResult = {
|
|
|
|
installedIDs: [],
|
|
|
|
installs: [],
|
|
|
|
addons: [],
|
|
|
|
errors: []
|
|
|
|
};
|
|
|
|
|
|
|
|
if (!addonsLength) {
|
|
|
|
cb(null, ourResult);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let expectedInstallCount = 0;
|
|
|
|
let finishedCount = 0;
|
|
|
|
let installCallback = function installCallback(error, result) {
|
|
|
|
finishedCount++;
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
ourResult.errors.push(error);
|
|
|
|
} else {
|
|
|
|
ourResult.installedIDs.push(result.id);
|
|
|
|
ourResult.installs.push(result.install);
|
|
|
|
ourResult.addons.push(result.addon);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (finishedCount >= expectedInstallCount) {
|
|
|
|
if (ourResult.errors.length > 0) {
|
|
|
|
cb(new Error("1 or more add-ons failed to install"), ourResult);
|
|
|
|
} else {
|
|
|
|
cb(null, ourResult);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}.bind(this);
|
|
|
|
|
|
|
|
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 each (let addon in addons) {
|
|
|
|
// 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);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
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";
|
|
|
|
} else {
|
|
|
|
return param;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
addon.sourceURI.query = params.join("&");
|
|
|
|
}
|
|
|
|
|
|
|
|
expectedInstallCount = toInstall.length;
|
|
|
|
|
|
|
|
if (!expectedInstallCount) {
|
|
|
|
cb(null, ourResult);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Start all the installs asynchronously. They will report back to us
|
|
|
|
// as they finish, eventually triggering the global callback.
|
|
|
|
for each (let addon in toInstall) {
|
|
|
|
let options = {};
|
|
|
|
for each (let install in installs) {
|
|
|
|
if (install.id == addon.id) {
|
|
|
|
options = install;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.installAddonFromSearchResult(addon, options, installCallback);
|
|
|
|
}
|
|
|
|
|
|
|
|
}.bind(this),
|
|
|
|
|
|
|
|
searchFailed: function searchFailed() {
|
|
|
|
cb(new Error("AddonRepository search failed"), null);
|
|
|
|
},
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update the user disabled flag for an add-on.
|
|
|
|
*
|
|
|
|
* The supplied callback will ba called when the operation is
|
|
|
|
* complete. If the new flag matches the existing or if the add-on
|
|
|
|
* isn't currently active, the function will fire the callback
|
|
|
|
* immediately. Else, the callback is invoked when the AddonManager
|
|
|
|
* reports the change has taken effect or has been registered.
|
|
|
|
*
|
|
|
|
* The callback receives as arguments:
|
|
|
|
*
|
|
|
|
* (Error) Encountered error during operation or null on success.
|
|
|
|
* (Addon) The add-on instance being operated on.
|
|
|
|
*
|
|
|
|
* @param addon
|
|
|
|
* (Addon) Add-on instance to operate on.
|
|
|
|
* @param value
|
|
|
|
* (bool) New value for add-on's userDisabled property.
|
|
|
|
* @param cb
|
|
|
|
* (function) Callback to be invoked on completion.
|
|
|
|
*/
|
|
|
|
updateUserDisabled: function updateUserDisabled(addon, value, cb) {
|
|
|
|
if (addon.userDisabled == value) {
|
|
|
|
cb(null, addon);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let listener = {
|
|
|
|
onEnabling: function onEnabling(wrapper, needsRestart) {
|
|
|
|
this._log.debug("onEnabling: " + wrapper.id);
|
|
|
|
if (wrapper.id != addon.id) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// We ignore the restartless case because we'll get onEnabled shortly.
|
|
|
|
if (!needsRestart) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
AddonManager.removeAddonListener(listener);
|
|
|
|
cb(null, wrapper);
|
|
|
|
}.bind(this),
|
|
|
|
|
|
|
|
onEnabled: function onEnabled(wrapper) {
|
|
|
|
this._log.debug("onEnabled: " + wrapper.id);
|
|
|
|
if (wrapper.id != addon.id) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
AddonManager.removeAddonListener(listener);
|
|
|
|
cb(null, wrapper);
|
|
|
|
}.bind(this),
|
|
|
|
|
|
|
|
onDisabling: function onDisabling(wrapper, needsRestart) {
|
|
|
|
this._log.debug("onDisabling: " + wrapper.id);
|
|
|
|
if (wrapper.id != addon.id) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!needsRestart) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
AddonManager.removeAddonListener(listener);
|
|
|
|
cb(null, wrapper);
|
|
|
|
}.bind(this),
|
|
|
|
|
|
|
|
onDisabled: function onDisabled(wrapper) {
|
|
|
|
this._log.debug("onDisabled: " + wrapper.id);
|
|
|
|
if (wrapper.id != addon.id) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
AddonManager.removeAddonListener(listener);
|
|
|
|
cb(null, wrapper);
|
|
|
|
}.bind(this),
|
|
|
|
|
|
|
|
onOperationCancelled: function onOperationCancelled(wrapper) {
|
|
|
|
this._log.debug("onOperationCancelled: " + wrapper.id);
|
|
|
|
if (wrapper.id != addon.id) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
AddonManager.removeAddonListener(listener);
|
|
|
|
cb(new Error("Operation cancelled"), wrapper);
|
|
|
|
}.bind(this)
|
|
|
|
};
|
|
|
|
|
|
|
|
// The add-on listeners are only fired if the add-on is active. If not, the
|
|
|
|
// change is silently updated and made active when/if the add-on is active.
|
|
|
|
|
|
|
|
if (!addon.appDisabled) {
|
|
|
|
AddonManager.addAddonListener(listener);
|
|
|
|
}
|
|
|
|
|
|
|
|
this._log.info("Updating userDisabled flag: " + addon.id + " -> " + value);
|
|
|
|
addon.userDisabled = !!value;
|
|
|
|
|
|
|
|
if (!addon.appDisabled) {
|
|
|
|
cb(null, addon);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Else the listener will handle invoking the callback.
|
|
|
|
},
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "AddonUtils", function() {
|
|
|
|
return new AddonUtilsInternal();
|
|
|
|
});
|