services/sync/modules/addonutils.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 "use strict";
michael@0 6
michael@0 7 this.EXPORTED_SYMBOLS = ["AddonUtils"];
michael@0 8
michael@0 9 const {interfaces: Ci, utils: Cu} = Components;
michael@0 10
michael@0 11 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 12 Cu.import("resource://gre/modules/Log.jsm");
michael@0 13 Cu.import("resource://services-sync/util.js");
michael@0 14
michael@0 15 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
michael@0 16 "resource://gre/modules/AddonManager.jsm");
michael@0 17 XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository",
michael@0 18 "resource://gre/modules/addons/AddonRepository.jsm");
michael@0 19
michael@0 20 function AddonUtilsInternal() {
michael@0 21 this._log = Log.repository.getLogger("Sync.AddonUtils");
michael@0 22 this._log.Level = Log.Level[Svc.Prefs.get("log.logger.addonutils")];
michael@0 23 }
michael@0 24 AddonUtilsInternal.prototype = {
michael@0 25 /**
michael@0 26 * Obtain an AddonInstall object from an AddonSearchResult instance.
michael@0 27 *
michael@0 28 * The callback will be invoked with the result of the operation. The
michael@0 29 * callback receives 2 arguments, error and result. Error will be falsy
michael@0 30 * on success or some kind of error value otherwise. The result argument
michael@0 31 * will be an AddonInstall on success or null on failure. It is possible
michael@0 32 * for the error to be falsy but result to be null. This could happen if
michael@0 33 * an install was not found.
michael@0 34 *
michael@0 35 * @param addon
michael@0 36 * AddonSearchResult to obtain install from.
michael@0 37 * @param cb
michael@0 38 * Function to be called with result of operation.
michael@0 39 */
michael@0 40 getInstallFromSearchResult:
michael@0 41 function getInstallFromSearchResult(addon, cb, requireSecureURI=true) {
michael@0 42
michael@0 43 this._log.debug("Obtaining install for " + addon.id);
michael@0 44
michael@0 45 // Verify that the source URI uses TLS. We don't allow installs from
michael@0 46 // insecure sources for security reasons. The Addon Manager ensures that
michael@0 47 // cert validation, etc is performed.
michael@0 48 if (requireSecureURI) {
michael@0 49 let scheme = addon.sourceURI.scheme;
michael@0 50 if (scheme != "https") {
michael@0 51 cb(new Error("Insecure source URI scheme: " + scheme), addon.install);
michael@0 52 return;
michael@0 53 }
michael@0 54 }
michael@0 55
michael@0 56 // We should theoretically be able to obtain (and use) addon.install if
michael@0 57 // it is available. However, the addon.sourceURI rewriting won't be
michael@0 58 // reflected in the AddonInstall, so we can't use it. If we ever get rid
michael@0 59 // of sourceURI rewriting, we can avoid having to reconstruct the
michael@0 60 // AddonInstall.
michael@0 61 AddonManager.getInstallForURL(
michael@0 62 addon.sourceURI.spec,
michael@0 63 function handleInstall(install) {
michael@0 64 cb(null, install);
michael@0 65 },
michael@0 66 "application/x-xpinstall",
michael@0 67 undefined,
michael@0 68 addon.name,
michael@0 69 addon.iconURL,
michael@0 70 addon.version
michael@0 71 );
michael@0 72 },
michael@0 73
michael@0 74 /**
michael@0 75 * Installs an add-on from an AddonSearchResult instance.
michael@0 76 *
michael@0 77 * The options argument defines extra options to control the install.
michael@0 78 * Recognized keys in this map are:
michael@0 79 *
michael@0 80 * syncGUID - Sync GUID to use for the new add-on.
michael@0 81 * enabled - Boolean indicating whether the add-on should be enabled upon
michael@0 82 * install.
michael@0 83 * requireSecureURI - Boolean indicating whether to require a secure
michael@0 84 * URI to install from. This defaults to true.
michael@0 85 *
michael@0 86 * When complete it calls a callback with 2 arguments, error and result.
michael@0 87 *
michael@0 88 * If error is falsy, result is an object. If error is truthy, result is
michael@0 89 * null.
michael@0 90 *
michael@0 91 * The result object has the following keys:
michael@0 92 *
michael@0 93 * id ID of add-on that was installed.
michael@0 94 * install AddonInstall that was installed.
michael@0 95 * addon Addon that was installed.
michael@0 96 *
michael@0 97 * @param addon
michael@0 98 * AddonSearchResult to install add-on from.
michael@0 99 * @param options
michael@0 100 * Object with additional metadata describing how to install add-on.
michael@0 101 * @param cb
michael@0 102 * Function to be invoked with result of operation.
michael@0 103 */
michael@0 104 installAddonFromSearchResult:
michael@0 105 function installAddonFromSearchResult(addon, options, cb) {
michael@0 106 this._log.info("Trying to install add-on from search result: " + addon.id);
michael@0 107
michael@0 108 if (options.requireSecureURI === undefined) {
michael@0 109 options.requireSecureURI = true;
michael@0 110 }
michael@0 111
michael@0 112 this.getInstallFromSearchResult(addon, function onResult(error, install) {
michael@0 113 if (error) {
michael@0 114 cb(error, null);
michael@0 115 return;
michael@0 116 }
michael@0 117
michael@0 118 if (!install) {
michael@0 119 cb(new Error("AddonInstall not available: " + addon.id), null);
michael@0 120 return;
michael@0 121 }
michael@0 122
michael@0 123 try {
michael@0 124 this._log.info("Installing " + addon.id);
michael@0 125 let log = this._log;
michael@0 126
michael@0 127 let listener = {
michael@0 128 onInstallStarted: function onInstallStarted(install) {
michael@0 129 if (!options) {
michael@0 130 return;
michael@0 131 }
michael@0 132
michael@0 133 if (options.syncGUID) {
michael@0 134 log.info("Setting syncGUID of " + install.name +": " +
michael@0 135 options.syncGUID);
michael@0 136 install.addon.syncGUID = options.syncGUID;
michael@0 137 }
michael@0 138
michael@0 139 // We only need to change userDisabled if it is disabled because
michael@0 140 // enabled is the default.
michael@0 141 if ("enabled" in options && !options.enabled) {
michael@0 142 log.info("Marking add-on as disabled for install: " +
michael@0 143 install.name);
michael@0 144 install.addon.userDisabled = true;
michael@0 145 }
michael@0 146 },
michael@0 147 onInstallEnded: function(install, addon) {
michael@0 148 install.removeListener(listener);
michael@0 149
michael@0 150 cb(null, {id: addon.id, install: install, addon: addon});
michael@0 151 },
michael@0 152 onInstallFailed: function(install) {
michael@0 153 install.removeListener(listener);
michael@0 154
michael@0 155 cb(new Error("Install failed: " + install.error), null);
michael@0 156 },
michael@0 157 onDownloadFailed: function(install) {
michael@0 158 install.removeListener(listener);
michael@0 159
michael@0 160 cb(new Error("Download failed: " + install.error), null);
michael@0 161 }
michael@0 162 };
michael@0 163 install.addListener(listener);
michael@0 164 install.install();
michael@0 165 }
michael@0 166 catch (ex) {
michael@0 167 this._log.error("Error installing add-on: " + Utils.exceptionstr(ex));
michael@0 168 cb(ex, null);
michael@0 169 }
michael@0 170 }.bind(this), options.requireSecureURI);
michael@0 171 },
michael@0 172
michael@0 173 /**
michael@0 174 * Uninstalls the Addon instance and invoke a callback when it is done.
michael@0 175 *
michael@0 176 * @param addon
michael@0 177 * Addon instance to uninstall.
michael@0 178 * @param cb
michael@0 179 * Function to be invoked when uninstall has finished. It receives a
michael@0 180 * truthy value signifying error and the add-on which was uninstalled.
michael@0 181 */
michael@0 182 uninstallAddon: function uninstallAddon(addon, cb) {
michael@0 183 let listener = {
michael@0 184 onUninstalling: function(uninstalling, needsRestart) {
michael@0 185 if (addon.id != uninstalling.id) {
michael@0 186 return;
michael@0 187 }
michael@0 188
michael@0 189 // We assume restartless add-ons will send the onUninstalled event
michael@0 190 // soon.
michael@0 191 if (!needsRestart) {
michael@0 192 return;
michael@0 193 }
michael@0 194
michael@0 195 // For non-restartless add-ons, we issue the callback on uninstalling
michael@0 196 // because we will likely never see the uninstalled event.
michael@0 197 AddonManager.removeAddonListener(listener);
michael@0 198 cb(null, addon);
michael@0 199 },
michael@0 200 onUninstalled: function(uninstalled) {
michael@0 201 if (addon.id != uninstalled.id) {
michael@0 202 return;
michael@0 203 }
michael@0 204
michael@0 205 AddonManager.removeAddonListener(listener);
michael@0 206 cb(null, addon);
michael@0 207 }
michael@0 208 };
michael@0 209 AddonManager.addAddonListener(listener);
michael@0 210 addon.uninstall();
michael@0 211 },
michael@0 212
michael@0 213 /**
michael@0 214 * Installs multiple add-ons specified by metadata.
michael@0 215 *
michael@0 216 * The first argument is an array of objects. Each object must have the
michael@0 217 * following keys:
michael@0 218 *
michael@0 219 * id - public ID of the add-on to install.
michael@0 220 * syncGUID - syncGUID for new add-on.
michael@0 221 * enabled - boolean indicating whether the add-on should be enabled.
michael@0 222 * requireSecureURI - Boolean indicating whether to require a secure
michael@0 223 * URI when installing from a remote location. This defaults to
michael@0 224 * true.
michael@0 225 *
michael@0 226 * The callback will be called when activity on all add-ons is complete. The
michael@0 227 * callback receives 2 arguments, error and result.
michael@0 228 *
michael@0 229 * If error is truthy, it contains a string describing the overall error.
michael@0 230 *
michael@0 231 * The 2nd argument to the callback is always an object with details on the
michael@0 232 * overall execution state. It contains the following keys:
michael@0 233 *
michael@0 234 * installedIDs Array of add-on IDs that were installed.
michael@0 235 * installs Array of AddonInstall instances that were installed.
michael@0 236 * addons Array of Addon instances that were installed.
michael@0 237 * errors Array of errors encountered. Only has elements if error is
michael@0 238 * truthy.
michael@0 239 *
michael@0 240 * @param installs
michael@0 241 * Array of objects describing add-ons to install.
michael@0 242 * @param cb
michael@0 243 * Function to be called when all actions are complete.
michael@0 244 */
michael@0 245 installAddons: function installAddons(installs, cb) {
michael@0 246 if (!cb) {
michael@0 247 throw new Error("Invalid argument: cb is not defined.");
michael@0 248 }
michael@0 249
michael@0 250 let ids = [];
michael@0 251 for each (let addon in installs) {
michael@0 252 ids.push(addon.id);
michael@0 253 }
michael@0 254
michael@0 255 AddonRepository.getAddonsByIDs(ids, {
michael@0 256 searchSucceeded: function searchSucceeded(addons, addonsLength, total) {
michael@0 257 this._log.info("Found " + addonsLength + "/" + ids.length +
michael@0 258 " add-ons during repository search.");
michael@0 259
michael@0 260 let ourResult = {
michael@0 261 installedIDs: [],
michael@0 262 installs: [],
michael@0 263 addons: [],
michael@0 264 errors: []
michael@0 265 };
michael@0 266
michael@0 267 if (!addonsLength) {
michael@0 268 cb(null, ourResult);
michael@0 269 return;
michael@0 270 }
michael@0 271
michael@0 272 let expectedInstallCount = 0;
michael@0 273 let finishedCount = 0;
michael@0 274 let installCallback = function installCallback(error, result) {
michael@0 275 finishedCount++;
michael@0 276
michael@0 277 if (error) {
michael@0 278 ourResult.errors.push(error);
michael@0 279 } else {
michael@0 280 ourResult.installedIDs.push(result.id);
michael@0 281 ourResult.installs.push(result.install);
michael@0 282 ourResult.addons.push(result.addon);
michael@0 283 }
michael@0 284
michael@0 285 if (finishedCount >= expectedInstallCount) {
michael@0 286 if (ourResult.errors.length > 0) {
michael@0 287 cb(new Error("1 or more add-ons failed to install"), ourResult);
michael@0 288 } else {
michael@0 289 cb(null, ourResult);
michael@0 290 }
michael@0 291 }
michael@0 292 }.bind(this);
michael@0 293
michael@0 294 let toInstall = [];
michael@0 295
michael@0 296 // Rewrite the "src" query string parameter of the source URI to note
michael@0 297 // that the add-on was installed by Sync and not something else so
michael@0 298 // server-side metrics aren't skewed (bug 708134). The server should
michael@0 299 // ideally send proper URLs, but this solution was deemed too
michael@0 300 // complicated at the time the functionality was implemented.
michael@0 301 for each (let addon in addons) {
michael@0 302 // sourceURI presence isn't enforced by AddonRepository. So, we skip
michael@0 303 // add-ons without a sourceURI.
michael@0 304 if (!addon.sourceURI) {
michael@0 305 this._log.info("Skipping install of add-on because missing " +
michael@0 306 "sourceURI: " + addon.id);
michael@0 307 continue;
michael@0 308 }
michael@0 309
michael@0 310 toInstall.push(addon);
michael@0 311
michael@0 312 // We should always be able to QI the nsIURI to nsIURL. If not, we
michael@0 313 // still try to install the add-on, but we don't rewrite the URL,
michael@0 314 // potentially skewing metrics.
michael@0 315 try {
michael@0 316 addon.sourceURI.QueryInterface(Ci.nsIURL);
michael@0 317 } catch (ex) {
michael@0 318 this._log.warn("Unable to QI sourceURI to nsIURL: " +
michael@0 319 addon.sourceURI.spec);
michael@0 320 continue;
michael@0 321 }
michael@0 322
michael@0 323 let params = addon.sourceURI.query.split("&").map(
michael@0 324 function rewrite(param) {
michael@0 325
michael@0 326 if (param.indexOf("src=") == 0) {
michael@0 327 return "src=sync";
michael@0 328 } else {
michael@0 329 return param;
michael@0 330 }
michael@0 331 });
michael@0 332
michael@0 333 addon.sourceURI.query = params.join("&");
michael@0 334 }
michael@0 335
michael@0 336 expectedInstallCount = toInstall.length;
michael@0 337
michael@0 338 if (!expectedInstallCount) {
michael@0 339 cb(null, ourResult);
michael@0 340 return;
michael@0 341 }
michael@0 342
michael@0 343 // Start all the installs asynchronously. They will report back to us
michael@0 344 // as they finish, eventually triggering the global callback.
michael@0 345 for each (let addon in toInstall) {
michael@0 346 let options = {};
michael@0 347 for each (let install in installs) {
michael@0 348 if (install.id == addon.id) {
michael@0 349 options = install;
michael@0 350 break;
michael@0 351 }
michael@0 352 }
michael@0 353
michael@0 354 this.installAddonFromSearchResult(addon, options, installCallback);
michael@0 355 }
michael@0 356
michael@0 357 }.bind(this),
michael@0 358
michael@0 359 searchFailed: function searchFailed() {
michael@0 360 cb(new Error("AddonRepository search failed"), null);
michael@0 361 },
michael@0 362 });
michael@0 363 },
michael@0 364
michael@0 365 /**
michael@0 366 * Update the user disabled flag for an add-on.
michael@0 367 *
michael@0 368 * The supplied callback will ba called when the operation is
michael@0 369 * complete. If the new flag matches the existing or if the add-on
michael@0 370 * isn't currently active, the function will fire the callback
michael@0 371 * immediately. Else, the callback is invoked when the AddonManager
michael@0 372 * reports the change has taken effect or has been registered.
michael@0 373 *
michael@0 374 * The callback receives as arguments:
michael@0 375 *
michael@0 376 * (Error) Encountered error during operation or null on success.
michael@0 377 * (Addon) The add-on instance being operated on.
michael@0 378 *
michael@0 379 * @param addon
michael@0 380 * (Addon) Add-on instance to operate on.
michael@0 381 * @param value
michael@0 382 * (bool) New value for add-on's userDisabled property.
michael@0 383 * @param cb
michael@0 384 * (function) Callback to be invoked on completion.
michael@0 385 */
michael@0 386 updateUserDisabled: function updateUserDisabled(addon, value, cb) {
michael@0 387 if (addon.userDisabled == value) {
michael@0 388 cb(null, addon);
michael@0 389 return;
michael@0 390 }
michael@0 391
michael@0 392 let listener = {
michael@0 393 onEnabling: function onEnabling(wrapper, needsRestart) {
michael@0 394 this._log.debug("onEnabling: " + wrapper.id);
michael@0 395 if (wrapper.id != addon.id) {
michael@0 396 return;
michael@0 397 }
michael@0 398
michael@0 399 // We ignore the restartless case because we'll get onEnabled shortly.
michael@0 400 if (!needsRestart) {
michael@0 401 return;
michael@0 402 }
michael@0 403
michael@0 404 AddonManager.removeAddonListener(listener);
michael@0 405 cb(null, wrapper);
michael@0 406 }.bind(this),
michael@0 407
michael@0 408 onEnabled: function onEnabled(wrapper) {
michael@0 409 this._log.debug("onEnabled: " + wrapper.id);
michael@0 410 if (wrapper.id != addon.id) {
michael@0 411 return;
michael@0 412 }
michael@0 413
michael@0 414 AddonManager.removeAddonListener(listener);
michael@0 415 cb(null, wrapper);
michael@0 416 }.bind(this),
michael@0 417
michael@0 418 onDisabling: function onDisabling(wrapper, needsRestart) {
michael@0 419 this._log.debug("onDisabling: " + wrapper.id);
michael@0 420 if (wrapper.id != addon.id) {
michael@0 421 return;
michael@0 422 }
michael@0 423
michael@0 424 if (!needsRestart) {
michael@0 425 return;
michael@0 426 }
michael@0 427
michael@0 428 AddonManager.removeAddonListener(listener);
michael@0 429 cb(null, wrapper);
michael@0 430 }.bind(this),
michael@0 431
michael@0 432 onDisabled: function onDisabled(wrapper) {
michael@0 433 this._log.debug("onDisabled: " + wrapper.id);
michael@0 434 if (wrapper.id != addon.id) {
michael@0 435 return;
michael@0 436 }
michael@0 437
michael@0 438 AddonManager.removeAddonListener(listener);
michael@0 439 cb(null, wrapper);
michael@0 440 }.bind(this),
michael@0 441
michael@0 442 onOperationCancelled: function onOperationCancelled(wrapper) {
michael@0 443 this._log.debug("onOperationCancelled: " + wrapper.id);
michael@0 444 if (wrapper.id != addon.id) {
michael@0 445 return;
michael@0 446 }
michael@0 447
michael@0 448 AddonManager.removeAddonListener(listener);
michael@0 449 cb(new Error("Operation cancelled"), wrapper);
michael@0 450 }.bind(this)
michael@0 451 };
michael@0 452
michael@0 453 // The add-on listeners are only fired if the add-on is active. If not, the
michael@0 454 // change is silently updated and made active when/if the add-on is active.
michael@0 455
michael@0 456 if (!addon.appDisabled) {
michael@0 457 AddonManager.addAddonListener(listener);
michael@0 458 }
michael@0 459
michael@0 460 this._log.info("Updating userDisabled flag: " + addon.id + " -> " + value);
michael@0 461 addon.userDisabled = !!value;
michael@0 462
michael@0 463 if (!addon.appDisabled) {
michael@0 464 cb(null, addon);
michael@0 465 return;
michael@0 466 }
michael@0 467 // Else the listener will handle invoking the callback.
michael@0 468 },
michael@0 469
michael@0 470 };
michael@0 471
michael@0 472 XPCOMUtils.defineLazyGetter(this, "AddonUtils", function() {
michael@0 473 return new AddonUtilsInternal();
michael@0 474 });

mercurial