toolkit/mozapps/extensions/internal/XPIProvider.jsm

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.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 "use strict";
     7 const Cc = Components.classes;
     8 const Ci = Components.interfaces;
     9 const Cr = Components.results;
    10 const Cu = Components.utils;
    12 this.EXPORTED_SYMBOLS = ["XPIProvider"];
    14 Components.utils.import("resource://gre/modules/Services.jsm");
    15 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
    16 Components.utils.import("resource://gre/modules/AddonManager.jsm");
    18 XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository",
    19                                   "resource://gre/modules/addons/AddonRepository.jsm");
    20 XPCOMUtils.defineLazyModuleGetter(this, "ChromeManifestParser",
    21                                   "resource://gre/modules/ChromeManifestParser.jsm");
    22 XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
    23                                   "resource://gre/modules/LightweightThemeManager.jsm");
    24 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
    25                                   "resource://gre/modules/FileUtils.jsm");
    26 XPCOMUtils.defineLazyModuleGetter(this, "ZipUtils",
    27                                   "resource://gre/modules/ZipUtils.jsm");
    28 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
    29                                   "resource://gre/modules/NetUtil.jsm");
    30 XPCOMUtils.defineLazyModuleGetter(this, "PermissionsUtils",
    31                                   "resource://gre/modules/PermissionsUtils.jsm");
    32 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
    33                                   "resource://gre/modules/Promise.jsm");
    34 XPCOMUtils.defineLazyModuleGetter(this, "Task",
    35                                   "resource://gre/modules/Task.jsm");
    36 XPCOMUtils.defineLazyModuleGetter(this, "OS",
    37                                   "resource://gre/modules/osfile.jsm");
    38 XPCOMUtils.defineLazyModuleGetter(this, "BrowserToolboxProcess",
    39                                   "resource:///modules/devtools/ToolboxProcess.jsm");
    41 XPCOMUtils.defineLazyServiceGetter(this,
    42                                    "ChromeRegistry",
    43                                    "@mozilla.org/chrome/chrome-registry;1",
    44                                    "nsIChromeRegistry");
    45 XPCOMUtils.defineLazyServiceGetter(this,
    46                                    "ResProtocolHandler",
    47                                    "@mozilla.org/network/protocol;1?name=resource",
    48                                    "nsIResProtocolHandler");
    50 const nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile",
    51                                        "initWithPath");
    53 const PREF_DB_SCHEMA                  = "extensions.databaseSchema";
    54 const PREF_INSTALL_CACHE              = "extensions.installCache";
    55 const PREF_BOOTSTRAP_ADDONS           = "extensions.bootstrappedAddons";
    56 const PREF_PENDING_OPERATIONS         = "extensions.pendingOperations";
    57 const PREF_MATCH_OS_LOCALE            = "intl.locale.matchOS";
    58 const PREF_SELECTED_LOCALE            = "general.useragent.locale";
    59 const PREF_EM_DSS_ENABLED             = "extensions.dss.enabled";
    60 const PREF_DSS_SWITCHPENDING          = "extensions.dss.switchPending";
    61 const PREF_DSS_SKIN_TO_SELECT         = "extensions.lastSelectedSkin";
    62 const PREF_GENERAL_SKINS_SELECTEDSKIN = "general.skins.selectedSkin";
    63 const PREF_EM_UPDATE_URL              = "extensions.update.url";
    64 const PREF_EM_UPDATE_BACKGROUND_URL   = "extensions.update.background.url";
    65 const PREF_EM_ENABLED_ADDONS          = "extensions.enabledAddons";
    66 const PREF_EM_EXTENSION_FORMAT        = "extensions.";
    67 const PREF_EM_ENABLED_SCOPES          = "extensions.enabledScopes";
    68 const PREF_EM_AUTO_DISABLED_SCOPES    = "extensions.autoDisableScopes";
    69 const PREF_EM_SHOW_MISMATCH_UI        = "extensions.showMismatchUI";
    70 const PREF_XPI_ENABLED                = "xpinstall.enabled";
    71 const PREF_XPI_WHITELIST_REQUIRED     = "xpinstall.whitelist.required";
    72 const PREF_XPI_DIRECT_WHITELISTED     = "xpinstall.whitelist.directRequest";
    73 const PREF_XPI_FILE_WHITELISTED       = "xpinstall.whitelist.fileRequest";
    74 const PREF_XPI_PERMISSIONS_BRANCH     = "xpinstall.";
    75 const PREF_XPI_UNPACK                 = "extensions.alwaysUnpack";
    76 const PREF_INSTALL_REQUIREBUILTINCERTS = "extensions.install.requireBuiltInCerts";
    77 const PREF_INSTALL_DISTRO_ADDONS      = "extensions.installDistroAddons";
    78 const PREF_BRANCH_INSTALLED_ADDON     = "extensions.installedDistroAddon.";
    79 const PREF_SHOWN_SELECTION_UI         = "extensions.shownSelectionUI";
    81 const PREF_EM_MIN_COMPAT_APP_VERSION      = "extensions.minCompatibleAppVersion";
    82 const PREF_EM_MIN_COMPAT_PLATFORM_VERSION = "extensions.minCompatiblePlatformVersion";
    84 const PREF_CHECKCOMAT_THEMEOVERRIDE   = "extensions.checkCompatibility.temporaryThemeOverride_minAppVersion";
    86 const URI_EXTENSION_SELECT_DIALOG     = "chrome://mozapps/content/extensions/selectAddons.xul";
    87 const URI_EXTENSION_UPDATE_DIALOG     = "chrome://mozapps/content/extensions/update.xul";
    88 const URI_EXTENSION_STRINGS           = "chrome://mozapps/locale/extensions/extensions.properties";
    90 const STRING_TYPE_NAME                = "type.%ID%.name";
    92 const DIR_EXTENSIONS                  = "extensions";
    93 const DIR_STAGE                       = "staged";
    94 const DIR_XPI_STAGE                   = "staged-xpis";
    95 const DIR_TRASH                       = "trash";
    97 const FILE_DATABASE                   = "extensions.json";
    98 const FILE_OLD_CACHE                  = "extensions.cache";
    99 const FILE_INSTALL_MANIFEST           = "install.rdf";
   100 const FILE_XPI_ADDONS_LIST            = "extensions.ini";
   102 const KEY_PROFILEDIR                  = "ProfD";
   103 const KEY_APPDIR                      = "XCurProcD";
   104 const KEY_TEMPDIR                     = "TmpD";
   105 const KEY_APP_DISTRIBUTION            = "XREAppDist";
   107 const KEY_APP_PROFILE                 = "app-profile";
   108 const KEY_APP_GLOBAL                  = "app-global";
   109 const KEY_APP_SYSTEM_LOCAL            = "app-system-local";
   110 const KEY_APP_SYSTEM_SHARE            = "app-system-share";
   111 const KEY_APP_SYSTEM_USER             = "app-system-user";
   113 const NOTIFICATION_FLUSH_PERMISSIONS  = "flush-pending-permissions";
   114 const XPI_PERMISSION                  = "install";
   116 const RDFURI_INSTALL_MANIFEST_ROOT    = "urn:mozilla:install-manifest";
   117 const PREFIX_NS_EM                    = "http://www.mozilla.org/2004/em-rdf#";
   119 const TOOLKIT_ID                      = "toolkit@mozilla.org";
   121 // The value for this is in Makefile.in
   122 #expand const DB_SCHEMA                       = __MOZ_EXTENSIONS_DB_SCHEMA__;
   124 // Properties that exist in the install manifest
   125 const PROP_METADATA      = ["id", "version", "type", "internalName", "updateURL",
   126                             "updateKey", "optionsURL", "optionsType", "aboutURL",
   127                             "iconURL", "icon64URL"];
   128 const PROP_LOCALE_SINGLE = ["name", "description", "creator", "homepageURL"];
   129 const PROP_LOCALE_MULTI  = ["developers", "translators", "contributors"];
   130 const PROP_TARGETAPP     = ["id", "minVersion", "maxVersion"];
   132 // Properties that should be migrated where possible from an old database. These
   133 // shouldn't include properties that can be read directly from install.rdf files
   134 // or calculated
   135 const DB_MIGRATE_METADATA= ["installDate", "userDisabled", "softDisabled",
   136                             "sourceURI", "applyBackgroundUpdates",
   137                             "releaseNotesURI", "foreignInstall", "syncGUID"];
   138 // Properties to cache and reload when an addon installation is pending
   139 const PENDING_INSTALL_METADATA =
   140     ["syncGUID", "targetApplications", "userDisabled", "softDisabled",
   141      "existingAddonID", "sourceURI", "releaseNotesURI", "installDate",
   142      "updateDate", "applyBackgroundUpdates", "compatibilityOverrides"];
   144 // Note: When adding/changing/removing items here, remember to change the
   145 // DB schema version to ensure changes are picked up ASAP.
   146 const STATIC_BLOCKLIST_PATTERNS = [
   147   { creator: "Mozilla Corp.",
   148     level: Ci.nsIBlocklistService.STATE_BLOCKED,
   149     blockID: "i162" },
   150   { creator: "Mozilla.org",
   151     level: Ci.nsIBlocklistService.STATE_BLOCKED,
   152     blockID: "i162" }
   153 ];
   156 const BOOTSTRAP_REASONS = {
   157   APP_STARTUP     : 1,
   158   APP_SHUTDOWN    : 2,
   159   ADDON_ENABLE    : 3,
   160   ADDON_DISABLE   : 4,
   161   ADDON_INSTALL   : 5,
   162   ADDON_UNINSTALL : 6,
   163   ADDON_UPGRADE   : 7,
   164   ADDON_DOWNGRADE : 8
   165 };
   167 // Map new string type identifiers to old style nsIUpdateItem types
   168 const TYPES = {
   169   extension: 2,
   170   theme: 4,
   171   locale: 8,
   172   multipackage: 32,
   173   dictionary: 64,
   174   experiment: 128,
   175 };
   177 const RESTARTLESS_TYPES = new Set([
   178   "dictionary",
   179   "experiment",
   180   "locale",
   181 ]);
   183 // Keep track of where we are in startup for telemetry
   184 // event happened during XPIDatabase.startup()
   185 const XPI_STARTING = "XPIStarting";
   186 // event happened after startup() but before the final-ui-startup event
   187 const XPI_BEFORE_UI_STARTUP = "BeforeFinalUIStartup";
   188 // event happened after final-ui-startup
   189 const XPI_AFTER_UI_STARTUP = "AfterFinalUIStartup";
   191 const COMPATIBLE_BY_DEFAULT_TYPES = {
   192   extension: true,
   193   dictionary: true
   194 };
   196 const MSG_JAR_FLUSH = "AddonJarFlush";
   198 var gGlobalScope = this;
   200 /**
   201  * Valid IDs fit this pattern.
   202  */
   203 var gIDTest = /^(\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}|[a-z0-9-\._]*\@[a-z0-9-\._]+)$/i;
   205 Cu.import("resource://gre/modules/Log.jsm");
   206 const LOGGER_ID = "addons.xpi";
   208 // Create a new logger for use by all objects in this Addons XPI Provider module
   209 // (Requires AddonManager.jsm)
   210 let logger = Log.repository.getLogger(LOGGER_ID);
   212 const LAZY_OBJECTS = ["XPIDatabase"];
   214 var gLazyObjectsLoaded = false;
   216 function loadLazyObjects() {
   217   let scope = {};
   218   scope.AddonInternal = AddonInternal;
   219   scope.XPIProvider = XPIProvider;
   220   Services.scriptloader.loadSubScript("resource://gre/modules/addons/XPIProviderUtils.js",
   221                                       scope);
   223   for (let name of LAZY_OBJECTS) {
   224     delete gGlobalScope[name];
   225     gGlobalScope[name] = scope[name];
   226   }
   227   gLazyObjectsLoaded = true;
   228   return scope;
   229 }
   231 for (let name of LAZY_OBJECTS) {
   232   Object.defineProperty(gGlobalScope, name, {
   233     get: function lazyObjectGetter() {
   234       let objs = loadLazyObjects();
   235       return objs[name];
   236     },
   237     configurable: true
   238   });
   239 }
   242 function findMatchingStaticBlocklistItem(aAddon) {
   243   for (let item of STATIC_BLOCKLIST_PATTERNS) {
   244     if ("creator" in item && typeof item.creator == "string") {
   245       if ((aAddon.defaultLocale && aAddon.defaultLocale.creator == item.creator) ||
   246           (aAddon.selectedLocale && aAddon.selectedLocale.creator == item.creator)) {
   247         return item;
   248       }
   249     }
   250   }
   251   return null;
   252 }
   255 /**
   256  * Sets permissions on a file
   257  *
   258  * @param  aFile
   259  *         The file or directory to operate on.
   260  * @param  aPermissions
   261  *         The permisions to set
   262  */
   263 function setFilePermissions(aFile, aPermissions) {
   264   try {
   265     aFile.permissions = aPermissions;
   266   }
   267   catch (e) {
   268     logger.warn("Failed to set permissions " + aPermissions.toString(8) + " on " +
   269          aFile.path, e);
   270   }
   271 }
   273 /**
   274  * A safe way to install a file or the contents of a directory to a new
   275  * directory. The file or directory is moved or copied recursively and if
   276  * anything fails an attempt is made to rollback the entire operation. The
   277  * operation may also be rolled back to its original state after it has
   278  * completed by calling the rollback method.
   279  *
   280  * Operations can be chained. Calling move or copy multiple times will remember
   281  * the whole set and if one fails all of the operations will be rolled back.
   282  */
   283 function SafeInstallOperation() {
   284   this._installedFiles = [];
   285   this._createdDirs = [];
   286 }
   288 SafeInstallOperation.prototype = {
   289   _installedFiles: null,
   290   _createdDirs: null,
   292   _installFile: function SIO_installFile(aFile, aTargetDirectory, aCopy) {
   293     let oldFile = aCopy ? null : aFile.clone();
   294     let newFile = aFile.clone();
   295     try {
   296       if (aCopy)
   297         newFile.copyTo(aTargetDirectory, null);
   298       else
   299         newFile.moveTo(aTargetDirectory, null);
   300     }
   301     catch (e) {
   302       logger.error("Failed to " + (aCopy ? "copy" : "move") + " file " + aFile.path +
   303             " to " + aTargetDirectory.path, e);
   304       throw e;
   305     }
   306     this._installedFiles.push({ oldFile: oldFile, newFile: newFile });
   307   },
   309   _installDirectory: function SIO_installDirectory(aDirectory, aTargetDirectory, aCopy) {
   310     let newDir = aTargetDirectory.clone();
   311     newDir.append(aDirectory.leafName);
   312     try {
   313       newDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
   314     }
   315     catch (e) {
   316       logger.error("Failed to create directory " + newDir.path, e);
   317       throw e;
   318     }
   319     this._createdDirs.push(newDir);
   321     // Use a snapshot of the directory contents to avoid possible issues with
   322     // iterating over a directory while removing files from it (the YAFFS2
   323     // embedded filesystem has this issue, see bug 772238), and to remove
   324     // normal files before their resource forks on OSX (see bug 733436).
   325     let entries = getDirectoryEntries(aDirectory, true);
   326     entries.forEach(function(aEntry) {
   327       try {
   328         this._installDirEntry(aEntry, newDir, aCopy);
   329       }
   330       catch (e) {
   331         logger.error("Failed to " + (aCopy ? "copy" : "move") + " entry " +
   332               aEntry.path, e);
   333         throw e;
   334       }
   335     }, this);
   337     // If this is only a copy operation then there is nothing else to do
   338     if (aCopy)
   339       return;
   341     // The directory should be empty by this point. If it isn't this will throw
   342     // and all of the operations will be rolled back
   343     try {
   344       setFilePermissions(aDirectory, FileUtils.PERMS_DIRECTORY);
   345       aDirectory.remove(false);
   346     }
   347     catch (e) {
   348       logger.error("Failed to remove directory " + aDirectory.path, e);
   349       throw e;
   350     }
   352     // Note we put the directory move in after all the file moves so the
   353     // directory is recreated before all the files are moved back
   354     this._installedFiles.push({ oldFile: aDirectory, newFile: newDir });
   355   },
   357   _installDirEntry: function SIO_installDirEntry(aDirEntry, aTargetDirectory, aCopy) {
   358     let isDir = null;
   360     try {
   361       isDir = aDirEntry.isDirectory();
   362     }
   363     catch (e) {
   364       // If the file has already gone away then don't worry about it, this can
   365       // happen on OSX where the resource fork is automatically moved with the
   366       // data fork for the file. See bug 733436.
   367       if (e.result == Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST)
   368         return;
   370       logger.error("Failure " + (aCopy ? "copying" : "moving") + " " + aDirEntry.path +
   371             " to " + aTargetDirectory.path);
   372       throw e;
   373     }
   375     try {
   376       if (isDir)
   377         this._installDirectory(aDirEntry, aTargetDirectory, aCopy);
   378       else
   379         this._installFile(aDirEntry, aTargetDirectory, aCopy);
   380     }
   381     catch (e) {
   382       logger.error("Failure " + (aCopy ? "copying" : "moving") + " " + aDirEntry.path +
   383             " to " + aTargetDirectory.path);
   384       throw e;
   385     }
   386   },
   388   /**
   389    * Moves a file or directory into a new directory. If an error occurs then all
   390    * files that have been moved will be moved back to their original location.
   391    *
   392    * @param  aFile
   393    *         The file or directory to be moved.
   394    * @param  aTargetDirectory
   395    *         The directory to move into, this is expected to be an empty
   396    *         directory.
   397    */
   398   move: function SIO_move(aFile, aTargetDirectory) {
   399     try {
   400       this._installDirEntry(aFile, aTargetDirectory, false);
   401     }
   402     catch (e) {
   403       this.rollback();
   404       throw e;
   405     }
   406   },
   408   /**
   409    * Copies a file or directory into a new directory. If an error occurs then
   410    * all new files that have been created will be removed.
   411    *
   412    * @param  aFile
   413    *         The file or directory to be copied.
   414    * @param  aTargetDirectory
   415    *         The directory to copy into, this is expected to be an empty
   416    *         directory.
   417    */
   418   copy: function SIO_copy(aFile, aTargetDirectory) {
   419     try {
   420       this._installDirEntry(aFile, aTargetDirectory, true);
   421     }
   422     catch (e) {
   423       this.rollback();
   424       throw e;
   425     }
   426   },
   428   /**
   429    * Rolls back all the moves that this operation performed. If an exception
   430    * occurs here then both old and new directories are left in an indeterminate
   431    * state
   432    */
   433   rollback: function SIO_rollback() {
   434     while (this._installedFiles.length > 0) {
   435       let move = this._installedFiles.pop();
   436       if (move.newFile.isDirectory()) {
   437         let oldDir = move.oldFile.parent.clone();
   438         oldDir.append(move.oldFile.leafName);
   439         oldDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
   440       }
   441       else if (!move.oldFile) {
   442         // No old file means this was a copied file
   443         move.newFile.remove(true);
   444       }
   445       else {
   446         move.newFile.moveTo(move.oldFile.parent, null);
   447       }
   448     }
   450     while (this._createdDirs.length > 0)
   451       recursiveRemove(this._createdDirs.pop());
   452   }
   453 };
   455 /**
   456  * Gets the currently selected locale for display.
   457  * @return  the selected locale or "en-US" if none is selected
   458  */
   459 function getLocale() {
   460   if (Prefs.getBoolPref(PREF_MATCH_OS_LOCALE, false))
   461     return Services.locale.getLocaleComponentForUserAgent();
   462   let locale = Prefs.getComplexValue(PREF_SELECTED_LOCALE, Ci.nsIPrefLocalizedString);
   463   if (locale)
   464     return locale;
   465   return Prefs.getCharPref(PREF_SELECTED_LOCALE, "en-US");
   466 }
   468 /**
   469  * Selects the closest matching locale from a list of locales.
   470  *
   471  * @param  aLocales
   472  *         An array of locales
   473  * @return the best match for the currently selected locale
   474  */
   475 function findClosestLocale(aLocales) {
   476   let appLocale = getLocale();
   478   // Holds the best matching localized resource
   479   var bestmatch = null;
   480   // The number of locale parts it matched with
   481   var bestmatchcount = 0;
   482   // The number of locale parts in the match
   483   var bestpartcount = 0;
   485   var matchLocales = [appLocale.toLowerCase()];
   486   /* If the current locale is English then it will find a match if there is
   487      a valid match for en-US so no point searching that locale too. */
   488   if (matchLocales[0].substring(0, 3) != "en-")
   489     matchLocales.push("en-us");
   491   for each (var locale in matchLocales) {
   492     var lparts = locale.split("-");
   493     for each (var localized in aLocales) {
   494       for each (let found in localized.locales) {
   495         found = found.toLowerCase();
   496         // Exact match is returned immediately
   497         if (locale == found)
   498           return localized;
   500         var fparts = found.split("-");
   501         /* If we have found a possible match and this one isn't any longer
   502            then we dont need to check further. */
   503         if (bestmatch && fparts.length < bestmatchcount)
   504           continue;
   506         // Count the number of parts that match
   507         var maxmatchcount = Math.min(fparts.length, lparts.length);
   508         var matchcount = 0;
   509         while (matchcount < maxmatchcount &&
   510                fparts[matchcount] == lparts[matchcount])
   511           matchcount++;
   513         /* If we matched more than the last best match or matched the same and
   514            this locale is less specific than the last best match. */
   515         if (matchcount > bestmatchcount ||
   516            (matchcount == bestmatchcount && fparts.length < bestpartcount)) {
   517           bestmatch = localized;
   518           bestmatchcount = matchcount;
   519           bestpartcount = fparts.length;
   520         }
   521       }
   522     }
   523     // If we found a valid match for this locale return it
   524     if (bestmatch)
   525       return bestmatch;
   526   }
   527   return null;
   528 }
   530 /**
   531  * Sets the userDisabled and softDisabled properties of an add-on based on what
   532  * values those properties had for a previous instance of the add-on. The
   533  * previous instance may be a previous install or in the case of an application
   534  * version change the same add-on.
   535  *
   536  * NOTE: this may modify aNewAddon in place; callers should save the database if
   537  * necessary
   538  *
   539  * @param  aOldAddon
   540  *         The previous instance of the add-on
   541  * @param  aNewAddon
   542  *         The new instance of the add-on
   543  * @param  aAppVersion
   544  *         The optional application version to use when checking the blocklist
   545  *         or undefined to use the current application
   546  * @param  aPlatformVersion
   547  *         The optional platform version to use when checking the blocklist or
   548  *         undefined to use the current platform
   549  */
   550 function applyBlocklistChanges(aOldAddon, aNewAddon, aOldAppVersion,
   551                                aOldPlatformVersion) {
   552   // Copy the properties by default
   553   aNewAddon.userDisabled = aOldAddon.userDisabled;
   554   aNewAddon.softDisabled = aOldAddon.softDisabled;
   556   let bs = Cc["@mozilla.org/extensions/blocklist;1"].
   557            getService(Ci.nsIBlocklistService);
   559   let oldBlocklistState = bs.getAddonBlocklistState(createWrapper(aOldAddon),
   560                                                     aOldAppVersion,
   561                                                     aOldPlatformVersion);
   562   let newBlocklistState = bs.getAddonBlocklistState(createWrapper(aNewAddon));
   564   // If the blocklist state hasn't changed then the properties don't need to
   565   // change
   566   if (newBlocklistState == oldBlocklistState)
   567     return;
   569   if (newBlocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED) {
   570     if (aNewAddon.type != "theme") {
   571       // The add-on has become softblocked, set softDisabled if it isn't already
   572       // userDisabled
   573       aNewAddon.softDisabled = !aNewAddon.userDisabled;
   574     }
   575     else {
   576       // Themes just get userDisabled to switch back to the default theme
   577       aNewAddon.userDisabled = true;
   578     }
   579   }
   580   else {
   581     // If the new add-on is not softblocked then it cannot be softDisabled
   582     aNewAddon.softDisabled = false;
   583   }
   584 }
   586 /**
   587  * Calculates whether an add-on should be appDisabled or not.
   588  *
   589  * @param  aAddon
   590  *         The add-on to check
   591  * @return true if the add-on should not be appDisabled
   592  */
   593 function isUsableAddon(aAddon) {
   594   // Hack to ensure the default theme is always usable
   595   if (aAddon.type == "theme" && aAddon.internalName == XPIProvider.defaultSkin)
   596     return true;
   598   if (aAddon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED)
   599     return false;
   601   if (AddonManager.checkUpdateSecurity && !aAddon.providesUpdatesSecurely)
   602     return false;
   604   if (!aAddon.isPlatformCompatible)
   605     return false;
   607   if (AddonManager.checkCompatibility) {
   608     if (!aAddon.isCompatible)
   609       return false;
   610   }
   611   else {
   612     let app = aAddon.matchingTargetApplication;
   613     if (!app)
   614       return false;
   616     // XXX Temporary solution to let applications opt-in to make themes safer
   617     //     following significant UI changes even if checkCompatibility=false has
   618     //     been set, until we get bug 962001.
   619     if (aAddon.type == "theme" && app.id == Services.appinfo.ID) {
   620       try {
   621         let minCompatVersion = Services.prefs.getCharPref(PREF_CHECKCOMAT_THEMEOVERRIDE);
   622         if (minCompatVersion &&
   623             Services.vc.compare(minCompatVersion, app.maxVersion) > 0) {
   624           return false;
   625         }
   626       } catch (e) {}
   627     }
   628   }
   630   return true;
   631 }
   633 function isAddonDisabled(aAddon) {
   634   return aAddon.appDisabled || aAddon.softDisabled || aAddon.userDisabled;
   635 }
   637 XPCOMUtils.defineLazyServiceGetter(this, "gRDF", "@mozilla.org/rdf/rdf-service;1",
   638                                    Ci.nsIRDFService);
   640 function EM_R(aProperty) {
   641   return gRDF.GetResource(PREFIX_NS_EM + aProperty);
   642 }
   644 /**
   645  * Converts an RDF literal, resource or integer into a string.
   646  *
   647  * @param  aLiteral
   648  *         The RDF object to convert
   649  * @return a string if the object could be converted or null
   650  */
   651 function getRDFValue(aLiteral) {
   652   if (aLiteral instanceof Ci.nsIRDFLiteral)
   653     return aLiteral.Value;
   654   if (aLiteral instanceof Ci.nsIRDFResource)
   655     return aLiteral.Value;
   656   if (aLiteral instanceof Ci.nsIRDFInt)
   657     return aLiteral.Value;
   658   return null;
   659 }
   661 /**
   662  * Gets an RDF property as a string
   663  *
   664  * @param  aDs
   665  *         The RDF datasource to read the property from
   666  * @param  aResource
   667  *         The RDF resource to read the property from
   668  * @param  aProperty
   669  *         The property to read
   670  * @return a string if the property existed or null
   671  */
   672 function getRDFProperty(aDs, aResource, aProperty) {
   673   return getRDFValue(aDs.GetTarget(aResource, EM_R(aProperty), true));
   674 }
   676 /**
   677  * Reads an AddonInternal object from an RDF stream.
   678  *
   679  * @param  aUri
   680  *         The URI that the manifest is being read from
   681  * @param  aStream
   682  *         An open stream to read the RDF from
   683  * @return an AddonInternal object
   684  * @throws if the install manifest in the RDF stream is corrupt or could not
   685  *         be read
   686  */
   687 function loadManifestFromRDF(aUri, aStream) {
   688   function getPropertyArray(aDs, aSource, aProperty) {
   689     let values = [];
   690     let targets = aDs.GetTargets(aSource, EM_R(aProperty), true);
   691     while (targets.hasMoreElements())
   692       values.push(getRDFValue(targets.getNext()));
   694     return values;
   695   }
   697   /**
   698    * Reads locale properties from either the main install manifest root or
   699    * an em:localized section in the install manifest.
   700    *
   701    * @param  aDs
   702    *         The nsIRDFDatasource to read from
   703    * @param  aSource
   704    *         The nsIRDFResource to read the properties from
   705    * @param  isDefault
   706    *         True if the locale is to be read from the main install manifest
   707    *         root
   708    * @param  aSeenLocales
   709    *         An array of locale names already seen for this install manifest.
   710    *         Any locale names seen as a part of this function will be added to
   711    *         this array
   712    * @return an object containing the locale properties
   713    */
   714   function readLocale(aDs, aSource, isDefault, aSeenLocales) {
   715     let locale = { };
   716     if (!isDefault) {
   717       locale.locales = [];
   718       let targets = ds.GetTargets(aSource, EM_R("locale"), true);
   719       while (targets.hasMoreElements()) {
   720         let localeName = getRDFValue(targets.getNext());
   721         if (!localeName) {
   722           logger.warn("Ignoring empty locale in localized properties");
   723           continue;
   724         }
   725         if (aSeenLocales.indexOf(localeName) != -1) {
   726           logger.warn("Ignoring duplicate locale in localized properties");
   727           continue;
   728         }
   729         aSeenLocales.push(localeName);
   730         locale.locales.push(localeName);
   731       }
   733       if (locale.locales.length == 0) {
   734         logger.warn("Ignoring localized properties with no listed locales");
   735         return null;
   736       }
   737     }
   739     PROP_LOCALE_SINGLE.forEach(function(aProp) {
   740       locale[aProp] = getRDFProperty(aDs, aSource, aProp);
   741     });
   743     PROP_LOCALE_MULTI.forEach(function(aProp) {
   744       // Don't store empty arrays
   745       let props = getPropertyArray(aDs, aSource,
   746                                    aProp.substring(0, aProp.length - 1));
   747       if (props.length > 0)
   748         locale[aProp] = props;
   749     });
   751     return locale;
   752   }
   754   let rdfParser = Cc["@mozilla.org/rdf/xml-parser;1"].
   755                   createInstance(Ci.nsIRDFXMLParser)
   756   let ds = Cc["@mozilla.org/rdf/datasource;1?name=in-memory-datasource"].
   757            createInstance(Ci.nsIRDFDataSource);
   758   let listener = rdfParser.parseAsync(ds, aUri);
   759   let channel = Cc["@mozilla.org/network/input-stream-channel;1"].
   760                 createInstance(Ci.nsIInputStreamChannel);
   761   channel.setURI(aUri);
   762   channel.contentStream = aStream;
   763   channel.QueryInterface(Ci.nsIChannel);
   764   channel.contentType = "text/xml";
   766   listener.onStartRequest(channel, null);
   768   try {
   769     let pos = 0;
   770     let count = aStream.available();
   771     while (count > 0) {
   772       listener.onDataAvailable(channel, null, aStream, pos, count);
   773       pos += count;
   774       count = aStream.available();
   775     }
   776     listener.onStopRequest(channel, null, Components.results.NS_OK);
   777   }
   778   catch (e) {
   779     listener.onStopRequest(channel, null, e.result);
   780     throw e;
   781   }
   783   let root = gRDF.GetResource(RDFURI_INSTALL_MANIFEST_ROOT);
   784   let addon = new AddonInternal();
   785   PROP_METADATA.forEach(function(aProp) {
   786     addon[aProp] = getRDFProperty(ds, root, aProp);
   787   });
   788   addon.unpack = getRDFProperty(ds, root, "unpack") == "true";
   790   if (!addon.type) {
   791     addon.type = addon.internalName ? "theme" : "extension";
   792   }
   793   else {
   794     let type = addon.type;
   795     addon.type = null;
   796     for (let name in TYPES) {
   797       if (TYPES[name] == type) {
   798         addon.type = name;
   799         break;
   800       }
   801     }
   802   }
   804   if (!(addon.type in TYPES))
   805     throw new Error("Install manifest specifies unknown type: " + addon.type);
   807   if (addon.type != "multipackage") {
   808     if (!addon.id)
   809       throw new Error("No ID in install manifest");
   810     if (!gIDTest.test(addon.id))
   811       throw new Error("Illegal add-on ID " + addon.id);
   812     if (!addon.version)
   813       throw new Error("No version in install manifest");
   814   }
   816   addon.strictCompatibility = !(addon.type in COMPATIBLE_BY_DEFAULT_TYPES) ||
   817                               getRDFProperty(ds, root, "strictCompatibility") == "true";
   819   // Only read the bootstrap property for extensions.
   820   if (addon.type == "extension") {
   821     addon.bootstrap = getRDFProperty(ds, root, "bootstrap") == "true";
   822     if (addon.optionsType &&
   823         addon.optionsType != AddonManager.OPTIONS_TYPE_DIALOG &&
   824         addon.optionsType != AddonManager.OPTIONS_TYPE_INLINE &&
   825         addon.optionsType != AddonManager.OPTIONS_TYPE_TAB &&
   826         addon.optionsType != AddonManager.OPTIONS_TYPE_INLINE_INFO) {
   827       throw new Error("Install manifest specifies unknown type: " + addon.optionsType);
   828     }
   829   }
   830   else {
   831     // Some add-on types are always restartless.
   832     if (RESTARTLESS_TYPES.has(addon.type)) {
   833       addon.bootstrap = true;
   834     }
   836     // Only extensions are allowed to provide an optionsURL, optionsType or aboutURL. For
   837     // all other types they are silently ignored
   838     addon.optionsURL = null;
   839     addon.optionsType = null;
   840     addon.aboutURL = null;
   842     if (addon.type == "theme") {
   843       if (!addon.internalName)
   844         throw new Error("Themes must include an internalName property");
   845       addon.skinnable = getRDFProperty(ds, root, "skinnable") == "true";
   846     }
   847   }
   849   addon.defaultLocale = readLocale(ds, root, true);
   851   let seenLocales = [];
   852   addon.locales = [];
   853   let targets = ds.GetTargets(root, EM_R("localized"), true);
   854   while (targets.hasMoreElements()) {
   855     let target = targets.getNext().QueryInterface(Ci.nsIRDFResource);
   856     let locale = readLocale(ds, target, false, seenLocales);
   857     if (locale)
   858       addon.locales.push(locale);
   859   }
   861   let seenApplications = [];
   862   addon.targetApplications = [];
   863   targets = ds.GetTargets(root, EM_R("targetApplication"), true);
   864   while (targets.hasMoreElements()) {
   865     let target = targets.getNext().QueryInterface(Ci.nsIRDFResource);
   866     let targetAppInfo = {};
   867     PROP_TARGETAPP.forEach(function(aProp) {
   868       targetAppInfo[aProp] = getRDFProperty(ds, target, aProp);
   869     });
   870     if (!targetAppInfo.id || !targetAppInfo.minVersion ||
   871         !targetAppInfo.maxVersion) {
   872       logger.warn("Ignoring invalid targetApplication entry in install manifest");
   873       continue;
   874     }
   875     if (seenApplications.indexOf(targetAppInfo.id) != -1) {
   876       logger.warn("Ignoring duplicate targetApplication entry for " + targetAppInfo.id +
   877            " in install manifest");
   878       continue;
   879     }
   880     seenApplications.push(targetAppInfo.id);
   881     addon.targetApplications.push(targetAppInfo);
   882   }
   884   // Note that we don't need to check for duplicate targetPlatform entries since
   885   // the RDF service coalesces them for us.
   886   let targetPlatforms = getPropertyArray(ds, root, "targetPlatform");
   887   addon.targetPlatforms = [];
   888   targetPlatforms.forEach(function(aPlatform) {
   889     let platform = {
   890       os: null,
   891       abi: null
   892     };
   894     let pos = aPlatform.indexOf("_");
   895     if (pos != -1) {
   896       platform.os = aPlatform.substring(0, pos);
   897       platform.abi = aPlatform.substring(pos + 1);
   898     }
   899     else {
   900       platform.os = aPlatform;
   901     }
   903     addon.targetPlatforms.push(platform);
   904   });
   906   // A theme's userDisabled value is true if the theme is not the selected skin
   907   // or if there is an active lightweight theme. We ignore whether softblocking
   908   // is in effect since it would change the active theme.
   909   if (addon.type == "theme") {
   910     addon.userDisabled = !!LightweightThemeManager.currentTheme ||
   911                          addon.internalName != XPIProvider.selectedSkin;
   912   }
   913   // Experiments are disabled by default. It is up to the Experiments Manager
   914   // to enable them (it drives installation).
   915   else if (addon.type == "experiment") {
   916     addon.userDisabled = true;
   917   }
   918   else {
   919     addon.userDisabled = false;
   920     addon.softDisabled = addon.blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED;
   921   }
   923   addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT;
   925   // Experiments are managed and updated through an external "experiments
   926   // manager." So disable some built-in mechanisms.
   927   if (addon.type == "experiment") {
   928     addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DISABLE;
   929     addon.updateURL = null;
   930     addon.updateKey = null;
   932     addon.targetApplications = [];
   933     addon.targetPlatforms = [];
   934   }
   936   // Load the storage service before NSS (nsIRandomGenerator),
   937   // to avoid a SQLite initialization error (bug 717904).
   938   let storage = Services.storage;
   940   // Generate random GUID used for Sync.
   941   // This was lifted from util.js:makeGUID() from services-sync.
   942   let rng = Cc["@mozilla.org/security/random-generator;1"].
   943             createInstance(Ci.nsIRandomGenerator);
   944   let bytes = rng.generateRandomBytes(9);
   945   let byte_string = [String.fromCharCode(byte) for each (byte in bytes)]
   946                     .join("");
   947   // Base64 encode
   948   addon.syncGUID = btoa(byte_string).replace(/\+/g, '-')
   949                                     .replace(/\//g, '_');
   951   return addon;
   952 }
   954 /**
   955  * Loads an AddonInternal object from an add-on extracted in a directory.
   956  *
   957  * @param  aDir
   958  *         The nsIFile directory holding the add-on
   959  * @return an AddonInternal object
   960  * @throws if the directory does not contain a valid install manifest
   961  */
   962 function loadManifestFromDir(aDir) {
   963   function getFileSize(aFile) {
   964     if (aFile.isSymlink())
   965       return 0;
   967     if (!aFile.isDirectory())
   968       return aFile.fileSize;
   970     let size = 0;
   971     let entries = aFile.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
   972     let entry;
   973     while ((entry = entries.nextFile))
   974       size += getFileSize(entry);
   975     entries.close();
   976     return size;
   977   }
   979   let file = aDir.clone();
   980   file.append(FILE_INSTALL_MANIFEST);
   981   if (!file.exists() || !file.isFile())
   982     throw new Error("Directory " + aDir.path + " does not contain a valid " +
   983                     "install manifest");
   985   let fis = Cc["@mozilla.org/network/file-input-stream;1"].
   986             createInstance(Ci.nsIFileInputStream);
   987   fis.init(file, -1, -1, false);
   988   let bis = Cc["@mozilla.org/network/buffered-input-stream;1"].
   989             createInstance(Ci.nsIBufferedInputStream);
   990   bis.init(fis, 4096);
   992   try {
   993     let addon = loadManifestFromRDF(Services.io.newFileURI(file), bis);
   994     addon._sourceBundle = aDir.clone();
   995     addon.size = getFileSize(aDir);
   997     file = aDir.clone();
   998     file.append("chrome.manifest");
   999     let chromeManifest = ChromeManifestParser.parseSync(Services.io.newFileURI(file));
  1000     addon.hasBinaryComponents = ChromeManifestParser.hasType(chromeManifest,
  1001                                                              "binary-component");
  1003     addon.appDisabled = !isUsableAddon(addon);
  1004     return addon;
  1006   finally {
  1007     bis.close();
  1008     fis.close();
  1012 /**
  1013  * Loads an AddonInternal object from an nsIZipReader for an add-on.
  1015  * @param  aZipReader
  1016  *         An open nsIZipReader for the add-on's files
  1017  * @return an AddonInternal object
  1018  * @throws if the XPI file does not contain a valid install manifest
  1019  */
  1020 function loadManifestFromZipReader(aZipReader) {
  1021   let zis = aZipReader.getInputStream(FILE_INSTALL_MANIFEST);
  1022   let bis = Cc["@mozilla.org/network/buffered-input-stream;1"].
  1023             createInstance(Ci.nsIBufferedInputStream);
  1024   bis.init(zis, 4096);
  1026   try {
  1027     let uri = buildJarURI(aZipReader.file, FILE_INSTALL_MANIFEST);
  1028     let addon = loadManifestFromRDF(uri, bis);
  1029     addon._sourceBundle = aZipReader.file;
  1031     addon.size = 0;
  1032     let entries = aZipReader.findEntries(null);
  1033     while (entries.hasMore())
  1034       addon.size += aZipReader.getEntry(entries.getNext()).realSize;
  1036     // Binary components can only be loaded from unpacked addons.
  1037     if (addon.unpack) {
  1038       uri = buildJarURI(aZipReader.file, "chrome.manifest");
  1039       let chromeManifest = ChromeManifestParser.parseSync(uri);
  1040       addon.hasBinaryComponents = ChromeManifestParser.hasType(chromeManifest,
  1041                                                                "binary-component");
  1042     } else {
  1043       addon.hasBinaryComponents = false;
  1046     addon.appDisabled = !isUsableAddon(addon);
  1047     return addon;
  1049   finally {
  1050     bis.close();
  1051     zis.close();
  1055 /**
  1056  * Loads an AddonInternal object from an add-on in an XPI file.
  1058  * @param  aXPIFile
  1059  *         An nsIFile pointing to the add-on's XPI file
  1060  * @return an AddonInternal object
  1061  * @throws if the XPI file does not contain a valid install manifest
  1062  */
  1063 function loadManifestFromZipFile(aXPIFile) {
  1064   let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].
  1065                   createInstance(Ci.nsIZipReader);
  1066   try {
  1067     zipReader.open(aXPIFile);
  1069     return loadManifestFromZipReader(zipReader);
  1071   finally {
  1072     zipReader.close();
  1076 function loadManifestFromFile(aFile) {
  1077   if (aFile.isFile())
  1078     return loadManifestFromZipFile(aFile);
  1079   else
  1080     return loadManifestFromDir(aFile);
  1083 /**
  1084  * Gets an nsIURI for a file within another file, either a directory or an XPI
  1085  * file. If aFile is a directory then this will return a file: URI, if it is an
  1086  * XPI file then it will return a jar: URI.
  1088  * @param  aFile
  1089  *         The file containing the resources, must be either a directory or an
  1090  *         XPI file
  1091  * @param  aPath
  1092  *         The path to find the resource at, "/" separated. If aPath is empty
  1093  *         then the uri to the root of the contained files will be returned
  1094  * @return an nsIURI pointing at the resource
  1095  */
  1096 function getURIForResourceInFile(aFile, aPath) {
  1097   if (aFile.isDirectory()) {
  1098     let resource = aFile.clone();
  1099     if (aPath) {
  1100       aPath.split("/").forEach(function(aPart) {
  1101         resource.append(aPart);
  1102       });
  1104     return NetUtil.newURI(resource);
  1107   return buildJarURI(aFile, aPath);
  1110 /**
  1111  * Creates a jar: URI for a file inside a ZIP file.
  1113  * @param  aJarfile
  1114  *         The ZIP file as an nsIFile
  1115  * @param  aPath
  1116  *         The path inside the ZIP file
  1117  * @return an nsIURI for the file
  1118  */
  1119 function buildJarURI(aJarfile, aPath) {
  1120   let uri = Services.io.newFileURI(aJarfile);
  1121   uri = "jar:" + uri.spec + "!/" + aPath;
  1122   return NetUtil.newURI(uri);
  1125 /**
  1126  * Sends local and remote notifications to flush a JAR file cache entry
  1128  * @param aJarFile
  1129  *        The ZIP/XPI/JAR file as a nsIFile
  1130  */
  1131 function flushJarCache(aJarFile) {
  1132   Services.obs.notifyObservers(aJarFile, "flush-cache-entry", null);
  1133   Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageBroadcaster)
  1134     .broadcastAsyncMessage(MSG_JAR_FLUSH, aJarFile.path);
  1137 function flushStartupCache() {
  1138   // Init this, so it will get the notification.
  1139   Services.obs.notifyObservers(null, "startupcache-invalidate", null);
  1142 /**
  1143  * Creates and returns a new unique temporary file. The caller should delete
  1144  * the file when it is no longer needed.
  1146  * @return an nsIFile that points to a randomly named, initially empty file in
  1147  *         the OS temporary files directory
  1148  */
  1149 function getTemporaryFile() {
  1150   let file = FileUtils.getDir(KEY_TEMPDIR, []);
  1151   let random = Math.random().toString(36).replace(/0./, '').substr(-3);
  1152   file.append("tmp-" + random + ".xpi");
  1153   file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
  1155   return file;
  1158 /**
  1159  * Verifies that a zip file's contents are all signed by the same principal.
  1160  * Directory entries and anything in the META-INF directory are not checked.
  1162  * @param  aZip
  1163  *         A nsIZipReader to check
  1164  * @param  aPrincipal
  1165  *         The nsIPrincipal to compare against
  1166  * @return true if all the contents that should be signed were signed by the
  1167  *         principal
  1168  */
  1169 function verifyZipSigning(aZip, aPrincipal) {
  1170   var count = 0;
  1171   var entries = aZip.findEntries(null);
  1172   while (entries.hasMore()) {
  1173     var entry = entries.getNext();
  1174     // Nothing in META-INF is in the manifest.
  1175     if (entry.substr(0, 9) == "META-INF/")
  1176       continue;
  1177     // Directory entries aren't in the manifest.
  1178     if (entry.substr(-1) == "/")
  1179       continue;
  1180     count++;
  1181     var entryPrincipal = aZip.getCertificatePrincipal(entry);
  1182     if (!entryPrincipal || !aPrincipal.equals(entryPrincipal))
  1183       return false;
  1185   return aZip.manifestEntriesCount == count;
  1188 /**
  1189  * Replaces %...% strings in an addon url (update and updateInfo) with
  1190  * appropriate values.
  1192  * @param  aAddon
  1193  *         The AddonInternal representing the add-on
  1194  * @param  aUri
  1195  *         The uri to escape
  1196  * @param  aUpdateType
  1197  *         An optional number representing the type of update, only applicable
  1198  *         when creating a url for retrieving an update manifest
  1199  * @param  aAppVersion
  1200  *         The optional application version to use for %APP_VERSION%
  1201  * @return the appropriately escaped uri.
  1202  */
  1203 function escapeAddonURI(aAddon, aUri, aUpdateType, aAppVersion)
  1205   let uri = AddonManager.escapeAddonURI(aAddon, aUri, aAppVersion);
  1207   // If there is an updateType then replace the UPDATE_TYPE string
  1208   if (aUpdateType)
  1209     uri = uri.replace(/%UPDATE_TYPE%/g, aUpdateType);
  1211   // If this add-on has compatibility information for either the current
  1212   // application or toolkit then replace the ITEM_MAXAPPVERSION with the
  1213   // maxVersion
  1214   let app = aAddon.matchingTargetApplication;
  1215   if (app)
  1216     var maxVersion = app.maxVersion;
  1217   else
  1218     maxVersion = "";
  1219   uri = uri.replace(/%ITEM_MAXAPPVERSION%/g, maxVersion);
  1221   let compatMode = "normal";
  1222   if (!AddonManager.checkCompatibility)
  1223     compatMode = "ignore";
  1224   else if (AddonManager.strictCompatibility)
  1225     compatMode = "strict";
  1226   uri = uri.replace(/%COMPATIBILITY_MODE%/g, compatMode);
  1228   return uri;
  1231 function removeAsync(aFile) {
  1232   return Task.spawn(function () {
  1233     let info = null;
  1234     try {
  1235       info = yield OS.File.stat(aFile.path);
  1236       if (info.isDir)
  1237         yield OS.File.removeDir(aFile.path);
  1238       else
  1239         yield OS.File.remove(aFile.path);
  1241     catch (e if e instanceof OS.File.Error && e.becauseNoSuchFile) {
  1242       // The file has already gone away
  1243       return;
  1245   });
  1248 /**
  1249  * Recursively removes a directory or file fixing permissions when necessary.
  1251  * @param  aFile
  1252  *         The nsIFile to remove
  1253  */
  1254 function recursiveRemove(aFile) {
  1255   let isDir = null;
  1257   try {
  1258     isDir = aFile.isDirectory();
  1260   catch (e) {
  1261     // If the file has already gone away then don't worry about it, this can
  1262     // happen on OSX where the resource fork is automatically moved with the
  1263     // data fork for the file. See bug 733436.
  1264     if (e.result == Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST)
  1265       return;
  1266     if (e.result == Cr.NS_ERROR_FILE_NOT_FOUND)
  1267       return;
  1269     throw e;
  1272   setFilePermissions(aFile, isDir ? FileUtils.PERMS_DIRECTORY
  1273                                   : FileUtils.PERMS_FILE);
  1275   try {
  1276     aFile.remove(true);
  1277     return;
  1279   catch (e) {
  1280     if (!aFile.isDirectory()) {
  1281       logger.error("Failed to remove file " + aFile.path, e);
  1282       throw e;
  1286   // Use a snapshot of the directory contents to avoid possible issues with
  1287   // iterating over a directory while removing files from it (the YAFFS2
  1288   // embedded filesystem has this issue, see bug 772238), and to remove
  1289   // normal files before their resource forks on OSX (see bug 733436).
  1290   let entries = getDirectoryEntries(aFile, true);
  1291   entries.forEach(recursiveRemove);
  1293   try {
  1294     aFile.remove(true);
  1296   catch (e) {
  1297     logger.error("Failed to remove empty directory " + aFile.path, e);
  1298     throw e;
  1302 /**
  1303  * Returns the timestamp and leaf file name of the most recently modified
  1304  * entry in a directory,
  1305  * or simply the file's own timestamp if it is not a directory.
  1306  * Also returns the total number of items (directories and files) visited in the scan
  1308  * @param  aFile
  1309  *         A non-null nsIFile object
  1310  * @return [File Name, Epoch time, items visited], as described above.
  1311  */
  1312 function recursiveLastModifiedTime(aFile) {
  1313   try {
  1314     let modTime = aFile.lastModifiedTime;
  1315     let fileName = aFile.leafName;
  1316     if (aFile.isFile())
  1317       return [fileName, modTime, 1];
  1319     if (aFile.isDirectory()) {
  1320       let entries = aFile.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
  1321       let entry;
  1322       let totalItems = 1;
  1323       while ((entry = entries.nextFile)) {
  1324         let [subName, subTime, items] = recursiveLastModifiedTime(entry);
  1325         totalItems += items;
  1326         if (subTime > modTime) {
  1327           modTime = subTime;
  1328           fileName = subName;
  1331       entries.close();
  1332       return [fileName, modTime, totalItems];
  1335   catch (e) {
  1336     logger.warn("Problem getting last modified time for " + aFile.path, e);
  1339   // If the file is something else, just ignore it.
  1340   return ["", 0, 0];
  1343 /**
  1344  * Gets a snapshot of directory entries.
  1346  * @param  aDir
  1347  *         Directory to look at
  1348  * @param  aSortEntries
  1349  *         True to sort entries by filename
  1350  * @return An array of nsIFile, or an empty array if aDir is not a readable directory
  1351  */
  1352 function getDirectoryEntries(aDir, aSortEntries) {
  1353   let dirEnum;
  1354   try {
  1355     dirEnum = aDir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
  1356     let entries = [];
  1357     while (dirEnum.hasMoreElements())
  1358       entries.push(dirEnum.nextFile);
  1360     if (aSortEntries) {
  1361       entries.sort(function sortDirEntries(a, b) {
  1362         return a.path > b.path ? -1 : 1;
  1363       });
  1366     return entries
  1368   catch (e) {
  1369     logger.warn("Can't iterate directory " + aDir.path, e);
  1370     return [];
  1372   finally {
  1373     if (dirEnum) {
  1374       dirEnum.close();
  1379 /**
  1380  * A helpful wrapper around the prefs service that allows for default values
  1381  * when requested values aren't set.
  1382  */
  1383 var Prefs = {
  1384   /**
  1385    * Gets a preference from the default branch ignoring user-set values.
  1387    * @param  aName
  1388    *         The name of the preference
  1389    * @param  aDefaultValue
  1390    *         A value to return if the preference does not exist
  1391    * @return the default value of the preference or aDefaultValue if there is
  1392    *         none
  1393    */
  1394   getDefaultCharPref: function Prefs_getDefaultCharPref(aName, aDefaultValue) {
  1395     try {
  1396       return Services.prefs.getDefaultBranch("").getCharPref(aName);
  1398     catch (e) {
  1400     return aDefaultValue;
  1401   },
  1403   /**
  1404    * Gets a string preference.
  1406    * @param  aName
  1407    *         The name of the preference
  1408    * @param  aDefaultValue
  1409    *         A value to return if the preference does not exist
  1410    * @return the value of the preference or aDefaultValue if there is none
  1411    */
  1412   getCharPref: function Prefs_getCharPref(aName, aDefaultValue) {
  1413     try {
  1414       return Services.prefs.getCharPref(aName);
  1416     catch (e) {
  1418     return aDefaultValue;
  1419   },
  1421   /**
  1422    * Gets a complex preference.
  1424    * @param  aName
  1425    *         The name of the preference
  1426    * @param  aType
  1427    *         The interface type of the preference
  1428    * @param  aDefaultValue
  1429    *         A value to return if the preference does not exist
  1430    * @return the value of the preference or aDefaultValue if there is none
  1431    */
  1432   getComplexValue: function Prefs_getComplexValue(aName, aType, aDefaultValue) {
  1433     try {
  1434       return Services.prefs.getComplexValue(aName, aType).data;
  1436     catch (e) {
  1438     return aDefaultValue;
  1439   },
  1441   /**
  1442    * Gets a boolean preference.
  1444    * @param  aName
  1445    *         The name of the preference
  1446    * @param  aDefaultValue
  1447    *         A value to return if the preference does not exist
  1448    * @return the value of the preference or aDefaultValue if there is none
  1449    */
  1450   getBoolPref: function Prefs_getBoolPref(aName, aDefaultValue) {
  1451     try {
  1452       return Services.prefs.getBoolPref(aName);
  1454     catch (e) {
  1456     return aDefaultValue;
  1457   },
  1459   /**
  1460    * Gets an integer preference.
  1462    * @param  aName
  1463    *         The name of the preference
  1464    * @param  defaultValue
  1465    *         A value to return if the preference does not exist
  1466    * @return the value of the preference or defaultValue if there is none
  1467    */
  1468   getIntPref: function Prefs_getIntPref(aName, defaultValue) {
  1469     try {
  1470       return Services.prefs.getIntPref(aName);
  1472     catch (e) {
  1474     return defaultValue;
  1475   },
  1477   /**
  1478    * Clears a preference if it has a user value
  1480    * @param  aName
  1481    *         The name of the preference
  1482    */
  1483   clearUserPref: function Prefs_clearUserPref(aName) {
  1484     if (Services.prefs.prefHasUserValue(aName))
  1485       Services.prefs.clearUserPref(aName);
  1489 // Helper function to compare JSON saved version of the directory state
  1490 // with the new state returned by getInstallLocationStates()
  1491 // Structure is: ordered array of {'name':?, 'addons': {addonID: {'descriptor':?, 'mtime':?} ...}}
  1492 function directoryStateDiffers(aState, aCache)
  1494   // check equality of an object full of addons; fortunately we can destroy the 'aOld' object
  1495   function addonsMismatch(aNew, aOld) {
  1496     for (let [id, val] of aNew) {
  1497       if (!id in aOld)
  1498         return true;
  1499       if (val.descriptor != aOld[id].descriptor ||
  1500           val.mtime != aOld[id].mtime)
  1501         return true;
  1502       delete aOld[id];
  1504     // make sure aOld doesn't have any extra entries
  1505     for (let id in aOld)
  1506       return true;
  1507     return false;
  1510   if (!aCache)
  1511     return true;
  1512   try {
  1513     let old = JSON.parse(aCache);
  1514     if (aState.length != old.length)
  1515       return true;
  1516     for (let i = 0; i < aState.length; i++) {
  1517       // conveniently, any missing fields would require a 'true' return, which is
  1518       // handled by our catch wrapper
  1519       if (aState[i].name != old[i].name)
  1520         return true;
  1521       if (addonsMismatch(aState[i].addons, old[i].addons))
  1522         return true;
  1525   catch (e) {
  1526     return true;
  1528   return false;
  1531 /**
  1532  * Wraps a function in an exception handler to protect against exceptions inside callbacks
  1533  * @param aFunction function(args...)
  1534  * @return function(args...), a function that takes the same arguments as aFunction
  1535  *         and returns the same result unless aFunction throws, in which case it logs
  1536  *         a warning and returns undefined.
  1537  */
  1538 function makeSafe(aFunction) {
  1539   return function(...aArgs) {
  1540     try {
  1541       return aFunction(...aArgs);
  1543     catch(ex) {
  1544       logger.warn("XPIProvider callback failed", ex);
  1546     return undefined;
  1550 this.XPIProvider = {
  1551   // An array of known install locations
  1552   installLocations: null,
  1553   // A dictionary of known install locations by name
  1554   installLocationsByName: null,
  1555   // An array of currently active AddonInstalls
  1556   installs: null,
  1557   // The default skin for the application
  1558   defaultSkin: "classic/1.0",
  1559   // The current skin used by the application
  1560   currentSkin: null,
  1561   // The selected skin to be used by the application when it is restarted. This
  1562   // will be the same as currentSkin when it is the skin to be used when the
  1563   // application is restarted
  1564   selectedSkin: null,
  1565   // The value of the minCompatibleAppVersion preference
  1566   minCompatibleAppVersion: null,
  1567   // The value of the minCompatiblePlatformVersion preference
  1568   minCompatiblePlatformVersion: null,
  1569   // A dictionary of the file descriptors for bootstrappable add-ons by ID
  1570   bootstrappedAddons: {},
  1571   // A dictionary of JS scopes of loaded bootstrappable add-ons by ID
  1572   bootstrapScopes: {},
  1573   // True if the platform could have activated extensions
  1574   extensionsActive: false,
  1575   // File / directory state of installed add-ons
  1576   installStates: [],
  1577   // True if all of the add-ons found during startup were installed in the
  1578   // application install location
  1579   allAppGlobal: true,
  1580   // A string listing the enabled add-ons for annotating crash reports
  1581   enabledAddons: null,
  1582   // An array of add-on IDs of add-ons that were inactive during startup
  1583   inactiveAddonIDs: [],
  1584   // Keep track of startup phases for telemetry
  1585   runPhase: XPI_STARTING,
  1586   // Keep track of the newest file in each add-on, in case we want to
  1587   // report it to telemetry.
  1588   _mostRecentlyModifiedFile: {},
  1589   // Per-addon telemetry information
  1590   _telemetryDetails: {},
  1591   // Experiments are disabled by default. Track ones that are locally enabled.
  1592   _enabledExperiments: null,
  1594   /*
  1595    * Set a value in the telemetry hash for a given ID
  1596    */
  1597   setTelemetry: function XPI_setTelemetry(aId, aName, aValue) {
  1598     if (!this._telemetryDetails[aId])
  1599       this._telemetryDetails[aId] = {};
  1600     this._telemetryDetails[aId][aName] = aValue;
  1601   },
  1603   // Keep track of in-progress operations that support cancel()
  1604   _inProgress: new Set(),
  1606   doing: function XPI_doing(aCancellable) {
  1607     this._inProgress.add(aCancellable);
  1608   },
  1610   done: function XPI_done(aCancellable) {
  1611     return this._inProgress.delete(aCancellable);
  1612   },
  1614   cancelAll: function XPI_cancelAll() {
  1615     // Cancelling one may alter _inProgress, so restart the iterator after each
  1616     while (this._inProgress.size > 0) {
  1617       for (let c of this._inProgress) {
  1618         try {
  1619           c.cancel();
  1621         catch (e) {
  1622           logger.warn("Cancel failed", e);
  1624         this._inProgress.delete(c);
  1627   },
  1629   /**
  1630    * Adds or updates a URI mapping for an Addon.id.
  1632    * Mappings should not be removed at any point. This is so that the mappings
  1633    * will be still valid after an add-on gets disabled or uninstalled, as
  1634    * consumers may still have URIs of (leaked) resources they want to map.
  1635    */
  1636   _addURIMapping: function XPI__addURIMapping(aID, aFile) {
  1637     try {
  1638       // Always use our own mechanics instead of nsIIOService.newFileURI, so
  1639       // that we can be sure to map things as we want them mapped.
  1640       let uri = this._resolveURIToFile(getURIForResourceInFile(aFile, "."));
  1641       if (!uri) {
  1642         throw new Error("Cannot resolve");
  1644       this._ensureURIMappings();
  1645       this._uriMappings[aID] = uri.spec;
  1647     catch (ex) {
  1648       logger.warn("Failed to add URI mapping", ex);
  1650   },
  1652   /**
  1653    * Ensures that the URI to Addon mappings are available.
  1655    * The function will add mappings for all non-bootstrapped but enabled
  1656    * add-ons.
  1657    * Bootstrapped add-on mappings will be added directly when the bootstrap
  1658    * scope get loaded. (See XPIProvider._addURIMapping() and callers)
  1659    */
  1660   _ensureURIMappings: function XPI__ensureURIMappings() {
  1661     if (this._uriMappings) {
  1662       return;
  1664     // XXX Convert to Map(), once it gets stable with stable iterators
  1665     this._uriMappings = Object.create(null);
  1667     // XXX Convert to Set(), once it gets stable with stable iterators
  1668     let enabled = Object.create(null);
  1669     let enabledAddons = this.enabledAddons || "";
  1670     for (let a of enabledAddons.split(",")) {
  1671       a = decodeURIComponent(a.split(":")[0]);
  1672       enabled[a] = null;
  1675     let cache = JSON.parse(Prefs.getCharPref(PREF_INSTALL_CACHE, "[]"));
  1676     for (let loc of cache) {
  1677       for (let [id, val] in Iterator(loc.addons)) {
  1678         if (!(id in enabled)) {
  1679           continue;
  1681         let file = new nsIFile(val.descriptor);
  1682         let spec = Services.io.newFileURI(file).spec;
  1683         this._uriMappings[id] = spec;
  1686   },
  1688   /**
  1689    * Resolve a URI back to physical file.
  1691    * Of course, this works only for URIs pointing to local resources.
  1693    * @param  aURI
  1694    *         URI to resolve
  1695    * @return
  1696    *         resolved nsIFileURL
  1697    */
  1698   _resolveURIToFile: function XPI__resolveURIToFile(aURI) {
  1699     switch (aURI.scheme) {
  1700       case "jar":
  1701       case "file":
  1702         if (aURI instanceof Ci.nsIJARURI) {
  1703           return this._resolveURIToFile(aURI.JARFile);
  1705         return aURI;
  1707       case "chrome":
  1708         aURI = ChromeRegistry.convertChromeURL(aURI);
  1709         return this._resolveURIToFile(aURI);
  1711       case "resource":
  1712         aURI = Services.io.newURI(ResProtocolHandler.resolveURI(aURI), null,
  1713                                   null);
  1714         return this._resolveURIToFile(aURI);
  1716       case "view-source":
  1717         aURI = Services.io.newURI(aURI.path, null, null);
  1718         return this._resolveURIToFile(aURI);
  1720       case "about":
  1721         if (aURI.spec == "about:blank") {
  1722           // Do not attempt to map about:blank
  1723           return null;
  1726         let chan;
  1727         try {
  1728           chan = Services.io.newChannelFromURI(aURI);
  1730         catch (ex) {
  1731           return null;
  1733         // Avoid looping
  1734         if (chan.URI.equals(aURI)) {
  1735           return null;
  1737         // We want to clone the channel URI to avoid accidentially keeping
  1738         // unnecessary references to the channel or implementation details
  1739         // around.
  1740         return this._resolveURIToFile(chan.URI.clone());
  1742       default:
  1743         return null;
  1745   },
  1747   /**
  1748    * Starts the XPI provider initializes the install locations and prefs.
  1750    * @param  aAppChanged
  1751    *         A tri-state value. Undefined means the current profile was created
  1752    *         for this session, true means the profile already existed but was
  1753    *         last used with an application with a different version number,
  1754    *         false means that the profile was last used by this version of the
  1755    *         application.
  1756    * @param  aOldAppVersion
  1757    *         The version of the application last run with this profile or null
  1758    *         if it is a new profile or the version is unknown
  1759    * @param  aOldPlatformVersion
  1760    *         The version of the platform last run with this profile or null
  1761    *         if it is a new profile or the version is unknown
  1762    */
  1763   startup: function XPI_startup(aAppChanged, aOldAppVersion, aOldPlatformVersion) {
  1764     function addDirectoryInstallLocation(aName, aKey, aPaths, aScope, aLocked) {
  1765       try {
  1766         var dir = FileUtils.getDir(aKey, aPaths);
  1768       catch (e) {
  1769         // Some directories aren't defined on some platforms, ignore them
  1770         logger.debug("Skipping unavailable install location " + aName);
  1771         return;
  1774       try {
  1775         var location = new DirectoryInstallLocation(aName, dir, aScope, aLocked);
  1777       catch (e) {
  1778         logger.warn("Failed to add directory install location " + aName, e);
  1779         return;
  1782       XPIProvider.installLocations.push(location);
  1783       XPIProvider.installLocationsByName[location.name] = location;
  1786     function addRegistryInstallLocation(aName, aRootkey, aScope) {
  1787       try {
  1788         var location = new WinRegInstallLocation(aName, aRootkey, aScope);
  1790       catch (e) {
  1791         logger.warn("Failed to add registry install location " + aName, e);
  1792         return;
  1795       XPIProvider.installLocations.push(location);
  1796       XPIProvider.installLocationsByName[location.name] = location;
  1799     try {
  1800       AddonManagerPrivate.recordTimestamp("XPI_startup_begin");
  1802       logger.debug("startup");
  1803       this.runPhase = XPI_STARTING;
  1804       this.installs = [];
  1805       this.installLocations = [];
  1806       this.installLocationsByName = {};
  1807       // Hook for tests to detect when saving database at shutdown time fails
  1808       this._shutdownError = null;
  1809       // Clear this at startup for xpcshell test restarts
  1810       this._telemetryDetails = {};
  1811       // Clear the set of enabled experiments (experiments disabled by default).
  1812       this._enabledExperiments = new Set();
  1813       // Register our details structure with AddonManager
  1814       AddonManagerPrivate.setTelemetryDetails("XPI", this._telemetryDetails);
  1816       let hasRegistry = ("nsIWindowsRegKey" in Ci);
  1818       let enabledScopes = Prefs.getIntPref(PREF_EM_ENABLED_SCOPES,
  1819                                            AddonManager.SCOPE_ALL);
  1821       // These must be in order of priority for processFileChanges etc. to work
  1822       if (enabledScopes & AddonManager.SCOPE_SYSTEM) {
  1823         if (hasRegistry) {
  1824           addRegistryInstallLocation("winreg-app-global",
  1825                                      Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
  1826                                      AddonManager.SCOPE_SYSTEM);
  1828         addDirectoryInstallLocation(KEY_APP_SYSTEM_LOCAL, "XRESysLExtPD",
  1829                                     [Services.appinfo.ID],
  1830                                     AddonManager.SCOPE_SYSTEM, true);
  1831         addDirectoryInstallLocation(KEY_APP_SYSTEM_SHARE, "XRESysSExtPD",
  1832                                     [Services.appinfo.ID],
  1833                                     AddonManager.SCOPE_SYSTEM, true);
  1836       if (enabledScopes & AddonManager.SCOPE_APPLICATION) {
  1837         addDirectoryInstallLocation(KEY_APP_GLOBAL, KEY_APPDIR,
  1838                                     [DIR_EXTENSIONS],
  1839                                     AddonManager.SCOPE_APPLICATION, true);
  1842       if (enabledScopes & AddonManager.SCOPE_USER) {
  1843         if (hasRegistry) {
  1844           addRegistryInstallLocation("winreg-app-user",
  1845                                      Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
  1846                                      AddonManager.SCOPE_USER);
  1848         addDirectoryInstallLocation(KEY_APP_SYSTEM_USER, "XREUSysExt",
  1849                                     [Services.appinfo.ID],
  1850                                     AddonManager.SCOPE_USER, true);
  1853       // The profile location is always enabled
  1854       addDirectoryInstallLocation(KEY_APP_PROFILE, KEY_PROFILEDIR,
  1855                                   [DIR_EXTENSIONS],
  1856                                   AddonManager.SCOPE_PROFILE, false);
  1858       this.defaultSkin = Prefs.getDefaultCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN,
  1859                                                   "classic/1.0");
  1860       this.currentSkin = Prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN,
  1861                                            this.defaultSkin);
  1862       this.selectedSkin = this.currentSkin;
  1863       this.applyThemeChange();
  1865       this.minCompatibleAppVersion = Prefs.getCharPref(PREF_EM_MIN_COMPAT_APP_VERSION,
  1866                                                        null);
  1867       this.minCompatiblePlatformVersion = Prefs.getCharPref(PREF_EM_MIN_COMPAT_PLATFORM_VERSION,
  1868                                                             null);
  1869       this.enabledAddons = "";
  1871       Services.prefs.addObserver(PREF_EM_MIN_COMPAT_APP_VERSION, this, false);
  1872       Services.prefs.addObserver(PREF_EM_MIN_COMPAT_PLATFORM_VERSION, this, false);
  1873       Services.obs.addObserver(this, NOTIFICATION_FLUSH_PERMISSIONS, false);
  1875       try {
  1876         BrowserToolboxProcess.on("connectionchange",
  1877                                  this.onDebugConnectionChange.bind(this));
  1879       catch (e) {
  1880         // BrowserToolboxProcess is not available in all applications
  1883       let flushCaches = this.checkForChanges(aAppChanged, aOldAppVersion,
  1884                                              aOldPlatformVersion);
  1886       // Changes to installed extensions may have changed which theme is selected
  1887       this.applyThemeChange();
  1889       // If the application has been upgraded and there are add-ons outside the
  1890       // application directory then we may need to synchronize compatibility
  1891       // information but only if the mismatch UI isn't disabled
  1892       if (aAppChanged && !this.allAppGlobal &&
  1893           Prefs.getBoolPref(PREF_EM_SHOW_MISMATCH_UI, true)) {
  1894         this.showUpgradeUI();
  1895         flushCaches = true;
  1897       else if (aAppChanged === undefined) {
  1898         // For new profiles we will never need to show the add-on selection UI
  1899         Services.prefs.setBoolPref(PREF_SHOWN_SELECTION_UI, true);
  1902       if (flushCaches) {
  1903         flushStartupCache();
  1905         // UI displayed early in startup (like the compatibility UI) may have
  1906         // caused us to cache parts of the skin or locale in memory. These must
  1907         // be flushed to allow extension provided skins and locales to take full
  1908         // effect
  1909         Services.obs.notifyObservers(null, "chrome-flush-skin-caches", null);
  1910         Services.obs.notifyObservers(null, "chrome-flush-caches", null);
  1913       this.enabledAddons = Prefs.getCharPref(PREF_EM_ENABLED_ADDONS, "");
  1915       // Invalidate the URI mappings now that |enabledAddons| was updated.
  1916       // |_ensureMappings()| will re-create the mappings when needed.
  1917       delete this._uriMappings;
  1919       if ("nsICrashReporter" in Ci &&
  1920           Services.appinfo instanceof Ci.nsICrashReporter) {
  1921         // Annotate the crash report with relevant add-on information.
  1922         try {
  1923           Services.appinfo.annotateCrashReport("Theme", this.currentSkin);
  1924         } catch (e) { }
  1925         try {
  1926           Services.appinfo.annotateCrashReport("EMCheckCompatibility",
  1927                                                AddonManager.checkCompatibility);
  1928         } catch (e) { }
  1929         this.addAddonsToCrashReporter();
  1932       try {
  1933         AddonManagerPrivate.recordTimestamp("XPI_bootstrap_addons_begin");
  1934         for (let id in this.bootstrappedAddons) {
  1935           try {
  1936             let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
  1937             file.persistentDescriptor = this.bootstrappedAddons[id].descriptor;
  1938             let reason = BOOTSTRAP_REASONS.APP_STARTUP;
  1939             // Eventually set INSTALLED reason when a bootstrap addon
  1940             // is dropped in profile folder and automatically installed
  1941             if (AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_INSTALLED)
  1942                             .indexOf(id) !== -1)
  1943               reason = BOOTSTRAP_REASONS.ADDON_INSTALL;
  1944             this.callBootstrapMethod(id, this.bootstrappedAddons[id].version,
  1945                                      this.bootstrappedAddons[id].type, file,
  1946                                      "startup", reason);
  1948           catch (e) {
  1949             logger.error("Failed to load bootstrap addon " + id + " from " +
  1950                   this.bootstrappedAddons[id].descriptor, e);
  1953         AddonManagerPrivate.recordTimestamp("XPI_bootstrap_addons_end");
  1955       catch (e) {
  1956         logger.error("bootstrap startup failed", e);
  1957         AddonManagerPrivate.recordException("XPI-BOOTSTRAP", "startup failed", e);
  1960       // Let these shutdown a little earlier when they still have access to most
  1961       // of XPCOM
  1962       Services.obs.addObserver({
  1963         observe: function shutdownObserver(aSubject, aTopic, aData) {
  1964           for (let id in XPIProvider.bootstrappedAddons) {
  1965             let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
  1966             file.persistentDescriptor = XPIProvider.bootstrappedAddons[id].descriptor;
  1967             XPIProvider.callBootstrapMethod(id, XPIProvider.bootstrappedAddons[id].version,
  1968                                             XPIProvider.bootstrappedAddons[id].type, file, "shutdown",
  1969                                             BOOTSTRAP_REASONS.APP_SHUTDOWN);
  1971           Services.obs.removeObserver(this, "quit-application-granted");
  1973       }, "quit-application-granted", false);
  1975       // Detect final-ui-startup for telemetry reporting
  1976       Services.obs.addObserver({
  1977         observe: function uiStartupObserver(aSubject, aTopic, aData) {
  1978           AddonManagerPrivate.recordTimestamp("XPI_finalUIStartup");
  1979           XPIProvider.runPhase = XPI_AFTER_UI_STARTUP;
  1980           Services.obs.removeObserver(this, "final-ui-startup");
  1982       }, "final-ui-startup", false);
  1984       AddonManagerPrivate.recordTimestamp("XPI_startup_end");
  1986       this.extensionsActive = true;
  1987       this.runPhase = XPI_BEFORE_UI_STARTUP;
  1989     catch (e) {
  1990       logger.error("startup failed", e);
  1991       AddonManagerPrivate.recordException("XPI", "startup failed", e);
  1993   },
  1995   /**
  1996    * Shuts down the database and releases all references.
  1997    * Return: Promise{integer} resolves / rejects with the result of
  1998    *                          flushing the XPI Database if it was loaded,
  1999    *                          0 otherwise.
  2000    */
  2001   shutdown: function XPI_shutdown() {
  2002     logger.debug("shutdown");
  2004     // Stop anything we were doing asynchronously
  2005     this.cancelAll();
  2007     this.bootstrappedAddons = {};
  2008     this.bootstrapScopes = {};
  2009     this.enabledAddons = null;
  2010     this.allAppGlobal = true;
  2012     this.inactiveAddonIDs = [];
  2014     // If there are pending operations then we must update the list of active
  2015     // add-ons
  2016     if (Prefs.getBoolPref(PREF_PENDING_OPERATIONS, false)) {
  2017       XPIDatabase.updateActiveAddons();
  2018       Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS,
  2019                                  !XPIDatabase.writeAddonsList());
  2022     this.installs = null;
  2023     this.installLocations = null;
  2024     this.installLocationsByName = null;
  2026     // This is needed to allow xpcshell tests to simulate a restart
  2027     this.extensionsActive = false;
  2029     // Remove URI mappings again
  2030     delete this._uriMappings;
  2032     if (gLazyObjectsLoaded) {
  2033       let done = XPIDatabase.shutdown();
  2034       done.then(
  2035         ret => {
  2036           logger.debug("Notifying XPI shutdown observers");
  2037           Services.obs.notifyObservers(null, "xpi-provider-shutdown", null);
  2038         },
  2039         err => {
  2040           logger.debug("Notifying XPI shutdown observers");
  2041           this._shutdownError = err;
  2042           Services.obs.notifyObservers(null, "xpi-provider-shutdown", err);
  2044       );
  2045       return done;
  2047     else {
  2048       logger.debug("Notifying XPI shutdown observers");
  2049       Services.obs.notifyObservers(null, "xpi-provider-shutdown", null);
  2051   },
  2053   /**
  2054    * Applies any pending theme change to the preferences.
  2055    */
  2056   applyThemeChange: function XPI_applyThemeChange() {
  2057     if (!Prefs.getBoolPref(PREF_DSS_SWITCHPENDING, false))
  2058       return;
  2060     // Tell the Chrome Registry which Skin to select
  2061     try {
  2062       this.selectedSkin = Prefs.getCharPref(PREF_DSS_SKIN_TO_SELECT);
  2063       Services.prefs.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN,
  2064                                  this.selectedSkin);
  2065       Services.prefs.clearUserPref(PREF_DSS_SKIN_TO_SELECT);
  2066       logger.debug("Changed skin to " + this.selectedSkin);
  2067       this.currentSkin = this.selectedSkin;
  2069     catch (e) {
  2070       logger.error("Error applying theme change", e);
  2072     Services.prefs.clearUserPref(PREF_DSS_SWITCHPENDING);
  2073   },
  2075   /**
  2076    * Shows the "Compatibility Updates" UI
  2077    */
  2078   showUpgradeUI: function XPI_showUpgradeUI() {
  2079     // Flip a flag to indicate that we interrupted startup with an interactive prompt
  2080     Services.startup.interrupted = true;
  2082     if (!Prefs.getBoolPref(PREF_SHOWN_SELECTION_UI, false)) {
  2083       // This *must* be modal as it has to block startup.
  2084       var features = "chrome,centerscreen,dialog,titlebar,modal";
  2085       Services.ww.openWindow(null, URI_EXTENSION_SELECT_DIALOG, "", features, null);
  2086       Services.prefs.setBoolPref(PREF_SHOWN_SELECTION_UI, true);
  2088     else {
  2089       var variant = Cc["@mozilla.org/variant;1"].
  2090                     createInstance(Ci.nsIWritableVariant);
  2091       variant.setFromVariant(this.inactiveAddonIDs);
  2093       // This *must* be modal as it has to block startup.
  2094       var features = "chrome,centerscreen,dialog,titlebar,modal";
  2095       var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].
  2096                getService(Ci.nsIWindowWatcher);
  2097       ww.openWindow(null, URI_EXTENSION_UPDATE_DIALOG, "", features, variant);
  2100     // Ensure any changes to the add-ons list are flushed to disk
  2101     Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS,
  2102                                !XPIDatabase.writeAddonsList());
  2103   },
  2105   /**
  2106    * Persists changes to XPIProvider.bootstrappedAddons to its store (a pref).
  2107    */
  2108   persistBootstrappedAddons: function XPI_persistBootstrappedAddons() {
  2109     // Experiments are disabled upon app load, so don't persist references.
  2110     let filtered = {};
  2111     for (let id in this.bootstrappedAddons) {
  2112       let entry = this.bootstrappedAddons[id];
  2113       if (entry.type == "experiment") {
  2114         continue;
  2117       filtered[id] = entry;
  2120     Services.prefs.setCharPref(PREF_BOOTSTRAP_ADDONS,
  2121                                JSON.stringify(filtered));
  2122   },
  2124   /**
  2125    * Adds a list of currently active add-ons to the next crash report.
  2126    */
  2127   addAddonsToCrashReporter: function XPI_addAddonsToCrashReporter() {
  2128     if (!("nsICrashReporter" in Ci) ||
  2129         !(Services.appinfo instanceof Ci.nsICrashReporter))
  2130       return;
  2132     // In safe mode no add-ons are loaded so we should not include them in the
  2133     // crash report
  2134     if (Services.appinfo.inSafeMode)
  2135       return;
  2137     let data = this.enabledAddons;
  2138     for (let id in this.bootstrappedAddons) {
  2139       data += (data ? "," : "") + encodeURIComponent(id) + ":" +
  2140               encodeURIComponent(this.bootstrappedAddons[id].version);
  2143     try {
  2144       Services.appinfo.annotateCrashReport("Add-ons", data);
  2146     catch (e) { }
  2148     Cu.import("resource://gre/modules/TelemetryPing.jsm", {}).TelemetryPing.setAddOns(data);
  2149   },
  2151   /**
  2152    * Gets the add-on states for an install location.
  2153    * This function may be expensive because of the recursiveLastModifiedTime call.
  2155    * @param  location
  2156    *         The install location to retrieve the add-on states for
  2157    * @return a dictionary mapping add-on IDs to objects with a descriptor
  2158    *         property which contains the add-ons dir/file descriptor and an
  2159    *         mtime property which contains the add-on's last modified time as
  2160    *         the number of milliseconds since the epoch.
  2161    */
  2162   getAddonStates: function XPI_getAddonStates(aLocation) {
  2163     let addonStates = {};
  2164     for (let file of aLocation.addonLocations) {
  2165       let scanStarted = Date.now();
  2166       let id = aLocation.getIDForLocation(file);
  2167       let unpacked = 0;
  2168       let [modFile, modTime, items] = recursiveLastModifiedTime(file);
  2169       addonStates[id] = {
  2170         descriptor: file.persistentDescriptor,
  2171         mtime: modTime
  2172       };
  2173       try {
  2174         // get the install.rdf update time, if any
  2175         file.append(FILE_INSTALL_MANIFEST);
  2176         let rdfTime = file.lastModifiedTime;
  2177         addonStates[id].rdfTime = rdfTime;
  2178         unpacked = 1;
  2180       catch (e) { }
  2181       this._mostRecentlyModifiedFile[id] = modFile;
  2182       this.setTelemetry(id, "unpacked", unpacked);
  2183       this.setTelemetry(id, "location", aLocation.name);
  2184       this.setTelemetry(id, "scan_MS", Date.now() - scanStarted);
  2185       this.setTelemetry(id, "scan_items", items);
  2188     return addonStates;
  2189   },
  2191   /**
  2192    * Gets an array of install location states which uniquely describes all
  2193    * installed add-ons with the add-on's InstallLocation name and last modified
  2194    * time. This function may be expensive because of the getAddonStates() call.
  2196    * @return an array of add-on states for each install location. Each state
  2197    *         is an object with a name property holding the location's name and
  2198    *         an addons property holding the add-on states for the location
  2199    */
  2200   getInstallLocationStates: function XPI_getInstallLocationStates() {
  2201     let states = [];
  2202     this.installLocations.forEach(function(aLocation) {
  2203       let addons = aLocation.addonLocations;
  2204       if (addons.length == 0)
  2205         return;
  2207       let locationState = {
  2208         name: aLocation.name,
  2209         addons: this.getAddonStates(aLocation)
  2210       };
  2212       states.push(locationState);
  2213     }, this);
  2214     return states;
  2215   },
  2217   /**
  2218    * Check the staging directories of install locations for any add-ons to be
  2219    * installed or add-ons to be uninstalled.
  2221    * @param  aManifests
  2222    *         A dictionary to add detected install manifests to for the purpose
  2223    *         of passing through updated compatibility information
  2224    * @return true if an add-on was installed or uninstalled
  2225    */
  2226   processPendingFileChanges: function XPI_processPendingFileChanges(aManifests) {
  2227     let changed = false;
  2228     this.installLocations.forEach(function(aLocation) {
  2229       aManifests[aLocation.name] = {};
  2230       // We can't install or uninstall anything in locked locations
  2231       if (aLocation.locked)
  2232         return;
  2234       let stagedXPIDir = aLocation.getXPIStagingDir();
  2235       let stagingDir = aLocation.getStagingDir();
  2237       if (stagedXPIDir.exists() && stagedXPIDir.isDirectory()) {
  2238         let entries = stagedXPIDir.directoryEntries
  2239                                   .QueryInterface(Ci.nsIDirectoryEnumerator);
  2240         while (entries.hasMoreElements()) {
  2241           let stageDirEntry = entries.nextFile;
  2243           if (!stageDirEntry.isDirectory()) {
  2244             logger.warn("Ignoring file in XPI staging directory: " + stageDirEntry.path);
  2245             continue;
  2248           // Find the last added XPI file in the directory
  2249           let stagedXPI = null;
  2250           var xpiEntries = stageDirEntry.directoryEntries
  2251                                         .QueryInterface(Ci.nsIDirectoryEnumerator);
  2252           while (xpiEntries.hasMoreElements()) {
  2253             let file = xpiEntries.nextFile;
  2254             if (file.isDirectory())
  2255               continue;
  2257             let extension = file.leafName;
  2258             extension = extension.substring(extension.length - 4);
  2260             if (extension != ".xpi" && extension != ".jar")
  2261               continue;
  2263             stagedXPI = file;
  2265           xpiEntries.close();
  2267           if (!stagedXPI)
  2268             continue;
  2270           let addon = null;
  2271           try {
  2272             addon = loadManifestFromZipFile(stagedXPI);
  2274           catch (e) {
  2275             logger.error("Unable to read add-on manifest from " + stagedXPI.path, e);
  2276             continue;
  2279           logger.debug("Migrating staged install of " + addon.id + " in " + aLocation.name);
  2281           if (addon.unpack || Prefs.getBoolPref(PREF_XPI_UNPACK, false)) {
  2282             let targetDir = stagingDir.clone();
  2283             targetDir.append(addon.id);
  2284             try {
  2285               targetDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
  2287             catch (e) {
  2288               logger.error("Failed to create staging directory for add-on " + addon.id, e);
  2289               continue;
  2292             try {
  2293               ZipUtils.extractFiles(stagedXPI, targetDir);
  2295             catch (e) {
  2296               logger.error("Failed to extract staged XPI for add-on " + addon.id + " in " +
  2297                     aLocation.name, e);
  2300           else {
  2301             try {
  2302               stagedXPI.moveTo(stagingDir, addon.id + ".xpi");
  2304             catch (e) {
  2305               logger.error("Failed to move staged XPI for add-on " + addon.id + " in " +
  2306                     aLocation.name, e);
  2310         entries.close();
  2313       if (stagedXPIDir.exists()) {
  2314         try {
  2315           recursiveRemove(stagedXPIDir);
  2317         catch (e) {
  2318           // Non-critical, just saves some perf on startup if we clean this up.
  2319           logger.debug("Error removing XPI staging dir " + stagedXPIDir.path, e);
  2323       try {
  2324         if (!stagingDir || !stagingDir.exists() || !stagingDir.isDirectory())
  2325           return;
  2327       catch (e) {
  2328         logger.warn("Failed to find staging directory", e);
  2329         return;
  2332       let seenFiles = [];
  2333       // Use a snapshot of the directory contents to avoid possible issues with
  2334       // iterating over a directory while removing files from it (the YAFFS2
  2335       // embedded filesystem has this issue, see bug 772238), and to remove
  2336       // normal files before their resource forks on OSX (see bug 733436).
  2337       let stagingDirEntries = getDirectoryEntries(stagingDir, true);
  2338       for (let stageDirEntry of stagingDirEntries) {
  2339         let id = stageDirEntry.leafName;
  2341         let isDir;
  2342         try {
  2343           isDir = stageDirEntry.isDirectory();
  2345         catch (e if e.result == Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) {
  2346           // If the file has already gone away then don't worry about it, this
  2347           // can happen on OSX where the resource fork is automatically moved
  2348           // with the data fork for the file. See bug 733436.
  2349           continue;
  2352         if (!isDir) {
  2353           if (id.substring(id.length - 4).toLowerCase() == ".xpi") {
  2354             id = id.substring(0, id.length - 4);
  2356           else {
  2357             if (id.substring(id.length - 5).toLowerCase() != ".json") {
  2358               logger.warn("Ignoring file: " + stageDirEntry.path);
  2359               seenFiles.push(stageDirEntry.leafName);
  2361             continue;
  2365         // Check that the directory's name is a valid ID.
  2366         if (!gIDTest.test(id)) {
  2367           logger.warn("Ignoring directory whose name is not a valid add-on ID: " +
  2368                stageDirEntry.path);
  2369           seenFiles.push(stageDirEntry.leafName);
  2370           continue;
  2373         changed = true;
  2375         if (isDir) {
  2376           // Check if the directory contains an install manifest.
  2377           let manifest = stageDirEntry.clone();
  2378           manifest.append(FILE_INSTALL_MANIFEST);
  2380           // If the install manifest doesn't exist uninstall this add-on in this
  2381           // install location.
  2382           if (!manifest.exists()) {
  2383             logger.debug("Processing uninstall of " + id + " in " + aLocation.name);
  2384             try {
  2385               aLocation.uninstallAddon(id);
  2386               seenFiles.push(stageDirEntry.leafName);
  2388             catch (e) {
  2389               logger.error("Failed to uninstall add-on " + id + " in " + aLocation.name, e);
  2391             // The file check later will spot the removal and cleanup the database
  2392             continue;
  2396         aManifests[aLocation.name][id] = null;
  2397         let existingAddonID = id;
  2399         let jsonfile = stagingDir.clone();
  2400         jsonfile.append(id + ".json");
  2402         try {
  2403           aManifests[aLocation.name][id] = loadManifestFromFile(stageDirEntry);
  2405         catch (e) {
  2406           logger.error("Unable to read add-on manifest from " + stageDirEntry.path, e);
  2407           // This add-on can't be installed so just remove it now
  2408           seenFiles.push(stageDirEntry.leafName);
  2409           seenFiles.push(jsonfile.leafName);
  2410           continue;
  2413         // Check for a cached metadata for this add-on, it may contain updated
  2414         // compatibility information
  2415         if (jsonfile.exists()) {
  2416           logger.debug("Found updated metadata for " + id + " in " + aLocation.name);
  2417           let fis = Cc["@mozilla.org/network/file-input-stream;1"].
  2418                        createInstance(Ci.nsIFileInputStream);
  2419           let json = Cc["@mozilla.org/dom/json;1"].
  2420                      createInstance(Ci.nsIJSON);
  2422           try {
  2423             fis.init(jsonfile, -1, 0, 0);
  2424             let metadata = json.decodeFromStream(fis, jsonfile.fileSize);
  2425             aManifests[aLocation.name][id].importMetadata(metadata);
  2427           catch (e) {
  2428             // If some data can't be recovered from the cached metadata then it
  2429             // is unlikely to be a problem big enough to justify throwing away
  2430             // the install, just log and error and continue
  2431             logger.error("Unable to read metadata from " + jsonfile.path, e);
  2433           finally {
  2434             fis.close();
  2437         seenFiles.push(jsonfile.leafName);
  2439         existingAddonID = aManifests[aLocation.name][id].existingAddonID || id;
  2441         var oldBootstrap = null;
  2442         logger.debug("Processing install of " + id + " in " + aLocation.name);
  2443         if (existingAddonID in this.bootstrappedAddons) {
  2444           try {
  2445             var existingAddon = aLocation.getLocationForID(existingAddonID);
  2446             if (this.bootstrappedAddons[existingAddonID].descriptor ==
  2447                 existingAddon.persistentDescriptor) {
  2448               oldBootstrap = this.bootstrappedAddons[existingAddonID];
  2450               // We'll be replacing a currently active bootstrapped add-on so
  2451               // call its uninstall method
  2452               let newVersion = aManifests[aLocation.name][id].version;
  2453               let oldVersion = oldBootstrap.version;
  2454               let uninstallReason = Services.vc.compare(oldVersion, newVersion) < 0 ?
  2455                                     BOOTSTRAP_REASONS.ADDON_UPGRADE :
  2456                                     BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
  2458               this.callBootstrapMethod(existingAddonID, oldBootstrap.version,
  2459                                        oldBootstrap.type, existingAddon, "uninstall", uninstallReason,
  2460                                        { newVersion: newVersion });
  2461               this.unloadBootstrapScope(existingAddonID);
  2462               flushStartupCache();
  2465           catch (e) {
  2469         try {
  2470           var addonInstallLocation = aLocation.installAddon(id, stageDirEntry,
  2471                                                             existingAddonID);
  2472           if (aManifests[aLocation.name][id])
  2473             aManifests[aLocation.name][id]._sourceBundle = addonInstallLocation;
  2475         catch (e) {
  2476           logger.error("Failed to install staged add-on " + id + " in " + aLocation.name,
  2477                 e);
  2478           // Re-create the staged install
  2479           AddonInstall.createStagedInstall(aLocation, stageDirEntry,
  2480                                            aManifests[aLocation.name][id]);
  2481           // Make sure not to delete the cached manifest json file
  2482           seenFiles.pop();
  2484           delete aManifests[aLocation.name][id];
  2486           if (oldBootstrap) {
  2487             // Re-install the old add-on
  2488             this.callBootstrapMethod(existingAddonID, oldBootstrap.version,
  2489                                      oldBootstrap.type, existingAddon, "install",
  2490                                      BOOTSTRAP_REASONS.ADDON_INSTALL);
  2492           continue;
  2496       try {
  2497         aLocation.cleanStagingDir(seenFiles);
  2499       catch (e) {
  2500         // Non-critical, just saves some perf on startup if we clean this up.
  2501         logger.debug("Error cleaning staging dir " + stagingDir.path, e);
  2503     }, this);
  2504     return changed;
  2505   },
  2507   /**
  2508    * Installs any add-ons located in the extensions directory of the
  2509    * application's distribution specific directory into the profile unless a
  2510    * newer version already exists or the user has previously uninstalled the
  2511    * distributed add-on.
  2513    * @param  aManifests
  2514    *         A dictionary to add new install manifests to to save having to
  2515    *         reload them later
  2516    * @return true if any new add-ons were installed
  2517    */
  2518   installDistributionAddons: function XPI_installDistributionAddons(aManifests) {
  2519     let distroDir;
  2520     try {
  2521       distroDir = FileUtils.getDir(KEY_APP_DISTRIBUTION, [DIR_EXTENSIONS]);
  2523     catch (e) {
  2524       return false;
  2527     if (!distroDir.exists())
  2528       return false;
  2530     if (!distroDir.isDirectory())
  2531       return false;
  2533     let changed = false;
  2534     let profileLocation = this.installLocationsByName[KEY_APP_PROFILE];
  2536     let entries = distroDir.directoryEntries
  2537                            .QueryInterface(Ci.nsIDirectoryEnumerator);
  2538     let entry;
  2539     while ((entry = entries.nextFile)) {
  2541       let id = entry.leafName;
  2543       if (entry.isFile()) {
  2544         if (id.substring(id.length - 4).toLowerCase() == ".xpi") {
  2545           id = id.substring(0, id.length - 4);
  2547         else {
  2548           logger.debug("Ignoring distribution add-on that isn't an XPI: " + entry.path);
  2549           continue;
  2552       else if (!entry.isDirectory()) {
  2553         logger.debug("Ignoring distribution add-on that isn't a file or directory: " +
  2554             entry.path);
  2555         continue;
  2558       if (!gIDTest.test(id)) {
  2559         logger.debug("Ignoring distribution add-on whose name is not a valid add-on ID: " +
  2560             entry.path);
  2561         continue;
  2564       let addon;
  2565       try {
  2566         addon = loadManifestFromFile(entry);
  2568       catch (e) {
  2569         logger.warn("File entry " + entry.path + " contains an invalid add-on", e);
  2570         continue;
  2573       if (addon.id != id) {
  2574         logger.warn("File entry " + entry.path + " contains an add-on with an " +
  2575              "incorrect ID")
  2576         continue;
  2579       let existingEntry = null;
  2580       try {
  2581         existingEntry = profileLocation.getLocationForID(id);
  2583       catch (e) {
  2586       if (existingEntry) {
  2587         let existingAddon;
  2588         try {
  2589           existingAddon = loadManifestFromFile(existingEntry);
  2591           if (Services.vc.compare(addon.version, existingAddon.version) <= 0)
  2592             continue;
  2594         catch (e) {
  2595           // Bad add-on in the profile so just proceed and install over the top
  2596           logger.warn("Profile contains an add-on with a bad or missing install " +
  2597                "manifest at " + existingEntry.path + ", overwriting", e);
  2600       else if (Prefs.getBoolPref(PREF_BRANCH_INSTALLED_ADDON + id, false)) {
  2601         continue;
  2604       // Install the add-on
  2605       try {
  2606         profileLocation.installAddon(id, entry, null, true);
  2607         logger.debug("Installed distribution add-on " + id);
  2609         Services.prefs.setBoolPref(PREF_BRANCH_INSTALLED_ADDON + id, true)
  2611         // aManifests may contain a copy of a newly installed add-on's manifest
  2612         // and we'll have overwritten that so instead cache our install manifest
  2613         // which will later be put into the database in processFileChanges
  2614         if (!(KEY_APP_PROFILE in aManifests))
  2615           aManifests[KEY_APP_PROFILE] = {};
  2616         aManifests[KEY_APP_PROFILE][id] = addon;
  2617         changed = true;
  2619       catch (e) {
  2620         logger.error("Failed to install distribution add-on " + entry.path, e);
  2624     entries.close();
  2626     return changed;
  2627   },
  2629   /**
  2630    * Compares the add-ons that are currently installed to those that were
  2631    * known to be installed when the application last ran and applies any
  2632    * changes found to the database. Also sends "startupcache-invalidate" signal to
  2633    * observerservice if it detects that data may have changed.
  2635    * @param  aState
  2636    *         The array of current install location states
  2637    * @param  aManifests
  2638    *         A dictionary of cached AddonInstalls for add-ons that have been
  2639    *         installed
  2640    * @param  aUpdateCompatibility
  2641    *         true to update add-ons appDisabled property when the application
  2642    *         version has changed
  2643    * @param  aOldAppVersion
  2644    *         The version of the application last run with this profile or null
  2645    *         if it is a new profile or the version is unknown
  2646    * @param  aOldPlatformVersion
  2647    *         The version of the platform last run with this profile or null
  2648    *         if it is a new profile or the version is unknown
  2649    * @return a boolean indicating if a change requiring flushing the caches was
  2650    *         detected
  2651    */
  2652   processFileChanges: function XPI_processFileChanges(aState, aManifests,
  2653                                                       aUpdateCompatibility,
  2654                                                       aOldAppVersion,
  2655                                                       aOldPlatformVersion) {
  2656     let visibleAddons = {};
  2657     let oldBootstrappedAddons = this.bootstrappedAddons;
  2658     this.bootstrappedAddons = {};
  2660     /**
  2661      * Updates an add-on's metadata and determines if a restart of the
  2662      * application is necessary. This is called when either the add-on's
  2663      * install directory path or last modified time has changed.
  2665      * @param  aInstallLocation
  2666      *         The install location containing the add-on
  2667      * @param  aOldAddon
  2668      *         The AddonInternal as it appeared the last time the application
  2669      *         ran
  2670      * @param  aAddonState
  2671      *         The new state of the add-on
  2672      * @return a boolean indicating if flushing caches is required to complete
  2673      *         changing this add-on
  2674      */
  2675     function updateMetadata(aInstallLocation, aOldAddon, aAddonState) {
  2676       logger.debug("Add-on " + aOldAddon.id + " modified in " + aInstallLocation.name);
  2678       // Check if there is an updated install manifest for this add-on
  2679       let newAddon = aManifests[aInstallLocation.name][aOldAddon.id];
  2681       try {
  2682         // If not load it
  2683         if (!newAddon) {
  2684           let file = aInstallLocation.getLocationForID(aOldAddon.id);
  2685           newAddon = loadManifestFromFile(file);
  2686           applyBlocklistChanges(aOldAddon, newAddon);
  2688           // Carry over any pendingUninstall state to add-ons modified directly
  2689           // in the profile. This is important when the attempt to remove the
  2690           // add-on in processPendingFileChanges failed and caused an mtime
  2691           // change to the add-ons files.
  2692           newAddon.pendingUninstall = aOldAddon.pendingUninstall;
  2695         // The ID in the manifest that was loaded must match the ID of the old
  2696         // add-on.
  2697         if (newAddon.id != aOldAddon.id)
  2698           throw new Error("Incorrect id in install manifest");
  2700       catch (e) {
  2701         logger.warn("Add-on is invalid", e);
  2702         XPIDatabase.removeAddonMetadata(aOldAddon);
  2703         if (!aInstallLocation.locked)
  2704           aInstallLocation.uninstallAddon(aOldAddon.id);
  2705         else
  2706           logger.warn("Could not uninstall invalid item from locked install location");
  2707         // If this was an active add-on then we must force a restart
  2708         if (aOldAddon.active)
  2709           return true;
  2711         return false;
  2714       // Set the additional properties on the new AddonInternal
  2715       newAddon._installLocation = aInstallLocation;
  2716       newAddon.updateDate = aAddonState.mtime;
  2717       newAddon.visible = !(newAddon.id in visibleAddons);
  2719       // Update the database
  2720       let newDBAddon = XPIDatabase.updateAddonMetadata(aOldAddon, newAddon,
  2721                                                        aAddonState.descriptor);
  2722       if (newDBAddon.visible) {
  2723         visibleAddons[newDBAddon.id] = newDBAddon;
  2724         // Remember add-ons that were changed during startup
  2725         AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED,
  2726                                              newDBAddon.id);
  2728         // If this was the active theme and it is now disabled then enable the
  2729         // default theme
  2730         if (aOldAddon.active && isAddonDisabled(newDBAddon))
  2731           XPIProvider.enableDefaultTheme();
  2733         // If the new add-on is bootstrapped and active then call its install method
  2734         if (newDBAddon.active && newDBAddon.bootstrap) {
  2735           // Startup cache must be flushed before calling the bootstrap script
  2736           flushStartupCache();
  2738           let installReason = Services.vc.compare(aOldAddon.version, newDBAddon.version) < 0 ?
  2739                               BOOTSTRAP_REASONS.ADDON_UPGRADE :
  2740                               BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
  2742           let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
  2743           file.persistentDescriptor = aAddonState.descriptor;
  2744           XPIProvider.callBootstrapMethod(newDBAddon.id, newDBAddon.version,
  2745                                           newDBAddon.type, file, "install",
  2746                                           installReason, { oldVersion: aOldAddon.version });
  2747           return false;
  2750         return true;
  2753       return false;
  2756     /**
  2757      * Updates an add-on's descriptor for when the add-on has moved in the
  2758      * filesystem but hasn't changed in any other way.
  2760      * @param  aInstallLocation
  2761      *         The install location containing the add-on
  2762      * @param  aOldAddon
  2763      *         The AddonInternal as it appeared the last time the application
  2764      *         ran
  2765      * @param  aAddonState
  2766      *         The new state of the add-on
  2767      * @return a boolean indicating if flushing caches is required to complete
  2768      *         changing this add-on
  2769      */
  2770     function updateDescriptor(aInstallLocation, aOldAddon, aAddonState) {
  2771       logger.debug("Add-on " + aOldAddon.id + " moved to " + aAddonState.descriptor);
  2773       aOldAddon.descriptor = aAddonState.descriptor;
  2774       aOldAddon.visible = !(aOldAddon.id in visibleAddons);
  2775       XPIDatabase.saveChanges();
  2777       if (aOldAddon.visible) {
  2778         visibleAddons[aOldAddon.id] = aOldAddon;
  2780         if (aOldAddon.bootstrap && aOldAddon.active) {
  2781           let bootstrap = oldBootstrappedAddons[aOldAddon.id];
  2782           bootstrap.descriptor = aAddonState.descriptor;
  2783           XPIProvider.bootstrappedAddons[aOldAddon.id] = bootstrap;
  2786         return true;
  2789       return false;
  2792     /**
  2793      * Called when no change has been detected for an add-on's metadata. The
  2794      * add-on may have become visible due to other add-ons being removed or
  2795      * the add-on may need to be updated when the application version has
  2796      * changed.
  2798      * @param  aInstallLocation
  2799      *         The install location containing the add-on
  2800      * @param  aOldAddon
  2801      *         The AddonInternal as it appeared the last time the application
  2802      *         ran
  2803      * @param  aAddonState
  2804      *         The new state of the add-on
  2805      * @return a boolean indicating if flushing caches is required to complete
  2806      *         changing this add-on
  2807      */
  2808     function updateVisibilityAndCompatibility(aInstallLocation, aOldAddon,
  2809                                               aAddonState) {
  2810       let changed = false;
  2812       // This add-ons metadata has not changed but it may have become visible
  2813       if (!(aOldAddon.id in visibleAddons)) {
  2814         visibleAddons[aOldAddon.id] = aOldAddon;
  2816         if (!aOldAddon.visible) {
  2817           // Remember add-ons that were changed during startup.
  2818           AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED,
  2819                                                aOldAddon.id);
  2820           XPIDatabase.makeAddonVisible(aOldAddon);
  2822           if (aOldAddon.bootstrap) {
  2823             // The add-on is bootstrappable so call its install script
  2824             let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
  2825             file.persistentDescriptor = aAddonState.descriptor;
  2826             XPIProvider.callBootstrapMethod(aOldAddon.id, aOldAddon.version, aOldAddon.type, file,
  2827                                             "install",
  2828                                             BOOTSTRAP_REASONS.ADDON_INSTALL);
  2830             // If it should be active then mark it as active otherwise unload
  2831             // its scope
  2832             if (!isAddonDisabled(aOldAddon)) {
  2833               XPIDatabase.updateAddonActive(aOldAddon, true);
  2835             else {
  2836               XPIProvider.unloadBootstrapScope(newAddon.id);
  2839           else {
  2840             // Otherwise a restart is necessary
  2841             changed = true;
  2846       // App version changed, we may need to update the appDisabled property.
  2847       if (aUpdateCompatibility) {
  2848         let wasDisabled = isAddonDisabled(aOldAddon);
  2849         let wasAppDisabled = aOldAddon.appDisabled;
  2850         let wasUserDisabled = aOldAddon.userDisabled;
  2851         let wasSoftDisabled = aOldAddon.softDisabled;
  2853         // This updates the addon's JSON cached data in place
  2854         applyBlocklistChanges(aOldAddon, aOldAddon, aOldAppVersion,
  2855                               aOldPlatformVersion);
  2856         aOldAddon.appDisabled = !isUsableAddon(aOldAddon);
  2858         let isDisabled = isAddonDisabled(aOldAddon);
  2860         // If either property has changed update the database.
  2861         if (wasAppDisabled != aOldAddon.appDisabled ||
  2862             wasUserDisabled != aOldAddon.userDisabled ||
  2863             wasSoftDisabled != aOldAddon.softDisabled) {
  2864           logger.debug("Add-on " + aOldAddon.id + " changed appDisabled state to " +
  2865               aOldAddon.appDisabled + ", userDisabled state to " +
  2866               aOldAddon.userDisabled + " and softDisabled state to " +
  2867               aOldAddon.softDisabled);
  2868           XPIDatabase.saveChanges();
  2871         // If this is a visible add-on and it has changed disabled state then we
  2872         // may need a restart or to update the bootstrap list.
  2873         if (aOldAddon.visible && wasDisabled != isDisabled) {
  2874           // Remember add-ons that became disabled or enabled by the application
  2875           // change
  2876           let change = isDisabled ? AddonManager.STARTUP_CHANGE_DISABLED
  2877                                   : AddonManager.STARTUP_CHANGE_ENABLED;
  2878           AddonManagerPrivate.addStartupChange(change, aOldAddon.id);
  2879           if (aOldAddon.bootstrap) {
  2880             // Update the add-ons active state
  2881             XPIDatabase.updateAddonActive(aOldAddon, !isDisabled);
  2883           else {
  2884             changed = true;
  2889       if (aOldAddon.visible && aOldAddon.active && aOldAddon.bootstrap) {
  2890         XPIProvider.bootstrappedAddons[aOldAddon.id] = {
  2891           version: aOldAddon.version,
  2892           type: aOldAddon.type,
  2893           descriptor: aAddonState.descriptor
  2894         };
  2897       return changed;
  2900     /**
  2901      * Called when an add-on has been removed.
  2903      * @param  aOldAddon
  2904      *         The AddonInternal as it appeared the last time the application
  2905      *         ran
  2906      * @return a boolean indicating if flushing caches is required to complete
  2907      *         changing this add-on
  2908      */
  2909     function removeMetadata(aOldAddon) {
  2910       // This add-on has disappeared
  2911       logger.debug("Add-on " + aOldAddon.id + " removed from " + aOldAddon.location);
  2912       XPIDatabase.removeAddonMetadata(aOldAddon);
  2914       // Remember add-ons that were uninstalled during startup
  2915       if (aOldAddon.visible) {
  2916         AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_UNINSTALLED,
  2917                                              aOldAddon.id);
  2919       else if (AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_INSTALLED)
  2920                            .indexOf(aOldAddon.id) != -1) {
  2921         AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED,
  2922                                              aOldAddon.id);
  2925       if (aOldAddon.active) {
  2926         // Enable the default theme if the previously active theme has been
  2927         // removed
  2928         if (aOldAddon.type == "theme")
  2929           XPIProvider.enableDefaultTheme();
  2931         return true;
  2934       return false;
  2937     /**
  2938      * Called to add the metadata for an add-on in one of the install locations
  2939      * to the database. This can be called in three different cases. Either an
  2940      * add-on has been dropped into the location from outside of Firefox, or
  2941      * an add-on has been installed through the application, or the database
  2942      * has been upgraded or become corrupt and add-on data has to be reloaded
  2943      * into it.
  2945      * @param  aInstallLocation
  2946      *         The install location containing the add-on
  2947      * @param  aId
  2948      *         The ID of the add-on
  2949      * @param  aAddonState
  2950      *         The new state of the add-on
  2951      * @param  aMigrateData
  2952      *         If during startup the database had to be upgraded this will
  2953      *         contain data that used to be held about this add-on
  2954      * @return a boolean indicating if flushing caches is required to complete
  2955      *         changing this add-on
  2956      */
  2957     function addMetadata(aInstallLocation, aId, aAddonState, aMigrateData) {
  2958       logger.debug("New add-on " + aId + " installed in " + aInstallLocation.name);
  2960       let newAddon = null;
  2961       let sameVersion = false;
  2962       // Check the updated manifests lists for the install location, If there
  2963       // is no manifest for the add-on ID then newAddon will be undefined
  2964       if (aInstallLocation.name in aManifests)
  2965         newAddon = aManifests[aInstallLocation.name][aId];
  2967       // If we had staged data for this add-on or we aren't recovering from a
  2968       // corrupt database and we don't have migration data for this add-on then
  2969       // this must be a new install.
  2970       let isNewInstall = (!!newAddon) || (!XPIDatabase.activeBundles && !aMigrateData);
  2972       // If it's a new install and we haven't yet loaded the manifest then it
  2973       // must be something dropped directly into the install location
  2974       let isDetectedInstall = isNewInstall && !newAddon;
  2976       // Load the manifest if necessary and sanity check the add-on ID
  2977       try {
  2978         if (!newAddon) {
  2979           // Load the manifest from the add-on.
  2980           let file = aInstallLocation.getLocationForID(aId);
  2981           newAddon = loadManifestFromFile(file);
  2983         // The add-on in the manifest should match the add-on ID.
  2984         if (newAddon.id != aId) {
  2985           throw new Error("Invalid addon ID: expected addon ID " + aId +
  2986                           ", found " + newAddon.id + " in manifest");
  2989       catch (e) {
  2990         logger.warn("Add-on is invalid", e);
  2992         // Remove the invalid add-on from the install location if the install
  2993         // location isn't locked, no restart will be necessary
  2994         if (!aInstallLocation.locked)
  2995           aInstallLocation.uninstallAddon(aId);
  2996         else
  2997           logger.warn("Could not uninstall invalid item from locked install location");
  2998         return false;
  3001       // Update the AddonInternal properties.
  3002       newAddon._installLocation = aInstallLocation;
  3003       newAddon.visible = !(newAddon.id in visibleAddons);
  3004       newAddon.installDate = aAddonState.mtime;
  3005       newAddon.updateDate = aAddonState.mtime;
  3006       newAddon.foreignInstall = isDetectedInstall;
  3008       if (aMigrateData) {
  3009         // If there is migration data then apply it.
  3010         logger.debug("Migrating data from old database");
  3012         DB_MIGRATE_METADATA.forEach(function(aProp) {
  3013           // A theme's disabled state is determined by the selected theme
  3014           // preference which is read in loadManifestFromRDF
  3015           if (aProp == "userDisabled" && newAddon.type == "theme")
  3016             return;
  3018           if (aProp in aMigrateData)
  3019             newAddon[aProp] = aMigrateData[aProp];
  3020         });
  3022         // Force all non-profile add-ons to be foreignInstalls since they can't
  3023         // have been installed through the API
  3024         newAddon.foreignInstall |= aInstallLocation.name != KEY_APP_PROFILE;
  3026         // Some properties should only be migrated if the add-on hasn't changed.
  3027         // The version property isn't a perfect check for this but covers the
  3028         // vast majority of cases.
  3029         if (aMigrateData.version == newAddon.version) {
  3030           logger.debug("Migrating compatibility info");
  3031           sameVersion = true;
  3032           if ("targetApplications" in aMigrateData)
  3033             newAddon.applyCompatibilityUpdate(aMigrateData, true);
  3036         // Since the DB schema has changed make sure softDisabled is correct
  3037         applyBlocklistChanges(newAddon, newAddon, aOldAppVersion,
  3038                               aOldPlatformVersion);
  3041       // The default theme is never a foreign install
  3042       if (newAddon.type == "theme" && newAddon.internalName == XPIProvider.defaultSkin)
  3043         newAddon.foreignInstall = false;
  3045       if (isDetectedInstall && newAddon.foreignInstall) {
  3046         // If the add-on is a foreign install and is in a scope where add-ons
  3047         // that were dropped in should default to disabled then disable it
  3048         let disablingScopes = Prefs.getIntPref(PREF_EM_AUTO_DISABLED_SCOPES, 0);
  3049         if (aInstallLocation.scope & disablingScopes)
  3050           newAddon.userDisabled = true;
  3053       // If we have a list of what add-ons should be marked as active then use
  3054       // it to guess at migration data.
  3055       if (!isNewInstall && XPIDatabase.activeBundles) {
  3056         // For themes we know which is active by the current skin setting
  3057         if (newAddon.type == "theme")
  3058           newAddon.active = newAddon.internalName == XPIProvider.currentSkin;
  3059         else
  3060           newAddon.active = XPIDatabase.activeBundles.indexOf(aAddonState.descriptor) != -1;
  3062         // If the add-on wasn't active and it isn't already disabled in some way
  3063         // then it was probably either softDisabled or userDisabled
  3064         if (!newAddon.active && newAddon.visible && !isAddonDisabled(newAddon)) {
  3065           // If the add-on is softblocked then assume it is softDisabled
  3066           if (newAddon.blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED)
  3067             newAddon.softDisabled = true;
  3068           else
  3069             newAddon.userDisabled = true;
  3072       else {
  3073         newAddon.active = (newAddon.visible && !isAddonDisabled(newAddon))
  3076       let newDBAddon = XPIDatabase.addAddonMetadata(newAddon, aAddonState.descriptor);
  3078       if (newDBAddon.visible) {
  3079         // Remember add-ons that were first detected during startup.
  3080         if (isDetectedInstall) {
  3081           // If a copy from a higher priority location was removed then this
  3082           // add-on has changed
  3083           if (AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_UNINSTALLED)
  3084                           .indexOf(newDBAddon.id) != -1) {
  3085             AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED,
  3086                                                  newDBAddon.id);
  3088           else {
  3089             AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_INSTALLED,
  3090                                                  newDBAddon.id);
  3094         // Note if any visible add-on is not in the application install location
  3095         if (newDBAddon._installLocation.name != KEY_APP_GLOBAL)
  3096           XPIProvider.allAppGlobal = false;
  3098         visibleAddons[newDBAddon.id] = newDBAddon;
  3100         let installReason = BOOTSTRAP_REASONS.ADDON_INSTALL;
  3101         let extraParams = {};
  3103         // If we're hiding a bootstrapped add-on then call its uninstall method
  3104         if (newDBAddon.id in oldBootstrappedAddons) {
  3105           let oldBootstrap = oldBootstrappedAddons[newDBAddon.id];
  3106           extraParams.oldVersion = oldBootstrap.version;
  3107           XPIProvider.bootstrappedAddons[newDBAddon.id] = oldBootstrap;
  3109           // If the old version is the same as the new version, or we're
  3110           // recovering from a corrupt DB, don't call uninstall and install
  3111           // methods.
  3112           if (sameVersion || !isNewInstall)
  3113             return false;
  3115           installReason = Services.vc.compare(oldBootstrap.version, newDBAddon.version) < 0 ?
  3116                           BOOTSTRAP_REASONS.ADDON_UPGRADE :
  3117                           BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
  3119           let oldAddonFile = Cc["@mozilla.org/file/local;1"].
  3120                              createInstance(Ci.nsIFile);
  3121           oldAddonFile.persistentDescriptor = oldBootstrap.descriptor;
  3123           XPIProvider.callBootstrapMethod(newDBAddon.id, oldBootstrap.version,
  3124                                           oldBootstrap.type, oldAddonFile, "uninstall",
  3125                                           installReason, { newVersion: newDBAddon.version });
  3126           XPIProvider.unloadBootstrapScope(newDBAddon.id);
  3128           // If the new add-on is bootstrapped then we must flush the caches
  3129           // before calling the new bootstrap script
  3130           if (newDBAddon.bootstrap)
  3131             flushStartupCache();
  3134         if (!newDBAddon.bootstrap)
  3135           return true;
  3137         // Visible bootstrapped add-ons need to have their install method called
  3138         let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
  3139         file.persistentDescriptor = aAddonState.descriptor;
  3140         XPIProvider.callBootstrapMethod(newDBAddon.id, newDBAddon.version, newDBAddon.type, file,
  3141                                         "install", installReason, extraParams);
  3142         if (!newDBAddon.active)
  3143           XPIProvider.unloadBootstrapScope(newDBAddon.id);
  3146       return false;
  3149     let changed = false;
  3150     let knownLocations = XPIDatabase.getInstallLocations();
  3152     // The install locations are iterated in reverse order of priority so when
  3153     // there are multiple add-ons installed with the same ID the one that
  3154     // should be visible is the first one encountered.
  3155     for (let aSt of aState.reverse()) {
  3157       // We can't include the install location directly in the state as it has
  3158       // to be cached as JSON.
  3159       let installLocation = this.installLocationsByName[aSt.name];
  3160       let addonStates = aSt.addons;
  3162       // Check if the database knows about any add-ons in this install location.
  3163       if (knownLocations.has(installLocation.name)) {
  3164         knownLocations.delete(installLocation.name);
  3165         let addons = XPIDatabase.getAddonsInLocation(installLocation.name);
  3166         // Iterate through the add-ons installed the last time the application
  3167         // ran
  3168         for (let aOldAddon of addons) {
  3169           // If a version of this add-on has been installed in an higher
  3170           // priority install location then count it as changed
  3171           if (AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_INSTALLED)
  3172                           .indexOf(aOldAddon.id) != -1) {
  3173             AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED,
  3174                                                  aOldAddon.id);
  3177           // Check if the add-on is still installed
  3178           if (aOldAddon.id in addonStates) {
  3179             let addonState = addonStates[aOldAddon.id];
  3180             delete addonStates[aOldAddon.id];
  3182             // Remember add-ons that were inactive during startup
  3183             if (aOldAddon.visible && !aOldAddon.active)
  3184               XPIProvider.inactiveAddonIDs.push(aOldAddon.id);
  3186             // record a bit more per-addon telemetry
  3187             let loc = aOldAddon.defaultLocale;
  3188             if (loc) {
  3189               XPIProvider.setTelemetry(aOldAddon.id, "name", loc.name);
  3190               XPIProvider.setTelemetry(aOldAddon.id, "creator", loc.creator);
  3193             // Check if the add-on has been changed outside the XPI provider
  3194             if (aOldAddon.updateDate != addonState.mtime) {
  3195               // Did time change in the wrong direction?
  3196               if (addonState.mtime < aOldAddon.updateDate) {
  3197                 this.setTelemetry(aOldAddon.id, "olderFile", {
  3198                   name: this._mostRecentlyModifiedFile[aOldAddon.id],
  3199                   mtime: addonState.mtime,
  3200                   oldtime: aOldAddon.updateDate
  3201                 });
  3203               // Is the add-on unpacked?
  3204               else if (addonState.rdfTime) {
  3205                 // Was the addon manifest "install.rdf" modified, or some other file?
  3206                 if (addonState.rdfTime > aOldAddon.updateDate) {
  3207                   this.setTelemetry(aOldAddon.id, "modifiedInstallRDF", 1);
  3209                 else {
  3210                   this.setTelemetry(aOldAddon.id, "modifiedFile",
  3211                                     this._mostRecentlyModifiedFile[aOldAddon.id]);
  3214               else {
  3215                 this.setTelemetry(aOldAddon.id, "modifiedXPI", 1);
  3219             // The add-on has changed if the modification time has changed, or
  3220             // we have an updated manifest for it. Also reload the metadata for
  3221             // add-ons in the application directory when the application version
  3222             // has changed
  3223             if (aOldAddon.id in aManifests[installLocation.name] ||
  3224                 aOldAddon.updateDate != addonState.mtime ||
  3225                 (aUpdateCompatibility && installLocation.name == KEY_APP_GLOBAL)) {
  3226               changed = updateMetadata(installLocation, aOldAddon, addonState) ||
  3227                         changed;
  3229             else if (aOldAddon.descriptor != addonState.descriptor) {
  3230               changed = updateDescriptor(installLocation, aOldAddon, addonState) ||
  3231                         changed;
  3233             else {
  3234               changed = updateVisibilityAndCompatibility(installLocation,
  3235                                                          aOldAddon, addonState) ||
  3236                         changed;
  3238             if (aOldAddon.visible && aOldAddon._installLocation.name != KEY_APP_GLOBAL)
  3239               XPIProvider.allAppGlobal = false;
  3241           else {
  3242             changed = removeMetadata(aOldAddon) || changed;
  3247       // All the remaining add-ons in this install location must be new.
  3249       // Get the migration data for this install location.
  3250       let locMigrateData = {};
  3251       if (XPIDatabase.migrateData && installLocation.name in XPIDatabase.migrateData)
  3252         locMigrateData = XPIDatabase.migrateData[installLocation.name];
  3253       for (let id in addonStates) {
  3254         changed = addMetadata(installLocation, id, addonStates[id],
  3255                               (locMigrateData[id] || null)) || changed;
  3259     // The remaining locations that had add-ons installed in them no longer
  3260     // have any add-ons installed in them, or the locations no longer exist.
  3261     // The metadata for the add-ons that were in them must be removed from the
  3262     // database.
  3263     for (let location of knownLocations) {
  3264       let addons = XPIDatabase.getAddonsInLocation(location);
  3265       for (let aOldAddon of addons) {
  3266         changed = removeMetadata(aOldAddon) || changed;
  3270     // Cache the new install location states
  3271     this.installStates = this.getInstallLocationStates();
  3272     let cache = JSON.stringify(this.installStates);
  3273     Services.prefs.setCharPref(PREF_INSTALL_CACHE, cache);
  3274     this.persistBootstrappedAddons();
  3276     // Clear out any cached migration data.
  3277     XPIDatabase.migrateData = null;
  3279     return changed;
  3280   },
  3282   /**
  3283    * Imports the xpinstall permissions from preferences into the permissions
  3284    * manager for the user to change later.
  3285    */
  3286   importPermissions: function XPI_importPermissions() {
  3287     PermissionsUtils.importFromPrefs(PREF_XPI_PERMISSIONS_BRANCH,
  3288                                      XPI_PERMISSION);
  3289   },
  3291   /**
  3292    * Checks for any changes that have occurred since the last time the
  3293    * application was launched.
  3295    * @param  aAppChanged
  3296    *         A tri-state value. Undefined means the current profile was created
  3297    *         for this session, true means the profile already existed but was
  3298    *         last used with an application with a different version number,
  3299    *         false means that the profile was last used by this version of the
  3300    *         application.
  3301    * @param  aOldAppVersion
  3302    *         The version of the application last run with this profile or null
  3303    *         if it is a new profile or the version is unknown
  3304    * @param  aOldPlatformVersion
  3305    *         The version of the platform last run with this profile or null
  3306    *         if it is a new profile or the version is unknown
  3307    * @return true if a change requiring a restart was detected
  3308    */
  3309   checkForChanges: function XPI_checkForChanges(aAppChanged, aOldAppVersion,
  3310                                                 aOldPlatformVersion) {
  3311     logger.debug("checkForChanges");
  3313     // Keep track of whether and why we need to open and update the database at
  3314     // startup time.
  3315     let updateReasons = [];
  3316     if (aAppChanged) {
  3317       updateReasons.push("appChanged");
  3320     // Load the list of bootstrapped add-ons first so processFileChanges can
  3321     // modify it
  3322     try {
  3323       this.bootstrappedAddons = JSON.parse(Prefs.getCharPref(PREF_BOOTSTRAP_ADDONS,
  3324                                            "{}"));
  3325     } catch (e) {
  3326       logger.warn("Error parsing enabled bootstrapped extensions cache", e);
  3329     // First install any new add-ons into the locations, if there are any
  3330     // changes then we must update the database with the information in the
  3331     // install locations
  3332     let manifests = {};
  3333     let updated = this.processPendingFileChanges(manifests);
  3334     if (updated) {
  3335       updateReasons.push("pendingFileChanges");
  3338     // This will be true if the previous session made changes that affect the
  3339     // active state of add-ons but didn't commit them properly (normally due
  3340     // to the application crashing)
  3341     let hasPendingChanges = Prefs.getBoolPref(PREF_PENDING_OPERATIONS);
  3342     if (hasPendingChanges) {
  3343       updateReasons.push("hasPendingChanges");
  3346     // If the application has changed then check for new distribution add-ons
  3347     if (aAppChanged !== false &&
  3348         Prefs.getBoolPref(PREF_INSTALL_DISTRO_ADDONS, true))
  3350       updated = this.installDistributionAddons(manifests);
  3351       if (updated) {
  3352         updateReasons.push("installDistributionAddons");
  3356     // Telemetry probe added around getInstallLocationStates() to check perf
  3357     let telemetryCaptureTime = Date.now();
  3358     this.installStates = this.getInstallLocationStates();
  3359     let telemetry = Services.telemetry;
  3360     telemetry.getHistogramById("CHECK_ADDONS_MODIFIED_MS").add(Date.now() - telemetryCaptureTime);
  3362     // If the install directory state has changed then we must update the database
  3363     let cache = Prefs.getCharPref(PREF_INSTALL_CACHE, "[]");
  3364     // For a little while, gather telemetry on whether the deep comparison
  3365     // makes a difference
  3366     let newState = JSON.stringify(this.installStates);
  3367     if (cache != newState) {
  3368       logger.debug("Directory state JSON differs: cache " + cache + " state " + newState);
  3369       if (directoryStateDiffers(this.installStates, cache)) {
  3370         updateReasons.push("directoryState");
  3372       else {
  3373         AddonManagerPrivate.recordSimpleMeasure("XPIDB_startup_state_badCompare", 1);
  3377     // If the schema appears to have changed then we should update the database
  3378     if (DB_SCHEMA != Prefs.getIntPref(PREF_DB_SCHEMA, 0)) {
  3379       // If we don't have any add-ons, just update the pref, since we don't need to
  3380       // write the database
  3381       if (this.installStates.length == 0) {
  3382         logger.debug("Empty XPI database, setting schema version preference to " + DB_SCHEMA);
  3383         Services.prefs.setIntPref(PREF_DB_SCHEMA, DB_SCHEMA);
  3385       else {
  3386         updateReasons.push("schemaChanged");
  3390     // If the database doesn't exist and there are add-ons installed then we
  3391     // must update the database however if there are no add-ons then there is
  3392     // no need to update the database.
  3393     let dbFile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_DATABASE], true);
  3394     if (!dbFile.exists() && this.installStates.length > 0) {
  3395       updateReasons.push("needNewDatabase");
  3398     if (updateReasons.length == 0) {
  3399       let bootstrapDescriptors = [this.bootstrappedAddons[b].descriptor
  3400                                   for (b in this.bootstrappedAddons)];
  3402       this.installStates.forEach(function(aInstallLocationState) {
  3403         for (let id in aInstallLocationState.addons) {
  3404           let pos = bootstrapDescriptors.indexOf(aInstallLocationState.addons[id].descriptor);
  3405           if (pos != -1)
  3406             bootstrapDescriptors.splice(pos, 1);
  3408       });
  3410       if (bootstrapDescriptors.length > 0) {
  3411         logger.warn("Bootstrap state is invalid (missing add-ons: " + bootstrapDescriptors.toSource() + ")");
  3412         updateReasons.push("missingBootstrapAddon");
  3416     // Catch and log any errors during the main startup
  3417     try {
  3418       let extensionListChanged = false;
  3419       // If the database needs to be updated then open it and then update it
  3420       // from the filesystem
  3421       if (updateReasons.length > 0) {
  3422         AddonManagerPrivate.recordSimpleMeasure("XPIDB_startup_load_reasons", updateReasons);
  3423         XPIDatabase.syncLoadDB(false);
  3424         try {
  3425           extensionListChanged = this.processFileChanges(this.installStates, manifests,
  3426                                                          aAppChanged,
  3427                                                          aOldAppVersion,
  3428                                                          aOldPlatformVersion);
  3430         catch (e) {
  3431           logger.error("Failed to process extension changes at startup", e);
  3435       if (aAppChanged) {
  3436         // When upgrading the app and using a custom skin make sure it is still
  3437         // compatible otherwise switch back the default
  3438         if (this.currentSkin != this.defaultSkin) {
  3439           let oldSkin = XPIDatabase.getVisibleAddonForInternalName(this.currentSkin);
  3440           if (!oldSkin || isAddonDisabled(oldSkin))
  3441             this.enableDefaultTheme();
  3444         // When upgrading remove the old extensions cache to force older
  3445         // versions to rescan the entire list of extensions
  3446         try {
  3447           let oldCache = FileUtils.getFile(KEY_PROFILEDIR, [FILE_OLD_CACHE], true);
  3448           if (oldCache.exists())
  3449             oldCache.remove(true);
  3451         catch (e) {
  3452           logger.warn("Unable to remove old extension cache " + oldCache.path, e);
  3456       // If the application crashed before completing any pending operations then
  3457       // we should perform them now.
  3458       if (extensionListChanged || hasPendingChanges) {
  3459         logger.debug("Updating database with changes to installed add-ons");
  3460         XPIDatabase.updateActiveAddons();
  3461         Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS,
  3462                                    !XPIDatabase.writeAddonsList());
  3463         Services.prefs.setCharPref(PREF_BOOTSTRAP_ADDONS,
  3464                                    JSON.stringify(this.bootstrappedAddons));
  3465         return true;
  3468       logger.debug("No changes found");
  3470     catch (e) {
  3471       logger.error("Error during startup file checks", e);
  3474     // Check that the add-ons list still exists
  3475     let addonsList = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST],
  3476                                        true);
  3477     if (addonsList.exists() == (this.installStates.length == 0)) {
  3478       logger.debug("Add-ons list is invalid, rebuilding");
  3479       XPIDatabase.writeAddonsList();
  3482     return false;
  3483   },
  3485   /**
  3486    * Called to test whether this provider supports installing a particular
  3487    * mimetype.
  3489    * @param  aMimetype
  3490    *         The mimetype to check for
  3491    * @return true if the mimetype is application/x-xpinstall
  3492    */
  3493   supportsMimetype: function XPI_supportsMimetype(aMimetype) {
  3494     return aMimetype == "application/x-xpinstall";
  3495   },
  3497   /**
  3498    * Called to test whether installing XPI add-ons is enabled.
  3500    * @return true if installing is enabled
  3501    */
  3502   isInstallEnabled: function XPI_isInstallEnabled() {
  3503     // Default to enabled if the preference does not exist
  3504     return Prefs.getBoolPref(PREF_XPI_ENABLED, true);
  3505   },
  3507   /**
  3508    * Called to test whether installing XPI add-ons by direct URL requests is
  3509    * whitelisted.
  3511    * @return true if installing by direct requests is whitelisted
  3512    */
  3513   isDirectRequestWhitelisted: function XPI_isDirectRequestWhitelisted() {
  3514     // Default to whitelisted if the preference does not exist.
  3515     return Prefs.getBoolPref(PREF_XPI_DIRECT_WHITELISTED, true);
  3516   },
  3518   /**
  3519    * Called to test whether installing XPI add-ons from file referrers is
  3520    * whitelisted.
  3522    * @return true if installing from file referrers is whitelisted
  3523    */
  3524   isFileRequestWhitelisted: function XPI_isFileRequestWhitelisted() {
  3525     // Default to whitelisted if the preference does not exist.
  3526     return Prefs.getBoolPref(PREF_XPI_FILE_WHITELISTED, true);
  3527   },
  3529   /**
  3530    * Called to test whether installing XPI add-ons from a URI is allowed.
  3532    * @param  aUri
  3533    *         The URI being installed from
  3534    * @return true if installing is allowed
  3535    */
  3536   isInstallAllowed: function XPI_isInstallAllowed(aUri) {
  3537     if (!this.isInstallEnabled())
  3538       return false;
  3540     // Direct requests without a referrer are either whitelisted or blocked.
  3541     if (!aUri)
  3542       return this.isDirectRequestWhitelisted();
  3544     // Local referrers can be whitelisted.
  3545     if (this.isFileRequestWhitelisted() &&
  3546         (aUri.schemeIs("chrome") || aUri.schemeIs("file")))
  3547       return true;
  3549     this.importPermissions();
  3551     let permission = Services.perms.testPermission(aUri, XPI_PERMISSION);
  3552     if (permission == Ci.nsIPermissionManager.DENY_ACTION)
  3553       return false;
  3555     let requireWhitelist = Prefs.getBoolPref(PREF_XPI_WHITELIST_REQUIRED, true);
  3556     if (requireWhitelist && (permission != Ci.nsIPermissionManager.ALLOW_ACTION))
  3557       return false;
  3559     return true;
  3560   },
  3562   /**
  3563    * Called to get an AddonInstall to download and install an add-on from a URL.
  3565    * @param  aUrl
  3566    *         The URL to be installed
  3567    * @param  aHash
  3568    *         A hash for the install
  3569    * @param  aName
  3570    *         A name for the install
  3571    * @param  aIcons
  3572    *         Icon URLs for the install
  3573    * @param  aVersion
  3574    *         A version for the install
  3575    * @param  aLoadGroup
  3576    *         An nsILoadGroup to associate requests with
  3577    * @param  aCallback
  3578    *         A callback to pass the AddonInstall to
  3579    */
  3580   getInstallForURL: function XPI_getInstallForURL(aUrl, aHash, aName, aIcons,
  3581                                                   aVersion, aLoadGroup, aCallback) {
  3582     AddonInstall.createDownload(function getInstallForURL_createDownload(aInstall) {
  3583       aCallback(aInstall.wrapper);
  3584     }, aUrl, aHash, aName, aIcons, aVersion, aLoadGroup);
  3585   },
  3587   /**
  3588    * Called to get an AddonInstall to install an add-on from a local file.
  3590    * @param  aFile
  3591    *         The file to be installed
  3592    * @param  aCallback
  3593    *         A callback to pass the AddonInstall to
  3594    */
  3595   getInstallForFile: function XPI_getInstallForFile(aFile, aCallback) {
  3596     AddonInstall.createInstall(function getInstallForFile_createInstall(aInstall) {
  3597       if (aInstall)
  3598         aCallback(aInstall.wrapper);
  3599       else
  3600         aCallback(null);
  3601     }, aFile);
  3602   },
  3604   /**
  3605    * Removes an AddonInstall from the list of active installs.
  3607    * @param  install
  3608    *         The AddonInstall to remove
  3609    */
  3610   removeActiveInstall: function XPI_removeActiveInstall(aInstall) {
  3611     this.installs = this.installs.filter(function installFilter(i) i != aInstall);
  3612   },
  3614   /**
  3615    * Called to get an Addon with a particular ID.
  3617    * @param  aId
  3618    *         The ID of the add-on to retrieve
  3619    * @param  aCallback
  3620    *         A callback to pass the Addon to
  3621    */
  3622   getAddonByID: function XPI_getAddonByID(aId, aCallback) {
  3623     XPIDatabase.getVisibleAddonForID (aId, function getAddonByID_getVisibleAddonForID(aAddon) {
  3624       aCallback(createWrapper(aAddon));
  3625     });
  3626   },
  3628   /**
  3629    * Called to get Addons of a particular type.
  3631    * @param  aTypes
  3632    *         An array of types to fetch. Can be null to get all types.
  3633    * @param  aCallback
  3634    *         A callback to pass an array of Addons to
  3635    */
  3636   getAddonsByTypes: function XPI_getAddonsByTypes(aTypes, aCallback) {
  3637     XPIDatabase.getVisibleAddons(aTypes, function getAddonsByTypes_getVisibleAddons(aAddons) {
  3638       aCallback([createWrapper(a) for each (a in aAddons)]);
  3639     });
  3640   },
  3642   /**
  3643    * Obtain an Addon having the specified Sync GUID.
  3645    * @param  aGUID
  3646    *         String GUID of add-on to retrieve
  3647    * @param  aCallback
  3648    *         A callback to pass the Addon to. Receives null if not found.
  3649    */
  3650   getAddonBySyncGUID: function XPI_getAddonBySyncGUID(aGUID, aCallback) {
  3651     XPIDatabase.getAddonBySyncGUID(aGUID, function getAddonBySyncGUID_getAddonBySyncGUID(aAddon) {
  3652       aCallback(createWrapper(aAddon));
  3653     });
  3654   },
  3656   /**
  3657    * Called to get Addons that have pending operations.
  3659    * @param  aTypes
  3660    *         An array of types to fetch. Can be null to get all types
  3661    * @param  aCallback
  3662    *         A callback to pass an array of Addons to
  3663    */
  3664   getAddonsWithOperationsByTypes:
  3665   function XPI_getAddonsWithOperationsByTypes(aTypes, aCallback) {
  3666     XPIDatabase.getVisibleAddonsWithPendingOperations(aTypes,
  3667       function getAddonsWithOpsByTypes_getVisibleAddonsWithPendingOps(aAddons) {
  3668       let results = [createWrapper(a) for each (a in aAddons)];
  3669       XPIProvider.installs.forEach(function(aInstall) {
  3670         if (aInstall.state == AddonManager.STATE_INSTALLED &&
  3671             !(aInstall.addon.inDatabase))
  3672           results.push(createWrapper(aInstall.addon));
  3673       });
  3674       aCallback(results);
  3675     });
  3676   },
  3678   /**
  3679    * Called to get the current AddonInstalls, optionally limiting to a list of
  3680    * types.
  3682    * @param  aTypes
  3683    *         An array of types or null to get all types
  3684    * @param  aCallback
  3685    *         A callback to pass the array of AddonInstalls to
  3686    */
  3687   getInstallsByTypes: function XPI_getInstallsByTypes(aTypes, aCallback) {
  3688     let results = [];
  3689     this.installs.forEach(function(aInstall) {
  3690       if (!aTypes || aTypes.indexOf(aInstall.type) >= 0)
  3691         results.push(aInstall.wrapper);
  3692     });
  3693     aCallback(results);
  3694   },
  3696   /**
  3697    * Synchronously map a URI to the corresponding Addon ID.
  3699    * Mappable URIs are limited to in-application resources belonging to the
  3700    * add-on, such as Javascript compartments, XUL windows, XBL bindings, etc.
  3701    * but do not include URIs from meta data, such as the add-on homepage.
  3703    * @param  aURI
  3704    *         nsIURI to map or null
  3705    * @return string containing the Addon ID
  3706    * @see    AddonManager.mapURIToAddonID
  3707    * @see    amIAddonManager.mapURIToAddonID
  3708    */
  3709   mapURIToAddonID: function XPI_mapURIToAddonID(aURI) {
  3710     this._ensureURIMappings();
  3711     let resolved = this._resolveURIToFile(aURI);
  3712     if (!resolved) {
  3713       return null;
  3715     resolved = resolved.spec;
  3716     for (let [id, spec] in Iterator(this._uriMappings)) {
  3717       if (resolved.startsWith(spec)) {
  3718         return id;
  3721     return null;
  3722   },
  3724   /**
  3725    * Called when a new add-on has been enabled when only one add-on of that type
  3726    * can be enabled.
  3728    * @param  aId
  3729    *         The ID of the newly enabled add-on
  3730    * @param  aType
  3731    *         The type of the newly enabled add-on
  3732    * @param  aPendingRestart
  3733    *         true if the newly enabled add-on will only become enabled after a
  3734    *         restart
  3735    */
  3736   addonChanged: function XPI_addonChanged(aId, aType, aPendingRestart) {
  3737     // We only care about themes in this provider
  3738     if (aType != "theme")
  3739       return;
  3741     if (!aId) {
  3742       // Fallback to the default theme when no theme was enabled
  3743       this.enableDefaultTheme();
  3744       return;
  3747     // Look for the previously enabled theme and find the internalName of the
  3748     // currently selected theme
  3749     let previousTheme = null;
  3750     let newSkin = this.defaultSkin;
  3751     let addons = XPIDatabase.getAddonsByType("theme");
  3752     addons.forEach(function(aTheme) {
  3753       if (!aTheme.visible)
  3754         return;
  3755       if (aTheme.id == aId)
  3756         newSkin = aTheme.internalName;
  3757       else if (aTheme.userDisabled == false && !aTheme.pendingUninstall)
  3758         previousTheme = aTheme;
  3759     }, this);
  3761     if (aPendingRestart) {
  3762       Services.prefs.setBoolPref(PREF_DSS_SWITCHPENDING, true);
  3763       Services.prefs.setCharPref(PREF_DSS_SKIN_TO_SELECT, newSkin);
  3765     else if (newSkin == this.currentSkin) {
  3766       try {
  3767         Services.prefs.clearUserPref(PREF_DSS_SWITCHPENDING);
  3769       catch (e) { }
  3770       try {
  3771         Services.prefs.clearUserPref(PREF_DSS_SKIN_TO_SELECT);
  3773       catch (e) { }
  3775     else {
  3776       Services.prefs.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN, newSkin);
  3777       this.currentSkin = newSkin;
  3779     this.selectedSkin = newSkin;
  3781     // Flush the preferences to disk so they don't get out of sync with the
  3782     // database
  3783     Services.prefs.savePrefFile(null);
  3785     // Mark the previous theme as disabled. This won't cause recursion since
  3786     // only enabled calls notifyAddonChanged.
  3787     if (previousTheme)
  3788       this.updateAddonDisabledState(previousTheme, true);
  3789   },
  3791   /**
  3792    * Update the appDisabled property for all add-ons.
  3793    */
  3794   updateAddonAppDisabledStates: function XPI_updateAddonAppDisabledStates() {
  3795     let addons = XPIDatabase.getAddons();
  3796     addons.forEach(function(aAddon) {
  3797       this.updateAddonDisabledState(aAddon);
  3798     }, this);
  3799   },
  3801   /**
  3802    * Update the repositoryAddon property for all add-ons.
  3804    * @param  aCallback
  3805    *         Function to call when operation is complete.
  3806    */
  3807   updateAddonRepositoryData: function XPI_updateAddonRepositoryData(aCallback) {
  3808     let self = this;
  3809     XPIDatabase.getVisibleAddons(null, function UARD_getVisibleAddonsCallback(aAddons) {
  3810       let pending = aAddons.length;
  3811       logger.debug("updateAddonRepositoryData found " + pending + " visible add-ons");
  3812       if (pending == 0) {
  3813         aCallback();
  3814         return;
  3817       function notifyComplete() {
  3818         if (--pending == 0)
  3819           aCallback();
  3822       for (let addon of aAddons) {
  3823         AddonRepository.getCachedAddonByID(addon.id,
  3824                                            function UARD_getCachedAddonCallback(aRepoAddon) {
  3825           if (aRepoAddon) {
  3826             logger.debug("updateAddonRepositoryData got info for " + addon.id);
  3827             addon._repositoryAddon = aRepoAddon;
  3828             addon.compatibilityOverrides = aRepoAddon.compatibilityOverrides;
  3829             self.updateAddonDisabledState(addon);
  3832           notifyComplete();
  3833         });
  3834       };
  3835     });
  3836   },
  3838   /**
  3839    * When the previously selected theme is removed this method will be called
  3840    * to enable the default theme.
  3841    */
  3842   enableDefaultTheme: function XPI_enableDefaultTheme() {
  3843     logger.debug("Activating default theme");
  3844     let addon = XPIDatabase.getVisibleAddonForInternalName(this.defaultSkin);
  3845     if (addon) {
  3846       if (addon.userDisabled) {
  3847         this.updateAddonDisabledState(addon, false);
  3849       else if (!this.extensionsActive) {
  3850         // During startup we may end up trying to enable the default theme when
  3851         // the database thinks it is already enabled (see f.e. bug 638847). In
  3852         // this case just force the theme preferences to be correct
  3853         Services.prefs.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN,
  3854                                    addon.internalName);
  3855         this.currentSkin = this.selectedSkin = addon.internalName;
  3856         Prefs.clearUserPref(PREF_DSS_SKIN_TO_SELECT);
  3857         Prefs.clearUserPref(PREF_DSS_SWITCHPENDING);
  3859       else {
  3860         logger.warn("Attempting to activate an already active default theme");
  3863     else {
  3864       logger.warn("Unable to activate the default theme");
  3866   },
  3868   onDebugConnectionChange: function(aEvent, aWhat, aConnection) {
  3869     if (aWhat != "opened")
  3870       return;
  3872     for (let id of Object.keys(this.bootstrapScopes)) {
  3873       aConnection.setAddonOptions(id, { global: this.bootstrapScopes[id] });
  3875   },
  3877   /**
  3878    * Notified when a preference we're interested in has changed.
  3880    * @see nsIObserver
  3881    */
  3882   observe: function XPI_observe(aSubject, aTopic, aData) {
  3883     if (aTopic == NOTIFICATION_FLUSH_PERMISSIONS) {
  3884       if (!aData || aData == XPI_PERMISSION) {
  3885         this.importPermissions();
  3887       return;
  3890     if (aTopic == "nsPref:changed") {
  3891       switch (aData) {
  3892       case PREF_EM_MIN_COMPAT_APP_VERSION:
  3893       case PREF_EM_MIN_COMPAT_PLATFORM_VERSION:
  3894         this.minCompatibleAppVersion = Prefs.getCharPref(PREF_EM_MIN_COMPAT_APP_VERSION,
  3895                                                          null);
  3896         this.minCompatiblePlatformVersion = Prefs.getCharPref(PREF_EM_MIN_COMPAT_PLATFORM_VERSION,
  3897                                                               null);
  3898         this.updateAddonAppDisabledStates();
  3899         break;
  3902   },
  3904   /**
  3905    * Tests whether enabling an add-on will require a restart.
  3907    * @param  aAddon
  3908    *         The add-on to test
  3909    * @return true if the operation requires a restart
  3910    */
  3911   enableRequiresRestart: function XPI_enableRequiresRestart(aAddon) {
  3912     // If the platform couldn't have activated extensions then we can make
  3913     // changes without any restart.
  3914     if (!this.extensionsActive)
  3915       return false;
  3917     // If the application is in safe mode then any change can be made without
  3918     // restarting
  3919     if (Services.appinfo.inSafeMode)
  3920       return false;
  3922     // Anything that is active is already enabled
  3923     if (aAddon.active)
  3924       return false;
  3926     if (aAddon.type == "theme") {
  3927       // If dynamic theme switching is enabled then switching themes does not
  3928       // require a restart
  3929       if (Prefs.getBoolPref(PREF_EM_DSS_ENABLED))
  3930         return false;
  3932       // If the theme is already the theme in use then no restart is necessary.
  3933       // This covers the case where the default theme is in use but a
  3934       // lightweight theme is considered active.
  3935       return aAddon.internalName != this.currentSkin;
  3938     return !aAddon.bootstrap;
  3939   },
  3941   /**
  3942    * Tests whether disabling an add-on will require a restart.
  3944    * @param  aAddon
  3945    *         The add-on to test
  3946    * @return true if the operation requires a restart
  3947    */
  3948   disableRequiresRestart: function XPI_disableRequiresRestart(aAddon) {
  3949     // If the platform couldn't have activated up extensions then we can make
  3950     // changes without any restart.
  3951     if (!this.extensionsActive)
  3952       return false;
  3954     // If the application is in safe mode then any change can be made without
  3955     // restarting
  3956     if (Services.appinfo.inSafeMode)
  3957       return false;
  3959     // Anything that isn't active is already disabled
  3960     if (!aAddon.active)
  3961       return false;
  3963     if (aAddon.type == "theme") {
  3964       // If dynamic theme switching is enabled then switching themes does not
  3965       // require a restart
  3966       if (Prefs.getBoolPref(PREF_EM_DSS_ENABLED))
  3967         return false;
  3969       // Non-default themes always require a restart to disable since it will
  3970       // be switching from one theme to another or to the default theme and a
  3971       // lightweight theme.
  3972       if (aAddon.internalName != this.defaultSkin)
  3973         return true;
  3975       // The default theme requires a restart to disable if we are in the
  3976       // process of switching to a different theme. Note that this makes the
  3977       // disabled flag of operationsRequiringRestart incorrect for the default
  3978       // theme (it will be false most of the time). Bug 520124 would be required
  3979       // to fix it. For the UI this isn't a problem since we never try to
  3980       // disable or uninstall the default theme.
  3981       return this.selectedSkin != this.currentSkin;
  3984     return !aAddon.bootstrap;
  3985   },
  3987   /**
  3988    * Tests whether installing an add-on will require a restart.
  3990    * @param  aAddon
  3991    *         The add-on to test
  3992    * @return true if the operation requires a restart
  3993    */
  3994   installRequiresRestart: function XPI_installRequiresRestart(aAddon) {
  3995     // If the platform couldn't have activated up extensions then we can make
  3996     // changes without any restart.
  3997     if (!this.extensionsActive)
  3998       return false;
  4000     // If the application is in safe mode then any change can be made without
  4001     // restarting
  4002     if (Services.appinfo.inSafeMode)
  4003       return false;
  4005     // Add-ons that are already installed don't require a restart to install.
  4006     // This wouldn't normally be called for an already installed add-on (except
  4007     // for forming the operationsRequiringRestart flags) so is really here as
  4008     // a safety measure.
  4009     if (aAddon.inDatabase)
  4010       return false;
  4012     // If we have an AddonInstall for this add-on then we can see if there is
  4013     // an existing installed add-on with the same ID
  4014     if ("_install" in aAddon && aAddon._install) {
  4015       // If there is an existing installed add-on and uninstalling it would
  4016       // require a restart then installing the update will also require a
  4017       // restart
  4018       let existingAddon = aAddon._install.existingAddon;
  4019       if (existingAddon && this.uninstallRequiresRestart(existingAddon))
  4020         return true;
  4023     // If the add-on is not going to be active after installation then it
  4024     // doesn't require a restart to install.
  4025     if (isAddonDisabled(aAddon))
  4026       return false;
  4028     // Themes will require a restart (even if dynamic switching is enabled due
  4029     // to some caching issues) and non-bootstrapped add-ons will require a
  4030     // restart
  4031     return aAddon.type == "theme" || !aAddon.bootstrap;
  4032   },
  4034   /**
  4035    * Tests whether uninstalling an add-on will require a restart.
  4037    * @param  aAddon
  4038    *         The add-on to test
  4039    * @return true if the operation requires a restart
  4040    */
  4041   uninstallRequiresRestart: function XPI_uninstallRequiresRestart(aAddon) {
  4042     // If the platform couldn't have activated up extensions then we can make
  4043     // changes without any restart.
  4044     if (!this.extensionsActive)
  4045       return false;
  4047     // If the application is in safe mode then any change can be made without
  4048     // restarting
  4049     if (Services.appinfo.inSafeMode)
  4050       return false;
  4052     // If the add-on can be disabled without a restart then it can also be
  4053     // uninstalled without a restart
  4054     return this.disableRequiresRestart(aAddon);
  4055   },
  4057   /**
  4058    * Loads a bootstrapped add-on's bootstrap.js into a sandbox and the reason
  4059    * values as constants in the scope. This will also add information about the
  4060    * add-on to the bootstrappedAddons dictionary and notify the crash reporter
  4061    * that new add-ons have been loaded.
  4063    * @param  aId
  4064    *         The add-on's ID
  4065    * @param  aFile
  4066    *         The nsIFile for the add-on
  4067    * @param  aVersion
  4068    *         The add-on's version
  4069    * @param  aType
  4070    *         The type for the add-on
  4071    * @return a JavaScript scope
  4072    */
  4073   loadBootstrapScope: function XPI_loadBootstrapScope(aId, aFile, aVersion, aType) {
  4074     // Mark the add-on as active for the crash reporter before loading
  4075     this.bootstrappedAddons[aId] = {
  4076       version: aVersion,
  4077       type: aType,
  4078       descriptor: aFile.persistentDescriptor
  4079     };
  4080     this.persistBootstrappedAddons();
  4081     this.addAddonsToCrashReporter();
  4083     // Locales only contain chrome and can't have bootstrap scripts
  4084     if (aType == "locale") {
  4085       this.bootstrapScopes[aId] = null;
  4086       return;
  4089     logger.debug("Loading bootstrap scope from " + aFile.path);
  4091     let principal = Cc["@mozilla.org/systemprincipal;1"].
  4092                     createInstance(Ci.nsIPrincipal);
  4094     if (!aFile.exists()) {
  4095       this.bootstrapScopes[aId] =
  4096         new Cu.Sandbox(principal, { sandboxName: aFile.path,
  4097                                     wantGlobalProperties: ["indexedDB"],
  4098                                     metadata: { addonID: aId } });
  4099       logger.error("Attempted to load bootstrap scope from missing directory " + aFile.path);
  4100       return;
  4103     let uri = getURIForResourceInFile(aFile, "bootstrap.js").spec;
  4104     if (aType == "dictionary")
  4105       uri = "resource://gre/modules/addons/SpellCheckDictionaryBootstrap.js"
  4107     this.bootstrapScopes[aId] =
  4108       new Cu.Sandbox(principal, { sandboxName: uri,
  4109                                   wantGlobalProperties: ["indexedDB"],
  4110                                   metadata: { addonID: aId, URI: uri } });
  4112     let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
  4113                  createInstance(Ci.mozIJSSubScriptLoader);
  4115     // Add a mapping for XPIProvider.mapURIToAddonID
  4116     this._addURIMapping(aId, aFile);
  4118     try {
  4119       // Copy the reason values from the global object into the bootstrap scope.
  4120       for (let name in BOOTSTRAP_REASONS)
  4121         this.bootstrapScopes[aId][name] = BOOTSTRAP_REASONS[name];
  4123       // Add other stuff that extensions want.
  4124       const features = [ "Worker", "ChromeWorker" ];
  4126       for (let feature of features)
  4127         this.bootstrapScopes[aId][feature] = gGlobalScope[feature];
  4129       // As we don't want our caller to control the JS version used for the
  4130       // bootstrap file, we run loadSubScript within the context of the
  4131       // sandbox with the latest JS version set explicitly.
  4132       this.bootstrapScopes[aId].__SCRIPT_URI_SPEC__ = uri;
  4133       Components.utils.evalInSandbox(
  4134         "Components.classes['@mozilla.org/moz/jssubscript-loader;1'] \
  4135                    .createInstance(Components.interfaces.mozIJSSubScriptLoader) \
  4136                    .loadSubScript(__SCRIPT_URI_SPEC__);", this.bootstrapScopes[aId], "ECMAv5");
  4138     catch (e) {
  4139       logger.warn("Error loading bootstrap.js for " + aId, e);
  4142     try {
  4143       BrowserToolboxProcess.setAddonOptions(aId, { global: this.bootstrapScopes[aId] });
  4145     catch (e) {
  4146       // BrowserToolboxProcess is not available in all applications
  4148   },
  4150   /**
  4151    * Unloads a bootstrap scope by dropping all references to it and then
  4152    * updating the list of active add-ons with the crash reporter.
  4154    * @param  aId
  4155    *         The add-on's ID
  4156    */
  4157   unloadBootstrapScope: function XPI_unloadBootstrapScope(aId) {
  4158     delete this.bootstrapScopes[aId];
  4159     delete this.bootstrappedAddons[aId];
  4160     this.persistBootstrappedAddons();
  4161     this.addAddonsToCrashReporter();
  4163     try {
  4164       BrowserToolboxProcess.setAddonOptions(aId, { global: null });
  4166     catch (e) {
  4167       // BrowserToolboxProcess is not available in all applications
  4169   },
  4171   /**
  4172    * Calls a bootstrap method for an add-on.
  4174    * @param  aId
  4175    *         The ID of the add-on
  4176    * @param  aVersion
  4177    *         The version of the add-on
  4178    * @param  aType
  4179    *         The type for the add-on
  4180    * @param  aFile
  4181    *         The nsIFile for the add-on
  4182    * @param  aMethod
  4183    *         The name of the bootstrap method to call
  4184    * @param  aReason
  4185    *         The reason flag to pass to the bootstrap's startup method
  4186    * @param  aExtraParams
  4187    *         An object of additional key/value pairs to pass to the method in
  4188    *         the params argument
  4189    */
  4190   callBootstrapMethod: function XPI_callBootstrapMethod(aId, aVersion, aType, aFile,
  4191                                                         aMethod, aReason, aExtraParams) {
  4192     // Never call any bootstrap methods in safe mode
  4193     if (Services.appinfo.inSafeMode)
  4194       return;
  4196     let timeStart = new Date();
  4197     if (aMethod == "startup") {
  4198       logger.debug("Registering manifest for " + aFile.path);
  4199       Components.manager.addBootstrappedManifestLocation(aFile);
  4202     try {
  4203       // Load the scope if it hasn't already been loaded
  4204       if (!(aId in this.bootstrapScopes))
  4205         this.loadBootstrapScope(aId, aFile, aVersion, aType);
  4207       // Nothing to call for locales
  4208       if (aType == "locale")
  4209         return;
  4211       if (!(aMethod in this.bootstrapScopes[aId])) {
  4212         logger.warn("Add-on " + aId + " is missing bootstrap method " + aMethod);
  4213         return;
  4216       let params = {
  4217         id: aId,
  4218         version: aVersion,
  4219         installPath: aFile.clone(),
  4220         resourceURI: getURIForResourceInFile(aFile, "")
  4221       };
  4223       if (aExtraParams) {
  4224         for (let key in aExtraParams) {
  4225           params[key] = aExtraParams[key];
  4229       logger.debug("Calling bootstrap method " + aMethod + " on " + aId + " version " +
  4230           aVersion);
  4231       try {
  4232         this.bootstrapScopes[aId][aMethod](params, aReason);
  4234       catch (e) {
  4235         logger.warn("Exception running bootstrap method " + aMethod + " on " + aId, e);
  4238     finally {
  4239       if (aMethod == "shutdown" && aReason != BOOTSTRAP_REASONS.APP_SHUTDOWN) {
  4240         logger.debug("Removing manifest for " + aFile.path);
  4241         Components.manager.removeBootstrappedManifestLocation(aFile);
  4243       this.setTelemetry(aId, aMethod + "_MS", new Date() - timeStart);
  4245   },
  4247   /**
  4248    * Updates the disabled state for an add-on. Its appDisabled property will be
  4249    * calculated and if the add-on is changed the database will be saved and
  4250    * appropriate notifications will be sent out to the registered AddonListeners.
  4252    * @param  aAddon
  4253    *         The DBAddonInternal to update
  4254    * @param  aUserDisabled
  4255    *         Value for the userDisabled property. If undefined the value will
  4256    *         not change
  4257    * @param  aSoftDisabled
  4258    *         Value for the softDisabled property. If undefined the value will
  4259    *         not change. If true this will force userDisabled to be true
  4260    * @throws if addon is not a DBAddonInternal
  4261    */
  4262   updateAddonDisabledState: function XPI_updateAddonDisabledState(aAddon,
  4263                                                                   aUserDisabled,
  4264                                                                   aSoftDisabled) {
  4265     if (!(aAddon.inDatabase))
  4266       throw new Error("Can only update addon states for installed addons.");
  4267     if (aUserDisabled !== undefined && aSoftDisabled !== undefined) {
  4268       throw new Error("Cannot change userDisabled and softDisabled at the " +
  4269                       "same time");
  4272     if (aUserDisabled === undefined) {
  4273       aUserDisabled = aAddon.userDisabled;
  4275     else if (!aUserDisabled) {
  4276       // If enabling the add-on then remove softDisabled
  4277       aSoftDisabled = false;
  4280     // If not changing softDisabled or the add-on is already userDisabled then
  4281     // use the existing value for softDisabled
  4282     if (aSoftDisabled === undefined || aUserDisabled)
  4283       aSoftDisabled = aAddon.softDisabled;
  4285     let appDisabled = !isUsableAddon(aAddon);
  4286     // No change means nothing to do here
  4287     if (aAddon.userDisabled == aUserDisabled &&
  4288         aAddon.appDisabled == appDisabled &&
  4289         aAddon.softDisabled == aSoftDisabled)
  4290       return;
  4292     let wasDisabled = isAddonDisabled(aAddon);
  4293     let isDisabled = aUserDisabled || aSoftDisabled || appDisabled;
  4295     // If appDisabled changes but the result of isAddonDisabled() doesn't,
  4296     // no onDisabling/onEnabling is sent - so send a onPropertyChanged.
  4297     let appDisabledChanged = aAddon.appDisabled != appDisabled;
  4299     // Update the properties in the database.
  4300     // We never persist this for experiments because the disabled flags
  4301     // are controlled by the Experiments Manager.
  4302     if (aAddon.type != "experiment") {
  4303       XPIDatabase.setAddonProperties(aAddon, {
  4304         userDisabled: aUserDisabled,
  4305         appDisabled: appDisabled,
  4306         softDisabled: aSoftDisabled
  4307       });
  4310     if (appDisabledChanged) {
  4311       AddonManagerPrivate.callAddonListeners("onPropertyChanged",
  4312                                             aAddon,
  4313                                             ["appDisabled"]);
  4316     // If the add-on is not visible or the add-on is not changing state then
  4317     // there is no need to do anything else
  4318     if (!aAddon.visible || (wasDisabled == isDisabled))
  4319       return;
  4321     // Flag that active states in the database need to be updated on shutdown
  4322     Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
  4324     let wrapper = createWrapper(aAddon);
  4325     // Have we just gone back to the current state?
  4326     if (isDisabled != aAddon.active) {
  4327       AddonManagerPrivate.callAddonListeners("onOperationCancelled", wrapper);
  4329     else {
  4330       if (isDisabled) {
  4331         var needsRestart = this.disableRequiresRestart(aAddon);
  4332         AddonManagerPrivate.callAddonListeners("onDisabling", wrapper,
  4333                                                needsRestart);
  4335       else {
  4336         needsRestart = this.enableRequiresRestart(aAddon);
  4337         AddonManagerPrivate.callAddonListeners("onEnabling", wrapper,
  4338                                                needsRestart);
  4341       if (!needsRestart) {
  4342         XPIDatabase.updateAddonActive(aAddon, !isDisabled);
  4343         if (isDisabled) {
  4344           if (aAddon.bootstrap) {
  4345             let file = aAddon._installLocation.getLocationForID(aAddon.id);
  4346             this.callBootstrapMethod(aAddon.id, aAddon.version, aAddon.type, file, "shutdown",
  4347                                      BOOTSTRAP_REASONS.ADDON_DISABLE);
  4348             this.unloadBootstrapScope(aAddon.id);
  4350           AddonManagerPrivate.callAddonListeners("onDisabled", wrapper);
  4352         else {
  4353           if (aAddon.bootstrap) {
  4354             let file = aAddon._installLocation.getLocationForID(aAddon.id);
  4355             this.callBootstrapMethod(aAddon.id, aAddon.version, aAddon.type, file, "startup",
  4356                                      BOOTSTRAP_REASONS.ADDON_ENABLE);
  4358           AddonManagerPrivate.callAddonListeners("onEnabled", wrapper);
  4363     // Notify any other providers that a new theme has been enabled
  4364     if (aAddon.type == "theme" && !isDisabled)
  4365       AddonManagerPrivate.notifyAddonChanged(aAddon.id, aAddon.type, needsRestart);
  4366   },
  4368   /**
  4369    * Uninstalls an add-on, immediately if possible or marks it as pending
  4370    * uninstall if not.
  4372    * @param  aAddon
  4373    *         The DBAddonInternal to uninstall
  4374    * @throws if the addon cannot be uninstalled because it is in an install
  4375    *         location that does not allow it
  4376    */
  4377   uninstallAddon: function XPI_uninstallAddon(aAddon) {
  4378     if (!(aAddon.inDatabase))
  4379       throw new Error("Can only uninstall installed addons.");
  4381     if (aAddon._installLocation.locked)
  4382       throw new Error("Cannot uninstall addons from locked install locations");
  4384     if ("_hasResourceCache" in aAddon)
  4385       aAddon._hasResourceCache = new Map();
  4387     if (aAddon._updateCheck) {
  4388       logger.debug("Cancel in-progress update check for " + aAddon.id);
  4389       aAddon._updateCheck.cancel();
  4392     // Inactive add-ons don't require a restart to uninstall
  4393     let requiresRestart = this.uninstallRequiresRestart(aAddon);
  4395     if (requiresRestart) {
  4396       // We create an empty directory in the staging directory to indicate that
  4397       // an uninstall is necessary on next startup.
  4398       let stage = aAddon._installLocation.getStagingDir();
  4399       stage.append(aAddon.id);
  4400       if (!stage.exists())
  4401         stage.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
  4403       XPIDatabase.setAddonProperties(aAddon, {
  4404         pendingUninstall: true
  4405       });
  4406       Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
  4409     // If the add-on is not visible then there is no need to notify listeners.
  4410     if (!aAddon.visible)
  4411       return;
  4413     let wrapper = createWrapper(aAddon);
  4414     AddonManagerPrivate.callAddonListeners("onUninstalling", wrapper,
  4415                                            requiresRestart);
  4417     // Reveal the highest priority add-on with the same ID
  4418     function revealAddon(aAddon) {
  4419       XPIDatabase.makeAddonVisible(aAddon);
  4421       let wrappedAddon = createWrapper(aAddon);
  4422       AddonManagerPrivate.callAddonListeners("onInstalling", wrappedAddon, false);
  4424       if (!isAddonDisabled(aAddon) && !XPIProvider.enableRequiresRestart(aAddon)) {
  4425         XPIDatabase.updateAddonActive(aAddon, true);
  4428       if (aAddon.bootstrap) {
  4429         let file = aAddon._installLocation.getLocationForID(aAddon.id);
  4430         XPIProvider.callBootstrapMethod(aAddon.id, aAddon.version, aAddon.type, file,
  4431                                         "install", BOOTSTRAP_REASONS.ADDON_INSTALL);
  4433         if (aAddon.active) {
  4434           XPIProvider.callBootstrapMethod(aAddon.id, aAddon.version, aAddon.type, file,
  4435                                           "startup", BOOTSTRAP_REASONS.ADDON_INSTALL);
  4437         else {
  4438           XPIProvider.unloadBootstrapScope(aAddon.id);
  4442       // We always send onInstalled even if a restart is required to enable
  4443       // the revealed add-on
  4444       AddonManagerPrivate.callAddonListeners("onInstalled", wrappedAddon);
  4447     function checkInstallLocation(aPos) {
  4448       if (aPos < 0)
  4449         return;
  4451       let location = XPIProvider.installLocations[aPos];
  4452       XPIDatabase.getAddonInLocation(aAddon.id, location.name,
  4453         function checkInstallLocation_getAddonInLocation(aNewAddon) {
  4454         if (aNewAddon)
  4455           revealAddon(aNewAddon);
  4456         else
  4457           checkInstallLocation(aPos - 1);
  4458       })
  4461     if (!requiresRestart) {
  4462       if (aAddon.bootstrap) {
  4463         let file = aAddon._installLocation.getLocationForID(aAddon.id);
  4464         if (aAddon.active) {
  4465           this.callBootstrapMethod(aAddon.id, aAddon.version, aAddon.type, file,
  4466                                    "shutdown",
  4467                                    BOOTSTRAP_REASONS.ADDON_UNINSTALL);
  4470         this.callBootstrapMethod(aAddon.id, aAddon.version, aAddon.type, file,
  4471                                  "uninstall",
  4472                                  BOOTSTRAP_REASONS.ADDON_UNINSTALL);
  4473         this.unloadBootstrapScope(aAddon.id);
  4474         flushStartupCache();
  4476       aAddon._installLocation.uninstallAddon(aAddon.id);
  4477       XPIDatabase.removeAddonMetadata(aAddon);
  4478       AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper);
  4480       checkInstallLocation(this.installLocations.length - 1);
  4483     // Notify any other providers that a new theme has been enabled
  4484     if (aAddon.type == "theme" && aAddon.active)
  4485       AddonManagerPrivate.notifyAddonChanged(null, aAddon.type, requiresRestart);
  4486   },
  4488   /**
  4489    * Cancels the pending uninstall of an add-on.
  4491    * @param  aAddon
  4492    *         The DBAddonInternal to cancel uninstall for
  4493    */
  4494   cancelUninstallAddon: function XPI_cancelUninstallAddon(aAddon) {
  4495     if (!(aAddon.inDatabase))
  4496       throw new Error("Can only cancel uninstall for installed addons.");
  4498     aAddon._installLocation.cleanStagingDir([aAddon.id]);
  4500     XPIDatabase.setAddonProperties(aAddon, {
  4501       pendingUninstall: false
  4502     });
  4504     if (!aAddon.visible)
  4505       return;
  4507     Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
  4509     // TODO hide hidden add-ons (bug 557710)
  4510     let wrapper = createWrapper(aAddon);
  4511     AddonManagerPrivate.callAddonListeners("onOperationCancelled", wrapper);
  4513     // Notify any other providers that this theme is now enabled again.
  4514     if (aAddon.type == "theme" && aAddon.active)
  4515       AddonManagerPrivate.notifyAddonChanged(aAddon.id, aAddon.type, false);
  4517 };
  4519 function getHashStringForCrypto(aCrypto) {
  4520   // return the two-digit hexadecimal code for a byte
  4521   function toHexString(charCode)
  4522     ("0" + charCode.toString(16)).slice(-2);
  4524   // convert the binary hash data to a hex string.
  4525   let binary = aCrypto.finish(false);
  4526   return [toHexString(binary.charCodeAt(i)) for (i in binary)].join("").toLowerCase()
  4529 /**
  4530  * Instantiates an AddonInstall.
  4532  * @param  aInstallLocation
  4533  *         The install location the add-on will be installed into
  4534  * @param  aUrl
  4535  *         The nsIURL to get the add-on from. If this is an nsIFileURL then
  4536  *         the add-on will not need to be downloaded
  4537  * @param  aHash
  4538  *         An optional hash for the add-on
  4539  * @param  aReleaseNotesURI
  4540  *         An optional nsIURI of release notes for the add-on
  4541  * @param  aExistingAddon
  4542  *         The add-on this install will update if known
  4543  * @param  aLoadGroup
  4544  *         The nsILoadGroup to associate any requests with
  4545  * @throws if the url is the url of a local file and the hash does not match
  4546  *         or the add-on does not contain an valid install manifest
  4547  */
  4548 function AddonInstall(aInstallLocation, aUrl, aHash, aReleaseNotesURI,
  4549                       aExistingAddon, aLoadGroup) {
  4550   this.wrapper = new AddonInstallWrapper(this);
  4551   this.installLocation = aInstallLocation;
  4552   this.sourceURI = aUrl;
  4553   this.releaseNotesURI = aReleaseNotesURI;
  4554   if (aHash) {
  4555     let hashSplit = aHash.toLowerCase().split(":");
  4556     this.originalHash = {
  4557       algorithm: hashSplit[0],
  4558       data: hashSplit[1]
  4559     };
  4561   this.hash = this.originalHash;
  4562   this.loadGroup = aLoadGroup;
  4563   this.listeners = [];
  4564   this.icons = {};
  4565   this.existingAddon = aExistingAddon;
  4566   this.error = 0;
  4567   if (aLoadGroup)
  4568     this.window = aLoadGroup.notificationCallbacks
  4569                             .getInterface(Ci.nsIDOMWindow);
  4570   else
  4571     this.window = null;
  4573   // Giving each instance of AddonInstall a reference to the logger.
  4574   this.logger = logger;
  4577 AddonInstall.prototype = {
  4578   installLocation: null,
  4579   wrapper: null,
  4580   stream: null,
  4581   crypto: null,
  4582   originalHash: null,
  4583   hash: null,
  4584   loadGroup: null,
  4585   badCertHandler: null,
  4586   listeners: null,
  4587   restartDownload: false,
  4589   name: null,
  4590   type: null,
  4591   version: null,
  4592   icons: null,
  4593   releaseNotesURI: null,
  4594   sourceURI: null,
  4595   file: null,
  4596   ownsTempFile: false,
  4597   certificate: null,
  4598   certName: null,
  4600   linkedInstalls: null,
  4601   existingAddon: null,
  4602   addon: null,
  4604   state: null,
  4605   error: null,
  4606   progress: null,
  4607   maxProgress: null,
  4609   /**
  4610    * Initialises this install to be a staged install waiting to be applied
  4612    * @param  aManifest
  4613    *         The cached manifest for the staged install
  4614    */
  4615   initStagedInstall: function AI_initStagedInstall(aManifest) {
  4616     this.name = aManifest.name;
  4617     this.type = aManifest.type;
  4618     this.version = aManifest.version;
  4619     this.icons = aManifest.icons;
  4620     this.releaseNotesURI = aManifest.releaseNotesURI ?
  4621                            NetUtil.newURI(aManifest.releaseNotesURI) :
  4622                            null
  4623     this.sourceURI = aManifest.sourceURI ?
  4624                      NetUtil.newURI(aManifest.sourceURI) :
  4625                      null;
  4626     this.file = null;
  4627     this.addon = aManifest;
  4629     this.state = AddonManager.STATE_INSTALLED;
  4631     XPIProvider.installs.push(this);
  4632   },
  4634   /**
  4635    * Initialises this install to be an install from a local file.
  4637    * @param  aCallback
  4638    *         The callback to pass the initialised AddonInstall to
  4639    */
  4640   initLocalInstall: function AI_initLocalInstall(aCallback) {
  4641     aCallback = makeSafe(aCallback);
  4642     this.file = this.sourceURI.QueryInterface(Ci.nsIFileURL).file;
  4644     if (!this.file.exists()) {
  4645       logger.warn("XPI file " + this.file.path + " does not exist");
  4646       this.state = AddonManager.STATE_DOWNLOAD_FAILED;
  4647       this.error = AddonManager.ERROR_NETWORK_FAILURE;
  4648       aCallback(this);
  4649       return;
  4652     this.state = AddonManager.STATE_DOWNLOADED;
  4653     this.progress = this.file.fileSize;
  4654     this.maxProgress = this.file.fileSize;
  4656     if (this.hash) {
  4657       let crypto = Cc["@mozilla.org/security/hash;1"].
  4658                    createInstance(Ci.nsICryptoHash);
  4659       try {
  4660         crypto.initWithString(this.hash.algorithm);
  4662       catch (e) {
  4663         logger.warn("Unknown hash algorithm '" + this.hash.algorithm + "' for addon " + this.sourceURI.spec, e);
  4664         this.state = AddonManager.STATE_DOWNLOAD_FAILED;
  4665         this.error = AddonManager.ERROR_INCORRECT_HASH;
  4666         aCallback(this);
  4667         return;
  4670       let fis = Cc["@mozilla.org/network/file-input-stream;1"].
  4671                 createInstance(Ci.nsIFileInputStream);
  4672       fis.init(this.file, -1, -1, false);
  4673       crypto.updateFromStream(fis, this.file.fileSize);
  4674       let calculatedHash = getHashStringForCrypto(crypto);
  4675       if (calculatedHash != this.hash.data) {
  4676         logger.warn("File hash (" + calculatedHash + ") did not match provided hash (" +
  4677              this.hash.data + ")");
  4678         this.state = AddonManager.STATE_DOWNLOAD_FAILED;
  4679         this.error = AddonManager.ERROR_INCORRECT_HASH;
  4680         aCallback(this);
  4681         return;
  4685     try {
  4686       let self = this;
  4687       this.loadManifest(function  initLocalInstall_loadManifest() {
  4688         XPIDatabase.getVisibleAddonForID(self.addon.id, function initLocalInstall_getVisibleAddon(aAddon) {
  4689           self.existingAddon = aAddon;
  4690           if (aAddon)
  4691             applyBlocklistChanges(aAddon, self.addon);
  4692           self.addon.updateDate = Date.now();
  4693           self.addon.installDate = aAddon ? aAddon.installDate : self.addon.updateDate;
  4695           if (!self.addon.isCompatible) {
  4696             // TODO Should we send some event here?
  4697             self.state = AddonManager.STATE_CHECKING;
  4698             new UpdateChecker(self.addon, {
  4699               onUpdateFinished: function updateChecker_onUpdateFinished(aAddon) {
  4700                 self.state = AddonManager.STATE_DOWNLOADED;
  4701                 XPIProvider.installs.push(self);
  4702                 AddonManagerPrivate.callInstallListeners("onNewInstall",
  4703                                                          self.listeners,
  4704                                                          self.wrapper);
  4706                 aCallback(self);
  4708             }, AddonManager.UPDATE_WHEN_ADDON_INSTALLED);
  4710           else {
  4711             XPIProvider.installs.push(self);
  4712             AddonManagerPrivate.callInstallListeners("onNewInstall",
  4713                                                      self.listeners,
  4714                                                      self.wrapper);
  4716             aCallback(self);
  4718         });
  4719       });
  4721     catch (e) {
  4722       logger.warn("Invalid XPI", e);
  4723       this.state = AddonManager.STATE_DOWNLOAD_FAILED;
  4724       this.error = AddonManager.ERROR_CORRUPT_FILE;
  4725       aCallback(this);
  4726       return;
  4728   },
  4730   /**
  4731    * Initialises this install to be a download from a remote url.
  4733    * @param  aCallback
  4734    *         The callback to pass the initialised AddonInstall to
  4735    * @param  aName
  4736    *         An optional name for the add-on
  4737    * @param  aType
  4738    *         An optional type for the add-on
  4739    * @param  aIcons
  4740    *         Optional icons for the add-on
  4741    * @param  aVersion
  4742    *         An optional version for the add-on
  4743    */
  4744   initAvailableDownload: function AI_initAvailableDownload(aName, aType, aIcons, aVersion, aCallback) {
  4745     this.state = AddonManager.STATE_AVAILABLE;
  4746     this.name = aName;
  4747     this.type = aType;
  4748     this.version = aVersion;
  4749     this.icons = aIcons;
  4750     this.progress = 0;
  4751     this.maxProgress = -1;
  4753     XPIProvider.installs.push(this);
  4754     AddonManagerPrivate.callInstallListeners("onNewInstall", this.listeners,
  4755                                              this.wrapper);
  4757     makeSafe(aCallback)(this);
  4758   },
  4760   /**
  4761    * Starts installation of this add-on from whatever state it is currently at
  4762    * if possible.
  4764    * @throws if installation cannot proceed from the current state
  4765    */
  4766   install: function AI_install() {
  4767     switch (this.state) {
  4768     case AddonManager.STATE_AVAILABLE:
  4769       this.startDownload();
  4770       break;
  4771     case AddonManager.STATE_DOWNLOADED:
  4772       this.startInstall();
  4773       break;
  4774     case AddonManager.STATE_DOWNLOAD_FAILED:
  4775     case AddonManager.STATE_INSTALL_FAILED:
  4776     case AddonManager.STATE_CANCELLED:
  4777       this.removeTemporaryFile();
  4778       this.state = AddonManager.STATE_AVAILABLE;
  4779       this.error = 0;
  4780       this.progress = 0;
  4781       this.maxProgress = -1;
  4782       this.hash = this.originalHash;
  4783       XPIProvider.installs.push(this);
  4784       this.startDownload();
  4785       break;
  4786     case AddonManager.STATE_DOWNLOADING:
  4787     case AddonManager.STATE_CHECKING:
  4788     case AddonManager.STATE_INSTALLING:
  4789       // Installation is already running
  4790       return;
  4791     default:
  4792       throw new Error("Cannot start installing from this state");
  4794   },
  4796   /**
  4797    * Cancels installation of this add-on.
  4799    * @throws if installation cannot be cancelled from the current state
  4800    */
  4801   cancel: function AI_cancel() {
  4802     switch (this.state) {
  4803     case AddonManager.STATE_DOWNLOADING:
  4804       if (this.channel)
  4805         this.channel.cancel(Cr.NS_BINDING_ABORTED);
  4806     case AddonManager.STATE_AVAILABLE:
  4807     case AddonManager.STATE_DOWNLOADED:
  4808       logger.debug("Cancelling download of " + this.sourceURI.spec);
  4809       this.state = AddonManager.STATE_CANCELLED;
  4810       XPIProvider.removeActiveInstall(this);
  4811       AddonManagerPrivate.callInstallListeners("onDownloadCancelled",
  4812                                                this.listeners, this.wrapper);
  4813       this.removeTemporaryFile();
  4814       break;
  4815     case AddonManager.STATE_INSTALLED:
  4816       logger.debug("Cancelling install of " + this.addon.id);
  4817       let xpi = this.installLocation.getStagingDir();
  4818       xpi.append(this.addon.id + ".xpi");
  4819       flushJarCache(xpi);
  4820       this.installLocation.cleanStagingDir([this.addon.id, this.addon.id + ".xpi",
  4821                                             this.addon.id + ".json"]);
  4822       this.state = AddonManager.STATE_CANCELLED;
  4823       XPIProvider.removeActiveInstall(this);
  4825       if (this.existingAddon) {
  4826         delete this.existingAddon.pendingUpgrade;
  4827         this.existingAddon.pendingUpgrade = null;
  4830       AddonManagerPrivate.callAddonListeners("onOperationCancelled", createWrapper(this.addon));
  4832       AddonManagerPrivate.callInstallListeners("onInstallCancelled",
  4833                                                this.listeners, this.wrapper);
  4834       break;
  4835     default:
  4836       throw new Error("Cannot cancel install of " + this.sourceURI.spec +
  4837                       " from this state (" + this.state + ")");
  4839   },
  4841   /**
  4842    * Adds an InstallListener for this instance if the listener is not already
  4843    * registered.
  4845    * @param  aListener
  4846    *         The InstallListener to add
  4847    */
  4848   addListener: function AI_addListener(aListener) {
  4849     if (!this.listeners.some(function addListener_matchListener(i) { return i == aListener; }))
  4850       this.listeners.push(aListener);
  4851   },
  4853   /**
  4854    * Removes an InstallListener for this instance if it is registered.
  4856    * @param  aListener
  4857    *         The InstallListener to remove
  4858    */
  4859   removeListener: function AI_removeListener(aListener) {
  4860     this.listeners = this.listeners.filter(function removeListener_filterListener(i) {
  4861       return i != aListener;
  4862     });
  4863   },
  4865   /**
  4866    * Removes the temporary file owned by this AddonInstall if there is one.
  4867    */
  4868   removeTemporaryFile: function AI_removeTemporaryFile() {
  4869     // Only proceed if this AddonInstall owns its XPI file
  4870     if (!this.ownsTempFile) {
  4871       this.logger.debug("removeTemporaryFile: " + this.sourceURI.spec + " does not own temp file");
  4872       return;
  4875     try {
  4876       this.logger.debug("removeTemporaryFile: " + this.sourceURI.spec + " removing temp file " +
  4877           this.file.path);
  4878       this.file.remove(true);
  4879       this.ownsTempFile = false;
  4881     catch (e) {
  4882       this.logger.warn("Failed to remove temporary file " + this.file.path + " for addon " +
  4883           this.sourceURI.spec,
  4884           e);
  4886   },
  4888   /**
  4889    * Updates the sourceURI and releaseNotesURI values on the Addon being
  4890    * installed by this AddonInstall instance.
  4891    */
  4892   updateAddonURIs: function AI_updateAddonURIs() {
  4893     this.addon.sourceURI = this.sourceURI.spec;
  4894     if (this.releaseNotesURI)
  4895       this.addon.releaseNotesURI = this.releaseNotesURI.spec;
  4896   },
  4898   /**
  4899    * Loads add-on manifests from a multi-package XPI file. Each of the
  4900    * XPI and JAR files contained in the XPI will be extracted. Any that
  4901    * do not contain valid add-ons will be ignored. The first valid add-on will
  4902    * be installed by this AddonInstall instance, the rest will have new
  4903    * AddonInstall instances created for them.
  4905    * @param  aZipReader
  4906    *         An open nsIZipReader for the multi-package XPI's files. This will
  4907    *         be closed before this method returns.
  4908    * @param  aCallback
  4909    *         A function to call when all of the add-on manifests have been
  4910    *         loaded. Because this loadMultipackageManifests is an internal API
  4911    *         we don't exception-wrap this callback
  4912    */
  4913   _loadMultipackageManifests: function AI_loadMultipackageManifests(aZipReader,
  4914                                                                    aCallback) {
  4915     let files = [];
  4916     let entries = aZipReader.findEntries("(*.[Xx][Pp][Ii]|*.[Jj][Aa][Rr])");
  4917     while (entries.hasMore()) {
  4918       let entryName = entries.getNext();
  4919       var target = getTemporaryFile();
  4920       try {
  4921         aZipReader.extract(entryName, target);
  4922         files.push(target);
  4924       catch (e) {
  4925         logger.warn("Failed to extract " + entryName + " from multi-package " +
  4926              "XPI", e);
  4927         target.remove(false);
  4931     aZipReader.close();
  4933     if (files.length == 0) {
  4934       throw new Error("Multi-package XPI does not contain any packages " +
  4935                       "to install");
  4938     let addon = null;
  4940     // Find the first file that has a valid install manifest and use it for
  4941     // the add-on that this AddonInstall instance will install.
  4942     while (files.length > 0) {
  4943       this.removeTemporaryFile();
  4944       this.file = files.shift();
  4945       this.ownsTempFile = true;
  4946       try {
  4947         addon = loadManifestFromZipFile(this.file);
  4948         break;
  4950       catch (e) {
  4951         logger.warn(this.file.leafName + " cannot be installed from multi-package " +
  4952              "XPI", e);
  4956     if (!addon) {
  4957       // No valid add-on was found
  4958       aCallback();
  4959       return;
  4962     this.addon = addon;
  4964     this.updateAddonURIs();
  4966     this.addon._install = this;
  4967     this.name = this.addon.selectedLocale.name;
  4968     this.type = this.addon.type;
  4969     this.version = this.addon.version;
  4971     // Setting the iconURL to something inside the XPI locks the XPI and
  4972     // makes it impossible to delete on Windows.
  4973     //let newIcon = createWrapper(this.addon).iconURL;
  4974     //if (newIcon)
  4975     //  this.iconURL = newIcon;
  4977     // Create new AddonInstall instances for every remaining file
  4978     if (files.length > 0) {
  4979       this.linkedInstalls = [];
  4980       let count = 0;
  4981       let self = this;
  4982       files.forEach(function(file) {
  4983         AddonInstall.createInstall(function loadMultipackageManifests_createInstall(aInstall) {
  4984           // Ignore bad add-ons (createInstall will have logged the error)
  4985           if (aInstall.state == AddonManager.STATE_DOWNLOAD_FAILED) {
  4986             // Manually remove the temporary file
  4987             file.remove(true);
  4989           else {
  4990             // Make the new install own its temporary file
  4991             aInstall.ownsTempFile = true;
  4993             self.linkedInstalls.push(aInstall)
  4995             aInstall.sourceURI = self.sourceURI;
  4996             aInstall.releaseNotesURI = self.releaseNotesURI;
  4997             aInstall.updateAddonURIs();
  5000           count++;
  5001           if (count == files.length)
  5002             aCallback();
  5003         }, file);
  5004       }, this);
  5006     else {
  5007       aCallback();
  5009   },
  5011   /**
  5012    * Called after the add-on is a local file and the signature and install
  5013    * manifest can be read.
  5015    * @param  aCallback
  5016    *         A function to call when the manifest has been loaded
  5017    * @throws if the add-on does not contain a valid install manifest or the
  5018    *         XPI is incorrectly signed
  5019    */
  5020   loadManifest: function AI_loadManifest(aCallback) {
  5021     aCallback = makeSafe(aCallback);
  5022     let self = this;
  5023     function addRepositoryData(aAddon) {
  5024       // Try to load from the existing cache first
  5025       AddonRepository.getCachedAddonByID(aAddon.id, function loadManifest_getCachedAddonByID(aRepoAddon) {
  5026         if (aRepoAddon) {
  5027           aAddon._repositoryAddon = aRepoAddon;
  5028           self.name = self.name || aAddon._repositoryAddon.name;
  5029           aAddon.compatibilityOverrides = aRepoAddon.compatibilityOverrides;
  5030           aAddon.appDisabled = !isUsableAddon(aAddon);
  5031           aCallback();
  5032           return;
  5035         // It wasn't there so try to re-download it
  5036         AddonRepository.cacheAddons([aAddon.id], function loadManifest_cacheAddons() {
  5037           AddonRepository.getCachedAddonByID(aAddon.id, function loadManifest_getCachedAddonByID(aRepoAddon) {
  5038             aAddon._repositoryAddon = aRepoAddon;
  5039             self.name = self.name || aAddon._repositoryAddon.name;
  5040             aAddon.compatibilityOverrides = aRepoAddon ?
  5041                                               aRepoAddon.compatibilityOverrides :
  5042                                               null;
  5043             aAddon.appDisabled = !isUsableAddon(aAddon);
  5044             aCallback();
  5045           });
  5046         });
  5047       });
  5050     let zipreader = Cc["@mozilla.org/libjar/zip-reader;1"].
  5051                     createInstance(Ci.nsIZipReader);
  5052     try {
  5053       zipreader.open(this.file);
  5055     catch (e) {
  5056       zipreader.close();
  5057       throw e;
  5060     let principal = zipreader.getCertificatePrincipal(null);
  5061     if (principal && principal.hasCertificate) {
  5062       logger.debug("Verifying XPI signature");
  5063       if (verifyZipSigning(zipreader, principal)) {
  5064         let x509 = principal.certificate;
  5065         if (x509 instanceof Ci.nsIX509Cert)
  5066           this.certificate = x509;
  5067         if (this.certificate && this.certificate.commonName.length > 0)
  5068           this.certName = this.certificate.commonName;
  5069         else
  5070           this.certName = principal.prettyName;
  5072       else {
  5073         zipreader.close();
  5074         throw new Error("XPI is incorrectly signed");
  5078     try {
  5079       this.addon = loadManifestFromZipReader(zipreader);
  5081     catch (e) {
  5082       zipreader.close();
  5083       throw e;
  5086     if (this.addon.type == "multipackage") {
  5087       this._loadMultipackageManifests(zipreader, function loadManifest_loadMultipackageManifests() {
  5088         addRepositoryData(self.addon);
  5089       });
  5090       return;
  5093     zipreader.close();
  5095     this.updateAddonURIs();
  5097     this.addon._install = this;
  5098     this.name = this.addon.selectedLocale.name;
  5099     this.type = this.addon.type;
  5100     this.version = this.addon.version;
  5102     // Setting the iconURL to something inside the XPI locks the XPI and
  5103     // makes it impossible to delete on Windows.
  5104     //let newIcon = createWrapper(this.addon).iconURL;
  5105     //if (newIcon)
  5106     //  this.iconURL = newIcon;
  5108     addRepositoryData(this.addon);
  5109   },
  5111   observe: function AI_observe(aSubject, aTopic, aData) {
  5112     // Network is going offline
  5113     this.cancel();
  5114   },
  5116   /**
  5117    * Starts downloading the add-on's XPI file.
  5118    */
  5119   startDownload: function AI_startDownload() {
  5120     this.state = AddonManager.STATE_DOWNLOADING;
  5121     if (!AddonManagerPrivate.callInstallListeners("onDownloadStarted",
  5122                                                   this.listeners, this.wrapper)) {
  5123       logger.debug("onDownloadStarted listeners cancelled installation of addon " + this.sourceURI.spec);
  5124       this.state = AddonManager.STATE_CANCELLED;
  5125       XPIProvider.removeActiveInstall(this);
  5126       AddonManagerPrivate.callInstallListeners("onDownloadCancelled",
  5127                                                this.listeners, this.wrapper)
  5128       return;
  5131     // If a listener changed our state then do not proceed with the download
  5132     if (this.state != AddonManager.STATE_DOWNLOADING)
  5133       return;
  5135     if (this.channel) {
  5136       // A previous download attempt hasn't finished cleaning up yet, signal
  5137       // that it should restart when complete
  5138       logger.debug("Waiting for previous download to complete");
  5139       this.restartDownload = true;
  5140       return;
  5143     this.openChannel();
  5144   },
  5146   openChannel: function AI_openChannel() {
  5147     this.restartDownload = false;
  5149     try {
  5150       this.file = getTemporaryFile();
  5151       this.ownsTempFile = true;
  5152       this.stream = Cc["@mozilla.org/network/file-output-stream;1"].
  5153                     createInstance(Ci.nsIFileOutputStream);
  5154       this.stream.init(this.file, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE |
  5155                        FileUtils.MODE_TRUNCATE, FileUtils.PERMS_FILE, 0);
  5157     catch (e) {
  5158       logger.warn("Failed to start download for addon " + this.sourceURI.spec, e);
  5159       this.state = AddonManager.STATE_DOWNLOAD_FAILED;
  5160       this.error = AddonManager.ERROR_FILE_ACCESS;
  5161       XPIProvider.removeActiveInstall(this);
  5162       AddonManagerPrivate.callInstallListeners("onDownloadFailed",
  5163                                                this.listeners, this.wrapper);
  5164       return;
  5167     let listener = Cc["@mozilla.org/network/stream-listener-tee;1"].
  5168                    createInstance(Ci.nsIStreamListenerTee);
  5169     listener.init(this, this.stream);
  5170     try {
  5171       Components.utils.import("resource://gre/modules/CertUtils.jsm");
  5172       let requireBuiltIn = Prefs.getBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, true);
  5173       this.badCertHandler = new BadCertHandler(!requireBuiltIn);
  5175       this.channel = NetUtil.newChannel(this.sourceURI);
  5176       this.channel.notificationCallbacks = this;
  5177       if (this.channel instanceof Ci.nsIHttpChannelInternal)
  5178         this.channel.forceAllowThirdPartyCookie = true;
  5179       this.channel.asyncOpen(listener, null);
  5181       Services.obs.addObserver(this, "network:offline-about-to-go-offline", false);
  5183     catch (e) {
  5184       logger.warn("Failed to start download for addon " + this.sourceURI.spec, e);
  5185       this.state = AddonManager.STATE_DOWNLOAD_FAILED;
  5186       this.error = AddonManager.ERROR_NETWORK_FAILURE;
  5187       XPIProvider.removeActiveInstall(this);
  5188       AddonManagerPrivate.callInstallListeners("onDownloadFailed",
  5189                                                this.listeners, this.wrapper);
  5191   },
  5193   /**
  5194    * Update the crypto hasher with the new data and call the progress listeners.
  5196    * @see nsIStreamListener
  5197    */
  5198   onDataAvailable: function AI_onDataAvailable(aRequest, aContext, aInputstream,
  5199                                                aOffset, aCount) {
  5200     this.crypto.updateFromStream(aInputstream, aCount);
  5201     this.progress += aCount;
  5202     if (!AddonManagerPrivate.callInstallListeners("onDownloadProgress",
  5203                                                   this.listeners, this.wrapper)) {
  5204       // TODO cancel the download and make it available again (bug 553024)
  5206   },
  5208   /**
  5209    * Check the redirect response for a hash of the target XPI and verify that
  5210    * we don't end up on an insecure channel.
  5212    * @see nsIChannelEventSink
  5213    */
  5214   asyncOnChannelRedirect: function AI_asyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, aCallback) {
  5215     if (!this.hash && aOldChannel.originalURI.schemeIs("https") &&
  5216         aOldChannel instanceof Ci.nsIHttpChannel) {
  5217       try {
  5218         let hashStr = aOldChannel.getResponseHeader("X-Target-Digest");
  5219         let hashSplit = hashStr.toLowerCase().split(":");
  5220         this.hash = {
  5221           algorithm: hashSplit[0],
  5222           data: hashSplit[1]
  5223         };
  5225       catch (e) {
  5229     // Verify that we don't end up on an insecure channel if we haven't got a
  5230     // hash to verify with (see bug 537761 for discussion)
  5231     if (!this.hash)
  5232       this.badCertHandler.asyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, aCallback);
  5233     else
  5234       aCallback.onRedirectVerifyCallback(Cr.NS_OK);
  5236     this.channel = aNewChannel;
  5237   },
  5239   /**
  5240    * This is the first chance to get at real headers on the channel.
  5242    * @see nsIStreamListener
  5243    */
  5244   onStartRequest: function AI_onStartRequest(aRequest, aContext) {
  5245     this.crypto = Cc["@mozilla.org/security/hash;1"].
  5246                   createInstance(Ci.nsICryptoHash);
  5247     if (this.hash) {
  5248       try {
  5249         this.crypto.initWithString(this.hash.algorithm);
  5251       catch (e) {
  5252         logger.warn("Unknown hash algorithm '" + this.hash.algorithm + "' for addon " + this.sourceURI.spec, e);
  5253         this.state = AddonManager.STATE_DOWNLOAD_FAILED;
  5254         this.error = AddonManager.ERROR_INCORRECT_HASH;
  5255         XPIProvider.removeActiveInstall(this);
  5256         AddonManagerPrivate.callInstallListeners("onDownloadFailed",
  5257                                                  this.listeners, this.wrapper);
  5258         aRequest.cancel(Cr.NS_BINDING_ABORTED);
  5259         return;
  5262     else {
  5263       // We always need something to consume data from the inputstream passed
  5264       // to onDataAvailable so just create a dummy cryptohasher to do that.
  5265       this.crypto.initWithString("sha1");
  5268     this.progress = 0;
  5269     if (aRequest instanceof Ci.nsIChannel) {
  5270       try {
  5271         this.maxProgress = aRequest.contentLength;
  5273       catch (e) {
  5275       logger.debug("Download started for " + this.sourceURI.spec + " to file " +
  5276           this.file.path);
  5278   },
  5280   /**
  5281    * The download is complete.
  5283    * @see nsIStreamListener
  5284    */
  5285   onStopRequest: function AI_onStopRequest(aRequest, aContext, aStatus) {
  5286     this.stream.close();
  5287     this.channel = null;
  5288     this.badCerthandler = null;
  5289     Services.obs.removeObserver(this, "network:offline-about-to-go-offline");
  5291     // If the download was cancelled then all events will have already been sent
  5292     if (aStatus == Cr.NS_BINDING_ABORTED) {
  5293       this.removeTemporaryFile();
  5294       if (this.restartDownload)
  5295         this.openChannel();
  5296       return;
  5299     logger.debug("Download of " + this.sourceURI.spec + " completed.");
  5301     if (Components.isSuccessCode(aStatus)) {
  5302       if (!(aRequest instanceof Ci.nsIHttpChannel) || aRequest.requestSucceeded) {
  5303         if (!this.hash && (aRequest instanceof Ci.nsIChannel)) {
  5304           try {
  5305             checkCert(aRequest,
  5306                       !Prefs.getBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, true));
  5308           catch (e) {
  5309             this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, e);
  5310             return;
  5314         // convert the binary hash data to a hex string.
  5315         let calculatedHash = getHashStringForCrypto(this.crypto);
  5316         this.crypto = null;
  5317         if (this.hash && calculatedHash != this.hash.data) {
  5318           this.downloadFailed(AddonManager.ERROR_INCORRECT_HASH,
  5319                               "Downloaded file hash (" + calculatedHash +
  5320                               ") did not match provided hash (" + this.hash.data + ")");
  5321           return;
  5323         try {
  5324           let self = this;
  5325           this.loadManifest(function onStopRequest_loadManifest() {
  5326             if (self.addon.isCompatible) {
  5327               self.downloadCompleted();
  5329             else {
  5330               // TODO Should we send some event here (bug 557716)?
  5331               self.state = AddonManager.STATE_CHECKING;
  5332               new UpdateChecker(self.addon, {
  5333                 onUpdateFinished: function onStopRequest_onUpdateFinished(aAddon) {
  5334                   self.downloadCompleted();
  5336               }, AddonManager.UPDATE_WHEN_ADDON_INSTALLED);
  5338           });
  5340         catch (e) {
  5341           this.downloadFailed(AddonManager.ERROR_CORRUPT_FILE, e);
  5344       else {
  5345         if (aRequest instanceof Ci.nsIHttpChannel)
  5346           this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE,
  5347                               aRequest.responseStatus + " " +
  5348                               aRequest.responseStatusText);
  5349         else
  5350           this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, aStatus);
  5353     else {
  5354       this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, aStatus);
  5356   },
  5358   /**
  5359    * Notify listeners that the download failed.
  5361    * @param  aReason
  5362    *         Something to log about the failure
  5363    * @param  error
  5364    *         The error code to pass to the listeners
  5365    */
  5366   downloadFailed: function AI_downloadFailed(aReason, aError) {
  5367     logger.warn("Download of " + this.sourceURI.spec + " failed", aError);
  5368     this.state = AddonManager.STATE_DOWNLOAD_FAILED;
  5369     this.error = aReason;
  5370     XPIProvider.removeActiveInstall(this);
  5371     AddonManagerPrivate.callInstallListeners("onDownloadFailed", this.listeners,
  5372                                              this.wrapper);
  5374     // If the listener hasn't restarted the download then remove any temporary
  5375     // file
  5376     if (this.state == AddonManager.STATE_DOWNLOAD_FAILED) {
  5377       logger.debug("downloadFailed: removing temp file for " + this.sourceURI.spec);
  5378       this.removeTemporaryFile();
  5380     else
  5381       logger.debug("downloadFailed: listener changed AddonInstall state for " +
  5382           this.sourceURI.spec + " to " + this.state);
  5383   },
  5385   /**
  5386    * Notify listeners that the download completed.
  5387    */
  5388   downloadCompleted: function AI_downloadCompleted() {
  5389     let self = this;
  5390     XPIDatabase.getVisibleAddonForID(this.addon.id, function downloadCompleted_getVisibleAddonForID(aAddon) {
  5391       if (aAddon)
  5392         self.existingAddon = aAddon;
  5394       self.state = AddonManager.STATE_DOWNLOADED;
  5395       self.addon.updateDate = Date.now();
  5397       if (self.existingAddon) {
  5398         self.addon.existingAddonID = self.existingAddon.id;
  5399         self.addon.installDate = self.existingAddon.installDate;
  5400         applyBlocklistChanges(self.existingAddon, self.addon);
  5402       else {
  5403         self.addon.installDate = self.addon.updateDate;
  5406       if (AddonManagerPrivate.callInstallListeners("onDownloadEnded",
  5407                                                    self.listeners,
  5408                                                    self.wrapper)) {
  5409         // If a listener changed our state then do not proceed with the install
  5410         if (self.state != AddonManager.STATE_DOWNLOADED)
  5411           return;
  5413         self.install();
  5415         if (self.linkedInstalls) {
  5416           self.linkedInstalls.forEach(function(aInstall) {
  5417             aInstall.install();
  5418           });
  5421     });
  5422   },
  5424   // TODO This relies on the assumption that we are always installing into the
  5425   // highest priority install location so the resulting add-on will be visible
  5426   // overriding any existing copy in another install location (bug 557710).
  5427   /**
  5428    * Installs the add-on into the install location.
  5429    */
  5430   startInstall: function AI_startInstall() {
  5431     this.state = AddonManager.STATE_INSTALLING;
  5432     if (!AddonManagerPrivate.callInstallListeners("onInstallStarted",
  5433                                                   this.listeners, this.wrapper)) {
  5434       this.state = AddonManager.STATE_DOWNLOADED;
  5435       XPIProvider.removeActiveInstall(this);
  5436       AddonManagerPrivate.callInstallListeners("onInstallCancelled",
  5437                                                this.listeners, this.wrapper)
  5438       return;
  5441     // Find and cancel any pending installs for the same add-on in the same
  5442     // install location
  5443     for (let aInstall of XPIProvider.installs) {
  5444       if (aInstall.state == AddonManager.STATE_INSTALLED &&
  5445           aInstall.installLocation == this.installLocation &&
  5446           aInstall.addon.id == this.addon.id) {
  5447         logger.debug("Cancelling previous pending install of " + aInstall.addon.id);
  5448         aInstall.cancel();
  5452     let isUpgrade = this.existingAddon &&
  5453                     this.existingAddon._installLocation == this.installLocation;
  5454     let requiresRestart = XPIProvider.installRequiresRestart(this.addon);
  5456     logger.debug("Starting install of " + this.addon.id + " from " + this.sourceURI.spec);
  5457     AddonManagerPrivate.callAddonListeners("onInstalling",
  5458                                            createWrapper(this.addon),
  5459                                            requiresRestart);
  5461     let stagingDir = this.installLocation.getStagingDir();
  5462     let stagedAddon = stagingDir.clone();
  5464     Task.spawn((function() {
  5465       let installedUnpacked = 0;
  5466       yield this.installLocation.requestStagingDir();
  5468       // First stage the file regardless of whether restarting is necessary
  5469       if (this.addon.unpack || Prefs.getBoolPref(PREF_XPI_UNPACK, false)) {
  5470         logger.debug("Addon " + this.addon.id + " will be installed as " +
  5471             "an unpacked directory");
  5472         stagedAddon.append(this.addon.id);
  5473         yield removeAsync(stagedAddon);
  5474         yield OS.File.makeDir(stagedAddon.path);
  5475         yield ZipUtils.extractFilesAsync(this.file, stagedAddon);
  5476         installedUnpacked = 1;
  5478       else {
  5479         logger.debug("Addon " + this.addon.id + " will be installed as " +
  5480             "a packed xpi");
  5481         stagedAddon.append(this.addon.id + ".xpi");
  5482         yield removeAsync(stagedAddon);
  5483         yield OS.File.copy(this.file.path, stagedAddon.path);
  5486       if (requiresRestart) {
  5487         // Point the add-on to its extracted files as the xpi may get deleted
  5488         this.addon._sourceBundle = stagedAddon;
  5490         // Cache the AddonInternal as it may have updated compatibility info
  5491         let stagedJSON = stagedAddon.clone();
  5492         stagedJSON.leafName = this.addon.id + ".json";
  5493         if (stagedJSON.exists())
  5494           stagedJSON.remove(true);
  5495         let stream = Cc["@mozilla.org/network/file-output-stream;1"].
  5496                      createInstance(Ci.nsIFileOutputStream);
  5497         let converter = Cc["@mozilla.org/intl/converter-output-stream;1"].
  5498                         createInstance(Ci.nsIConverterOutputStream);
  5500         try {
  5501           stream.init(stagedJSON, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE |
  5502                                   FileUtils.MODE_TRUNCATE, FileUtils.PERMS_FILE,
  5503                                  0);
  5504           converter.init(stream, "UTF-8", 0, 0x0000);
  5505           converter.writeString(JSON.stringify(this.addon));
  5507         finally {
  5508           converter.close();
  5509           stream.close();
  5512         logger.debug("Staged install of " + this.addon.id + " from " + this.sourceURI.spec + " ready; waiting for restart.");
  5513         this.state = AddonManager.STATE_INSTALLED;
  5514         if (isUpgrade) {
  5515           delete this.existingAddon.pendingUpgrade;
  5516           this.existingAddon.pendingUpgrade = this.addon;
  5518         AddonManagerPrivate.callInstallListeners("onInstallEnded",
  5519                                                  this.listeners, this.wrapper,
  5520                                                  createWrapper(this.addon));
  5522       else {
  5523         // The install is completed so it should be removed from the active list
  5524         XPIProvider.removeActiveInstall(this);
  5526         // TODO We can probably reduce the number of DB operations going on here
  5527         // We probably also want to support rolling back failed upgrades etc.
  5528         // See bug 553015.
  5530         // Deactivate and remove the old add-on as necessary
  5531         let reason = BOOTSTRAP_REASONS.ADDON_INSTALL;
  5532         if (this.existingAddon) {
  5533           if (Services.vc.compare(this.existingAddon.version, this.addon.version) < 0)
  5534             reason = BOOTSTRAP_REASONS.ADDON_UPGRADE;
  5535           else
  5536             reason = BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
  5538           if (this.existingAddon.bootstrap) {
  5539             let file = this.existingAddon._installLocation
  5540                            .getLocationForID(this.existingAddon.id);
  5541             if (this.existingAddon.active) {
  5542               XPIProvider.callBootstrapMethod(this.existingAddon.id,
  5543                                               this.existingAddon.version,
  5544                                               this.existingAddon.type, file,
  5545                                               "shutdown", reason,
  5546                                               { newVersion: this.addon.version });
  5549             XPIProvider.callBootstrapMethod(this.existingAddon.id,
  5550                                             this.existingAddon.version,
  5551                                             this.existingAddon.type, file,
  5552                                             "uninstall", reason,
  5553                                             { newVersion: this.addon.version });
  5554             XPIProvider.unloadBootstrapScope(this.existingAddon.id);
  5555             flushStartupCache();
  5558           if (!isUpgrade && this.existingAddon.active) {
  5559             XPIDatabase.updateAddonActive(this.existingAddon, false);
  5563         // Install the new add-on into its final location
  5564         let existingAddonID = this.existingAddon ? this.existingAddon.id : null;
  5565         let file = this.installLocation.installAddon(this.addon.id, stagedAddon,
  5566                                                      existingAddonID);
  5568         // Update the metadata in the database
  5569         this.addon._sourceBundle = file;
  5570         this.addon._installLocation = this.installLocation;
  5571         let scanStarted = Date.now();
  5572         let [, mTime, scanItems] = recursiveLastModifiedTime(file);
  5573         let scanTime = Date.now() - scanStarted;
  5574         this.addon.updateDate = mTime;
  5575         this.addon.visible = true;
  5576         if (isUpgrade) {
  5577           this.addon =  XPIDatabase.updateAddonMetadata(this.existingAddon, this.addon,
  5578                                                         file.persistentDescriptor);
  5580         else {
  5581           this.addon.installDate = this.addon.updateDate;
  5582           this.addon.active = (this.addon.visible && !isAddonDisabled(this.addon))
  5583           this.addon = XPIDatabase.addAddonMetadata(this.addon, file.persistentDescriptor);
  5586         let extraParams = {};
  5587         if (this.existingAddon) {
  5588           extraParams.oldVersion = this.existingAddon.version;
  5591         if (this.addon.bootstrap) {
  5592           XPIProvider.callBootstrapMethod(this.addon.id, this.addon.version,
  5593                                           this.addon.type, file, "install",
  5594                                           reason, extraParams);
  5597         AddonManagerPrivate.callAddonListeners("onInstalled",
  5598                                                createWrapper(this.addon));
  5600         logger.debug("Install of " + this.sourceURI.spec + " completed.");
  5601         this.state = AddonManager.STATE_INSTALLED;
  5602         AddonManagerPrivate.callInstallListeners("onInstallEnded",
  5603                                                  this.listeners, this.wrapper,
  5604                                                  createWrapper(this.addon));
  5606         if (this.addon.bootstrap) {
  5607           if (this.addon.active) {
  5608             XPIProvider.callBootstrapMethod(this.addon.id, this.addon.version,
  5609                                             this.addon.type, file, "startup",
  5610                                             reason, extraParams);
  5612           else {
  5613             // XXX this makes it dangerous to do some things in onInstallEnded
  5614             // listeners because important cleanup hasn't been done yet
  5615             XPIProvider.unloadBootstrapScope(this.addon.id);
  5618         XPIProvider.setTelemetry(this.addon.id, "unpacked", installedUnpacked);
  5619         XPIProvider.setTelemetry(this.addon.id, "location", this.installLocation.name);
  5620         XPIProvider.setTelemetry(this.addon.id, "scan_MS", scanTime);
  5621         XPIProvider.setTelemetry(this.addon.id, "scan_items", scanItems);
  5622         let loc = this.addon.defaultLocale;
  5623         if (loc) {
  5624           XPIProvider.setTelemetry(this.addon.id, "name", loc.name);
  5625           XPIProvider.setTelemetry(this.addon.id, "creator", loc.creator);
  5628     }).bind(this)).then(null, (e) => {
  5629       logger.warn("Failed to install " + this.file.path + " from " + this.sourceURI.spec, e);
  5630       if (stagedAddon.exists())
  5631         recursiveRemove(stagedAddon);
  5632       this.state = AddonManager.STATE_INSTALL_FAILED;
  5633       this.error = AddonManager.ERROR_FILE_ACCESS;
  5634       XPIProvider.removeActiveInstall(this);
  5635       AddonManagerPrivate.callAddonListeners("onOperationCancelled",
  5636                                              createWrapper(this.addon));
  5637       AddonManagerPrivate.callInstallListeners("onInstallFailed",
  5638                                                this.listeners,
  5639                                                this.wrapper);
  5640     }).then(() => {
  5641       this.removeTemporaryFile();
  5642       return this.installLocation.releaseStagingDir();
  5643     });
  5644   },
  5646   getInterface: function AI_getInterface(iid) {
  5647     if (iid.equals(Ci.nsIAuthPrompt2)) {
  5648       var factory = Cc["@mozilla.org/prompter;1"].
  5649                     getService(Ci.nsIPromptFactory);
  5650       return factory.getPrompt(this.window, Ci.nsIAuthPrompt);
  5652     else if (iid.equals(Ci.nsIChannelEventSink)) {
  5653       return this;
  5656     return this.badCertHandler.getInterface(iid);
  5660 /**
  5661  * Creates a new AddonInstall for an already staged install. Used when
  5662  * installing the staged install failed for some reason.
  5664  * @param  aDir
  5665  *         The directory holding the staged install
  5666  * @param  aManifest
  5667  *         The cached manifest for the install
  5668  */
  5669 AddonInstall.createStagedInstall = function AI_createStagedInstall(aInstallLocation, aDir, aManifest) {
  5670   let url = Services.io.newFileURI(aDir);
  5672   let install = new AddonInstall(aInstallLocation, aDir);
  5673   install.initStagedInstall(aManifest);
  5674 };
  5676 /**
  5677  * Creates a new AddonInstall to install an add-on from a local file. Installs
  5678  * always go into the profile install location.
  5680  * @param  aCallback
  5681  *         The callback to pass the new AddonInstall to
  5682  * @param  aFile
  5683  *         The file to install
  5684  */
  5685 AddonInstall.createInstall = function AI_createInstall(aCallback, aFile) {
  5686   let location = XPIProvider.installLocationsByName[KEY_APP_PROFILE];
  5687   let url = Services.io.newFileURI(aFile);
  5689   try {
  5690     let install = new AddonInstall(location, url);
  5691     install.initLocalInstall(aCallback);
  5693   catch(e) {
  5694     logger.error("Error creating install", e);
  5695     makeSafe(aCallback)(null);
  5697 };
  5699 /**
  5700  * Creates a new AddonInstall to download and install a URL.
  5702  * @param  aCallback
  5703  *         The callback to pass the new AddonInstall to
  5704  * @param  aUri
  5705  *         The URI to download
  5706  * @param  aHash
  5707  *         A hash for the add-on
  5708  * @param  aName
  5709  *         A name for the add-on
  5710  * @param  aIcons
  5711  *         An icon URLs for the add-on
  5712  * @param  aVersion
  5713  *         A version for the add-on
  5714  * @param  aLoadGroup
  5715  *         An nsILoadGroup to associate the download with
  5716  */
  5717 AddonInstall.createDownload = function AI_createDownload(aCallback, aUri, aHash, aName, aIcons,
  5718                                        aVersion, aLoadGroup) {
  5719   let location = XPIProvider.installLocationsByName[KEY_APP_PROFILE];
  5720   let url = NetUtil.newURI(aUri);
  5722   let install = new AddonInstall(location, url, aHash, null, null, aLoadGroup);
  5723   if (url instanceof Ci.nsIFileURL)
  5724     install.initLocalInstall(aCallback);
  5725   else
  5726     install.initAvailableDownload(aName, null, aIcons, aVersion, aCallback);
  5727 };
  5729 /**
  5730  * Creates a new AddonInstall for an update.
  5732  * @param  aCallback
  5733  *         The callback to pass the new AddonInstall to
  5734  * @param  aAddon
  5735  *         The add-on being updated
  5736  * @param  aUpdate
  5737  *         The metadata about the new version from the update manifest
  5738  */
  5739 AddonInstall.createUpdate = function AI_createUpdate(aCallback, aAddon, aUpdate) {
  5740   let url = NetUtil.newURI(aUpdate.updateURL);
  5741   let releaseNotesURI = null;
  5742   try {
  5743     if (aUpdate.updateInfoURL)
  5744       releaseNotesURI = NetUtil.newURI(escapeAddonURI(aAddon, aUpdate.updateInfoURL));
  5746   catch (e) {
  5747     // If the releaseNotesURI cannot be parsed then just ignore it.
  5750   let install = new AddonInstall(aAddon._installLocation, url,
  5751                                  aUpdate.updateHash, releaseNotesURI, aAddon);
  5752   if (url instanceof Ci.nsIFileURL) {
  5753     install.initLocalInstall(aCallback);
  5755   else {
  5756     install.initAvailableDownload(aAddon.selectedLocale.name, aAddon.type,
  5757                                   aAddon.icons, aUpdate.version, aCallback);
  5759 };
  5761 /**
  5762  * Creates a wrapper for an AddonInstall that only exposes the public API
  5764  * @param  install
  5765  *         The AddonInstall to create a wrapper for
  5766  */
  5767 function AddonInstallWrapper(aInstall) {
  5768 #ifdef MOZ_EM_DEBUG
  5769   this.__defineGetter__("__AddonInstallInternal__", function AIW_debugGetter() {
  5770     return aInstall;
  5771   });
  5772 #endif
  5774   ["name", "type", "version", "icons", "releaseNotesURI", "file", "state", "error",
  5775    "progress", "maxProgress", "certificate", "certName"].forEach(function(aProp) {
  5776     this.__defineGetter__(aProp, function AIW_propertyGetter() aInstall[aProp]);
  5777   }, this);
  5779   this.__defineGetter__("iconURL", function AIW_iconURL() aInstall.icons[32]);
  5781   this.__defineGetter__("existingAddon", function AIW_existingAddonGetter() {
  5782     return createWrapper(aInstall.existingAddon);
  5783   });
  5784   this.__defineGetter__("addon", function AIW_addonGetter() createWrapper(aInstall.addon));
  5785   this.__defineGetter__("sourceURI", function AIW_sourceURIGetter() aInstall.sourceURI);
  5787   this.__defineGetter__("linkedInstalls", function AIW_linkedInstallsGetter() {
  5788     if (!aInstall.linkedInstalls)
  5789       return null;
  5790     return [i.wrapper for each (i in aInstall.linkedInstalls)];
  5791   });
  5793   this.install = function AIW_install() {
  5794     aInstall.install();
  5797   this.cancel = function AIW_cancel() {
  5798     aInstall.cancel();
  5801   this.addListener = function AIW_addListener(listener) {
  5802     aInstall.addListener(listener);
  5805   this.removeListener = function AIW_removeListener(listener) {
  5806     aInstall.removeListener(listener);
  5810 AddonInstallWrapper.prototype = {};
  5812 /**
  5813  * Creates a new update checker.
  5815  * @param  aAddon
  5816  *         The add-on to check for updates
  5817  * @param  aListener
  5818  *         An UpdateListener to notify of updates
  5819  * @param  aReason
  5820  *         The reason for the update check
  5821  * @param  aAppVersion
  5822  *         An optional application version to check for updates for
  5823  * @param  aPlatformVersion
  5824  *         An optional platform version to check for updates for
  5825  * @throws if the aListener or aReason arguments are not valid
  5826  */
  5827 function UpdateChecker(aAddon, aListener, aReason, aAppVersion, aPlatformVersion) {
  5828   if (!aListener || !aReason)
  5829     throw Cr.NS_ERROR_INVALID_ARG;
  5831   Components.utils.import("resource://gre/modules/addons/AddonUpdateChecker.jsm");
  5833   this.addon = aAddon;
  5834   aAddon._updateCheck = this;
  5835   XPIProvider.doing(this);
  5836   this.listener = aListener;
  5837   this.appVersion = aAppVersion;
  5838   this.platformVersion = aPlatformVersion;
  5839   this.syncCompatibility = (aReason == AddonManager.UPDATE_WHEN_NEW_APP_INSTALLED);
  5841   let updateURL = aAddon.updateURL;
  5842   if (!updateURL) {
  5843     if (aReason == AddonManager.UPDATE_WHEN_PERIODIC_UPDATE &&
  5844         Services.prefs.getPrefType(PREF_EM_UPDATE_BACKGROUND_URL) == Services.prefs.PREF_STRING) {
  5845       updateURL = Services.prefs.getCharPref(PREF_EM_UPDATE_BACKGROUND_URL);
  5846     } else {
  5847       updateURL = Services.prefs.getCharPref(PREF_EM_UPDATE_URL);
  5851   const UPDATE_TYPE_COMPATIBILITY = 32;
  5852   const UPDATE_TYPE_NEWVERSION = 64;
  5854   aReason |= UPDATE_TYPE_COMPATIBILITY;
  5855   if ("onUpdateAvailable" in this.listener)
  5856     aReason |= UPDATE_TYPE_NEWVERSION;
  5858   let url = escapeAddonURI(aAddon, updateURL, aReason, aAppVersion);
  5859   this._parser = AddonUpdateChecker.checkForUpdates(aAddon.id, aAddon.updateKey,
  5860                                                     url, this);
  5863 UpdateChecker.prototype = {
  5864   addon: null,
  5865   listener: null,
  5866   appVersion: null,
  5867   platformVersion: null,
  5868   syncCompatibility: null,
  5870   /**
  5871    * Calls a method on the listener passing any number of arguments and
  5872    * consuming any exceptions.
  5874    * @param  aMethod
  5875    *         The method to call on the listener
  5876    */
  5877   callListener: function UC_callListener(aMethod, ...aArgs) {
  5878     if (!(aMethod in this.listener))
  5879       return;
  5881     try {
  5882       this.listener[aMethod].apply(this.listener, aArgs);
  5884     catch (e) {
  5885       logger.warn("Exception calling UpdateListener method " + aMethod, e);
  5887   },
  5889   /**
  5890    * Called when AddonUpdateChecker completes the update check
  5892    * @param  updates
  5893    *         The list of update details for the add-on
  5894    */
  5895   onUpdateCheckComplete: function UC_onUpdateCheckComplete(aUpdates) {
  5896     XPIProvider.done(this.addon._updateCheck);
  5897     this.addon._updateCheck = null;
  5898     let AUC = AddonUpdateChecker;
  5900     let ignoreMaxVersion = false;
  5901     let ignoreStrictCompat = false;
  5902     if (!AddonManager.checkCompatibility) {
  5903       ignoreMaxVersion = true;
  5904       ignoreStrictCompat = true;
  5905     } else if (this.addon.type in COMPATIBLE_BY_DEFAULT_TYPES &&
  5906                !AddonManager.strictCompatibility &&
  5907                !this.addon.strictCompatibility &&
  5908                !this.addon.hasBinaryComponents) {
  5909       ignoreMaxVersion = true;
  5912     // Always apply any compatibility update for the current version
  5913     let compatUpdate = AUC.getCompatibilityUpdate(aUpdates, this.addon.version,
  5914                                                   this.syncCompatibility,
  5915                                                   null, null,
  5916                                                   ignoreMaxVersion,
  5917                                                   ignoreStrictCompat);
  5918     // Apply the compatibility update to the database
  5919     if (compatUpdate)
  5920       this.addon.applyCompatibilityUpdate(compatUpdate, this.syncCompatibility);
  5922     // If the request is for an application or platform version that is
  5923     // different to the current application or platform version then look for a
  5924     // compatibility update for those versions.
  5925     if ((this.appVersion &&
  5926          Services.vc.compare(this.appVersion, Services.appinfo.version) != 0) ||
  5927         (this.platformVersion &&
  5928          Services.vc.compare(this.platformVersion, Services.appinfo.platformVersion) != 0)) {
  5929       compatUpdate = AUC.getCompatibilityUpdate(aUpdates, this.addon.version,
  5930                                                 false, this.appVersion,
  5931                                                 this.platformVersion,
  5932                                                 ignoreMaxVersion,
  5933                                                 ignoreStrictCompat);
  5936     if (compatUpdate)
  5937       this.callListener("onCompatibilityUpdateAvailable", createWrapper(this.addon));
  5938     else
  5939       this.callListener("onNoCompatibilityUpdateAvailable", createWrapper(this.addon));
  5941     function sendUpdateAvailableMessages(aSelf, aInstall) {
  5942       if (aInstall) {
  5943         aSelf.callListener("onUpdateAvailable", createWrapper(aSelf.addon),
  5944                            aInstall.wrapper);
  5946       else {
  5947         aSelf.callListener("onNoUpdateAvailable", createWrapper(aSelf.addon));
  5949       aSelf.callListener("onUpdateFinished", createWrapper(aSelf.addon),
  5950                          AddonManager.UPDATE_STATUS_NO_ERROR);
  5953     let compatOverrides = AddonManager.strictCompatibility ?
  5954                             null :
  5955                             this.addon.compatibilityOverrides;
  5957     let update = AUC.getNewestCompatibleUpdate(aUpdates,
  5958                                                this.appVersion,
  5959                                                this.platformVersion,
  5960                                                ignoreMaxVersion,
  5961                                                ignoreStrictCompat,
  5962                                                compatOverrides);
  5964     if (update && Services.vc.compare(this.addon.version, update.version) < 0) {
  5965       for (let currentInstall of XPIProvider.installs) {
  5966         // Skip installs that don't match the available update
  5967         if (currentInstall.existingAddon != this.addon ||
  5968             currentInstall.version != update.version)
  5969           continue;
  5971         // If the existing install has not yet started downloading then send an
  5972         // available update notification. If it is already downloading then
  5973         // don't send any available update notification
  5974         if (currentInstall.state == AddonManager.STATE_AVAILABLE) {
  5975           logger.debug("Found an existing AddonInstall for " + this.addon.id);
  5976           sendUpdateAvailableMessages(this, currentInstall);
  5978         else
  5979           sendUpdateAvailableMessages(this, null);
  5980         return;
  5983       let self = this;
  5984       AddonInstall.createUpdate(function onUpdateCheckComplete_createUpdate(aInstall) {
  5985         sendUpdateAvailableMessages(self, aInstall);
  5986       }, this.addon, update);
  5988     else {
  5989       sendUpdateAvailableMessages(this, null);
  5991   },
  5993   /**
  5994    * Called when AddonUpdateChecker fails the update check
  5996    * @param  aError
  5997    *         An error status
  5998    */
  5999   onUpdateCheckError: function UC_onUpdateCheckError(aError) {
  6000     XPIProvider.done(this.addon._updateCheck);
  6001     this.addon._updateCheck = null;
  6002     this.callListener("onNoCompatibilityUpdateAvailable", createWrapper(this.addon));
  6003     this.callListener("onNoUpdateAvailable", createWrapper(this.addon));
  6004     this.callListener("onUpdateFinished", createWrapper(this.addon), aError);
  6005   },
  6007   /**
  6008    * Called to cancel an in-progress update check
  6009    */
  6010   cancel: function UC_cancel() {
  6011     let parser = this._parser;
  6012     if (parser) {
  6013       this._parser = null;
  6014       // This will call back to onUpdateCheckError with a CANCELLED error
  6015       parser.cancel();
  6018 };
  6020 /**
  6021  * The AddonInternal is an internal only representation of add-ons. It may
  6022  * have come from the database (see DBAddonInternal in XPIProviderUtils.jsm)
  6023  * or an install manifest.
  6024  */
  6025 function AddonInternal() {
  6028 AddonInternal.prototype = {
  6029   _selectedLocale: null,
  6030   active: false,
  6031   visible: false,
  6032   userDisabled: false,
  6033   appDisabled: false,
  6034   softDisabled: false,
  6035   sourceURI: null,
  6036   releaseNotesURI: null,
  6037   foreignInstall: false,
  6039   get selectedLocale() {
  6040     if (this._selectedLocale)
  6041       return this._selectedLocale;
  6042     let locale = findClosestLocale(this.locales);
  6043     this._selectedLocale = locale ? locale : this.defaultLocale;
  6044     return this._selectedLocale;
  6045   },
  6047   get providesUpdatesSecurely() {
  6048     return !!(this.updateKey || !this.updateURL ||
  6049               this.updateURL.substring(0, 6) == "https:");
  6050   },
  6052   get isCompatible() {
  6053     return this.isCompatibleWith();
  6054   },
  6056   get isPlatformCompatible() {
  6057     if (this.targetPlatforms.length == 0)
  6058       return true;
  6060     let matchedOS = false;
  6062     // If any targetPlatform matches the OS and contains an ABI then we will
  6063     // only match a targetPlatform that contains both the current OS and ABI
  6064     let needsABI = false;
  6066     // Some platforms do not specify an ABI, test against null in that case.
  6067     let abi = null;
  6068     try {
  6069       abi = Services.appinfo.XPCOMABI;
  6071     catch (e) { }
  6073     for (let platform of this.targetPlatforms) {
  6074       if (platform.os == Services.appinfo.OS) {
  6075         if (platform.abi) {
  6076           needsABI = true;
  6077           if (platform.abi === abi)
  6078             return true;
  6080         else {
  6081           matchedOS = true;
  6086     return matchedOS && !needsABI;
  6087   },
  6089   isCompatibleWith: function AddonInternal_isCompatibleWith(aAppVersion, aPlatformVersion) {
  6090     // Experiments are installed through an external mechanism that
  6091     // limits target audience to compatible clients. We trust it knows what
  6092     // it's doing and skip compatibility checks.
  6093     //
  6094     // This decision does forfeit defense in depth. If the experiments system
  6095     // is ever wrong about targeting an add-on to a specific application
  6096     // or platform, the client will likely see errors.
  6097     if (this.type == "experiment") {
  6098       return true;
  6101     let app = this.matchingTargetApplication;
  6102     if (!app)
  6103       return false;
  6105     if (!aAppVersion)
  6106       aAppVersion = Services.appinfo.version;
  6107     if (!aPlatformVersion)
  6108       aPlatformVersion = Services.appinfo.platformVersion;
  6110     let version;
  6111     if (app.id == Services.appinfo.ID)
  6112       version = aAppVersion;
  6113     else if (app.id == TOOLKIT_ID)
  6114       version = aPlatformVersion
  6116     // Only extensions and dictionaries can be compatible by default; themes
  6117     // and language packs always use strict compatibility checking.
  6118     if (this.type in COMPATIBLE_BY_DEFAULT_TYPES &&
  6119         !AddonManager.strictCompatibility && !this.strictCompatibility &&
  6120         !this.hasBinaryComponents) {
  6122       // The repository can specify compatibility overrides.
  6123       // Note: For now, only blacklisting is supported by overrides.
  6124       if (this._repositoryAddon &&
  6125           this._repositoryAddon.compatibilityOverrides) {
  6126         let overrides = this._repositoryAddon.compatibilityOverrides;
  6127         let override = AddonRepository.findMatchingCompatOverride(this.version,
  6128                                                                   overrides);
  6129         if (override && override.type == "incompatible")
  6130           return false;
  6133       // Extremely old extensions should not be compatible by default.
  6134       let minCompatVersion;
  6135       if (app.id == Services.appinfo.ID)
  6136         minCompatVersion = XPIProvider.minCompatibleAppVersion;
  6137       else if (app.id == TOOLKIT_ID)
  6138         minCompatVersion = XPIProvider.minCompatiblePlatformVersion;
  6140       if (minCompatVersion &&
  6141           Services.vc.compare(minCompatVersion, app.maxVersion) > 0)
  6142         return false;
  6144       return Services.vc.compare(version, app.minVersion) >= 0;
  6147     return (Services.vc.compare(version, app.minVersion) >= 0) &&
  6148            (Services.vc.compare(version, app.maxVersion) <= 0)
  6149   },
  6151   get matchingTargetApplication() {
  6152     let app = null;
  6153     for (let targetApp of this.targetApplications) {
  6154       if (targetApp.id == Services.appinfo.ID)
  6155         return targetApp;
  6156       if (targetApp.id == TOOLKIT_ID)
  6157         app = targetApp;
  6159     return app;
  6160   },
  6162   get blocklistState() {
  6163     let staticItem = findMatchingStaticBlocklistItem(this);
  6164     if (staticItem)
  6165       return staticItem.level;
  6167     let bs = Cc["@mozilla.org/extensions/blocklist;1"].
  6168              getService(Ci.nsIBlocklistService);
  6169     return bs.getAddonBlocklistState(createWrapper(this));
  6170   },
  6172   get blocklistURL() {
  6173     let staticItem = findMatchingStaticBlocklistItem(this);
  6174     if (staticItem) {
  6175       let url = Services.urlFormatter.formatURLPref("extensions.blocklist.itemURL");
  6176       return url.replace(/%blockID%/g, staticItem.blockID);
  6179     let bs = Cc["@mozilla.org/extensions/blocklist;1"].
  6180              getService(Ci.nsIBlocklistService);
  6181     return bs.getAddonBlocklistURL(createWrapper(this));
  6182   },
  6184   applyCompatibilityUpdate: function AddonInternal_applyCompatibilityUpdate(aUpdate, aSyncCompatibility) {
  6185     this.targetApplications.forEach(function(aTargetApp) {
  6186       aUpdate.targetApplications.forEach(function(aUpdateTarget) {
  6187         if (aTargetApp.id == aUpdateTarget.id && (aSyncCompatibility ||
  6188             Services.vc.compare(aTargetApp.maxVersion, aUpdateTarget.maxVersion) < 0)) {
  6189           aTargetApp.minVersion = aUpdateTarget.minVersion;
  6190           aTargetApp.maxVersion = aUpdateTarget.maxVersion;
  6192       });
  6193     });
  6194     this.appDisabled = !isUsableAddon(this);
  6195   },
  6197   /**
  6198    * toJSON is called by JSON.stringify in order to create a filtered version
  6199    * of this object to be serialized to a JSON file. A new object is returned
  6200    * with copies of all non-private properties. Functions, getters and setters
  6201    * are not copied.
  6203    * @param  aKey
  6204    *         The key that this object is being serialized as in the JSON.
  6205    *         Unused here since this is always the main object serialized
  6207    * @return an object containing copies of the properties of this object
  6208    *         ignoring private properties, functions, getters and setters
  6209    */
  6210   toJSON: function AddonInternal_toJSON(aKey) {
  6211     let obj = {};
  6212     for (let prop in this) {
  6213       // Ignore private properties
  6214       if (prop.substring(0, 1) == "_")
  6215         continue;
  6217       // Ignore getters
  6218       if (this.__lookupGetter__(prop))
  6219         continue;
  6221       // Ignore setters
  6222       if (this.__lookupSetter__(prop))
  6223         continue;
  6225       // Ignore functions
  6226       if (typeof this[prop] == "function")
  6227         continue;
  6229       obj[prop] = this[prop];
  6232     return obj;
  6233   },
  6235   /**
  6236    * When an add-on install is pending its metadata will be cached in a file.
  6237    * This method reads particular properties of that metadata that may be newer
  6238    * than that in the install manifest, like compatibility information.
  6240    * @param  aObj
  6241    *         A JS object containing the cached metadata
  6242    */
  6243   importMetadata: function AddonInternal_importMetaData(aObj) {
  6244     PENDING_INSTALL_METADATA.forEach(function(aProp) {
  6245       if (!(aProp in aObj))
  6246         return;
  6248       this[aProp] = aObj[aProp];
  6249     }, this);
  6251     // Compatibility info may have changed so update appDisabled
  6252     this.appDisabled = !isUsableAddon(this);
  6254 };
  6256 /**
  6257  * Creates an AddonWrapper for an AddonInternal.
  6259  * @param   addon
  6260  *          The AddonInternal to wrap
  6261  * @return  an AddonWrapper or null if addon was null
  6262  */
  6263 function createWrapper(aAddon) {
  6264   if (!aAddon)
  6265     return null;
  6266   if (!aAddon._wrapper) {
  6267     aAddon._hasResourceCache = new Map();
  6268     aAddon._wrapper = new AddonWrapper(aAddon);
  6270   return aAddon._wrapper;
  6273 /**
  6274  * The AddonWrapper wraps an Addon to provide the data visible to consumers of
  6275  * the public API.
  6276  */
  6277 function AddonWrapper(aAddon) {
  6278 #ifdef MOZ_EM_DEBUG
  6279   this.__defineGetter__("__AddonInternal__", function AW_debugGetter() {
  6280     return aAddon;
  6281   });
  6282 #endif
  6284   function chooseValue(aObj, aProp) {
  6285     let repositoryAddon = aAddon._repositoryAddon;
  6286     let objValue = aObj[aProp];
  6288     if (repositoryAddon && (aProp in repositoryAddon) &&
  6289         (objValue === undefined || objValue === null)) {
  6290       return [repositoryAddon[aProp], true];
  6293     return [objValue, false];
  6296   ["id", "syncGUID", "version", "type", "isCompatible", "isPlatformCompatible",
  6297    "providesUpdatesSecurely", "blocklistState", "blocklistURL", "appDisabled",
  6298    "softDisabled", "skinnable", "size", "foreignInstall", "hasBinaryComponents",
  6299    "strictCompatibility", "compatibilityOverrides", "updateURL"].forEach(function(aProp) {
  6300      this.__defineGetter__(aProp, function AddonWrapper_propertyGetter() aAddon[aProp]);
  6301   }, this);
  6303   ["fullDescription", "developerComments", "eula", "supportURL",
  6304    "contributionURL", "contributionAmount", "averageRating", "reviewCount",
  6305    "reviewURL", "totalDownloads", "weeklyDownloads", "dailyUsers",
  6306    "repositoryStatus"].forEach(function(aProp) {
  6307     this.__defineGetter__(aProp, function AddonWrapper_repoPropertyGetter() {
  6308       if (aAddon._repositoryAddon)
  6309         return aAddon._repositoryAddon[aProp];
  6311       return null;
  6312     });
  6313   }, this);
  6315   this.__defineGetter__("aboutURL", function AddonWrapper_aboutURLGetter() {
  6316     return this.isActive ? aAddon["aboutURL"] : null;
  6317   });
  6319   ["installDate", "updateDate"].forEach(function(aProp) {
  6320     this.__defineGetter__(aProp, function AddonWrapper_datePropertyGetter() new Date(aAddon[aProp]));
  6321   }, this);
  6323   ["sourceURI", "releaseNotesURI"].forEach(function(aProp) {
  6324     this.__defineGetter__(aProp, function AddonWrapper_URIPropertyGetter() {
  6325       let [target, fromRepo] = chooseValue(aAddon, aProp);
  6326       if (!target)
  6327         return null;
  6328       if (fromRepo)
  6329         return target;
  6330       return NetUtil.newURI(target);
  6331     });
  6332   }, this);
  6334   this.__defineGetter__("optionsURL", function AddonWrapper_optionsURLGetter() {
  6335     if (this.isActive && aAddon.optionsURL)
  6336       return aAddon.optionsURL;
  6338     if (this.isActive && this.hasResource("options.xul"))
  6339       return this.getResourceURI("options.xul").spec;
  6341     return null;
  6342   }, this);
  6344   this.__defineGetter__("optionsType", function AddonWrapper_optionsTypeGetter() {
  6345     if (!this.isActive)
  6346       return null;
  6348     let hasOptionsXUL = this.hasResource("options.xul");
  6349     let hasOptionsURL = !!this.optionsURL;
  6351     if (aAddon.optionsType) {
  6352       switch (parseInt(aAddon.optionsType, 10)) {
  6353       case AddonManager.OPTIONS_TYPE_DIALOG:
  6354       case AddonManager.OPTIONS_TYPE_TAB:
  6355         return hasOptionsURL ? aAddon.optionsType : null;
  6356       case AddonManager.OPTIONS_TYPE_INLINE:
  6357       case AddonManager.OPTIONS_TYPE_INLINE_INFO:
  6358         return (hasOptionsXUL || hasOptionsURL) ? aAddon.optionsType : null;
  6360       return null;
  6363     if (hasOptionsXUL)
  6364       return AddonManager.OPTIONS_TYPE_INLINE;
  6366     if (hasOptionsURL)
  6367       return AddonManager.OPTIONS_TYPE_DIALOG;
  6369     return null;
  6370   }, this);
  6372   this.__defineGetter__("iconURL", function AddonWrapper_iconURLGetter() {
  6373     return this.icons[32];
  6374   }, this);
  6376   this.__defineGetter__("icon64URL", function AddonWrapper_icon64URLGetter() {
  6377     return this.icons[64];
  6378   }, this);
  6380   this.__defineGetter__("icons", function AddonWrapper_iconsGetter() {
  6381     let icons = {};
  6382     if (aAddon._repositoryAddon) {
  6383       for (let size in aAddon._repositoryAddon.icons) {
  6384         icons[size] = aAddon._repositoryAddon.icons[size];
  6387     if (this.isActive && aAddon.iconURL) {
  6388       icons[32] = aAddon.iconURL;
  6389     } else if (this.hasResource("icon.png")) {
  6390       icons[32] = this.getResourceURI("icon.png").spec;
  6392     if (this.isActive && aAddon.icon64URL) {
  6393       icons[64] = aAddon.icon64URL;
  6394     } else if (this.hasResource("icon64.png")) {
  6395       icons[64] = this.getResourceURI("icon64.png").spec;
  6397     Object.freeze(icons);
  6398     return icons;
  6399   }, this);
  6401   PROP_LOCALE_SINGLE.forEach(function(aProp) {
  6402     this.__defineGetter__(aProp, function AddonWrapper_singleLocaleGetter() {
  6403       // Override XPI creator if repository creator is defined
  6404       if (aProp == "creator" &&
  6405           aAddon._repositoryAddon && aAddon._repositoryAddon.creator) {
  6406         return aAddon._repositoryAddon.creator;
  6409       let result = null;
  6411       if (aAddon.active) {
  6412         try {
  6413           let pref = PREF_EM_EXTENSION_FORMAT + aAddon.id + "." + aProp;
  6414           let value = Services.prefs.getComplexValue(pref,
  6415                                                      Ci.nsIPrefLocalizedString);
  6416           if (value.data)
  6417             result = value.data;
  6419         catch (e) {
  6423       if (result == null)
  6424         [result, ] = chooseValue(aAddon.selectedLocale, aProp);
  6426       if (aProp == "creator")
  6427         return result ? new AddonManagerPrivate.AddonAuthor(result) : null;
  6429       return result;
  6430     });
  6431   }, this);
  6433   PROP_LOCALE_MULTI.forEach(function(aProp) {
  6434     this.__defineGetter__(aProp, function AddonWrapper_multiLocaleGetter() {
  6435       let results = null;
  6436       let usedRepository = false;
  6438       if (aAddon.active) {
  6439         let pref = PREF_EM_EXTENSION_FORMAT + aAddon.id + "." +
  6440                    aProp.substring(0, aProp.length - 1);
  6441         let list = Services.prefs.getChildList(pref, {});
  6442         if (list.length > 0) {
  6443           list.sort();
  6444           results = [];
  6445           list.forEach(function(aPref) {
  6446             let value = Services.prefs.getComplexValue(aPref,
  6447                                                        Ci.nsIPrefLocalizedString);
  6448             if (value.data)
  6449               results.push(value.data);
  6450           });
  6454       if (results == null)
  6455         [results, usedRepository] = chooseValue(aAddon.selectedLocale, aProp);
  6457       if (results && !usedRepository) {
  6458         results = results.map(function mapResult(aResult) {
  6459           return new AddonManagerPrivate.AddonAuthor(aResult);
  6460         });
  6463       return results;
  6464     });
  6465   }, this);
  6467   this.__defineGetter__("screenshots", function AddonWrapper_screenshotsGetter() {
  6468     let repositoryAddon = aAddon._repositoryAddon;
  6469     if (repositoryAddon && ("screenshots" in repositoryAddon)) {
  6470       let repositoryScreenshots = repositoryAddon.screenshots;
  6471       if (repositoryScreenshots && repositoryScreenshots.length > 0)
  6472         return repositoryScreenshots;
  6475     if (aAddon.type == "theme" && this.hasResource("preview.png")) {
  6476       let url = this.getResourceURI("preview.png").spec;
  6477       return [new AddonManagerPrivate.AddonScreenshot(url)];
  6480     return null;
  6481   });
  6483   this.__defineGetter__("applyBackgroundUpdates", function AddonWrapper_applyBackgroundUpdatesGetter() {
  6484     return aAddon.applyBackgroundUpdates;
  6485   });
  6486   this.__defineSetter__("applyBackgroundUpdates", function AddonWrapper_applyBackgroundUpdatesSetter(val) {
  6487     if (this.type == "experiment") {
  6488       logger.warn("Setting applyBackgroundUpdates on an experiment is not supported.");
  6489       return;
  6492     if (val != AddonManager.AUTOUPDATE_DEFAULT &&
  6493         val != AddonManager.AUTOUPDATE_DISABLE &&
  6494         val != AddonManager.AUTOUPDATE_ENABLE) {
  6495       val = val ? AddonManager.AUTOUPDATE_DEFAULT :
  6496                   AddonManager.AUTOUPDATE_DISABLE;
  6499     if (val == aAddon.applyBackgroundUpdates)
  6500       return val;
  6502     XPIDatabase.setAddonProperties(aAddon, {
  6503       applyBackgroundUpdates: val
  6504     });
  6505     AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, ["applyBackgroundUpdates"]);
  6507     return val;
  6508   });
  6510   this.__defineSetter__("syncGUID", function AddonWrapper_syncGUIDGetter(val) {
  6511     if (aAddon.syncGUID == val)
  6512       return val;
  6514     if (aAddon.inDatabase)
  6515       XPIDatabase.setAddonSyncGUID(aAddon, val);
  6517     aAddon.syncGUID = val;
  6519     return val;
  6520   });
  6522   this.__defineGetter__("install", function AddonWrapper_installGetter() {
  6523     if (!("_install" in aAddon) || !aAddon._install)
  6524       return null;
  6525     return aAddon._install.wrapper;
  6526   });
  6528   this.__defineGetter__("pendingUpgrade", function AddonWrapper_pendingUpgradeGetter() {
  6529     return createWrapper(aAddon.pendingUpgrade);
  6530   });
  6532   this.__defineGetter__("scope", function AddonWrapper_scopeGetter() {
  6533     if (aAddon._installLocation)
  6534       return aAddon._installLocation.scope;
  6536     return AddonManager.SCOPE_PROFILE;
  6537   });
  6539   this.__defineGetter__("pendingOperations", function AddonWrapper_pendingOperationsGetter() {
  6540     let pending = 0;
  6541     if (!(aAddon.inDatabase)) {
  6542       // Add-on is pending install if there is no associated install (shouldn't
  6543       // happen here) or if the install is in the process of or has successfully
  6544       // completed the install. If an add-on is pending install then we ignore
  6545       // any other pending operations.
  6546       if (!aAddon._install || aAddon._install.state == AddonManager.STATE_INSTALLING ||
  6547           aAddon._install.state == AddonManager.STATE_INSTALLED)
  6548         return AddonManager.PENDING_INSTALL;
  6550     else if (aAddon.pendingUninstall) {
  6551       // If an add-on is pending uninstall then we ignore any other pending
  6552       // operations
  6553       return AddonManager.PENDING_UNINSTALL;
  6556     if (aAddon.active && isAddonDisabled(aAddon))
  6557       pending |= AddonManager.PENDING_DISABLE;
  6558     else if (!aAddon.active && !isAddonDisabled(aAddon))
  6559       pending |= AddonManager.PENDING_ENABLE;
  6561     if (aAddon.pendingUpgrade)
  6562       pending |= AddonManager.PENDING_UPGRADE;
  6564     return pending;
  6565   });
  6567   this.__defineGetter__("operationsRequiringRestart", function AddonWrapper_operationsRequiringRestartGetter() {
  6568     let ops = 0;
  6569     if (XPIProvider.installRequiresRestart(aAddon))
  6570       ops |= AddonManager.OP_NEEDS_RESTART_INSTALL;
  6571     if (XPIProvider.uninstallRequiresRestart(aAddon))
  6572       ops |= AddonManager.OP_NEEDS_RESTART_UNINSTALL;
  6573     if (XPIProvider.enableRequiresRestart(aAddon))
  6574       ops |= AddonManager.OP_NEEDS_RESTART_ENABLE;
  6575     if (XPIProvider.disableRequiresRestart(aAddon))
  6576       ops |= AddonManager.OP_NEEDS_RESTART_DISABLE;
  6578     return ops;
  6579   });
  6581   this.__defineGetter__("isDebuggable", function AddonWrapper_isDebuggable() {
  6582     return this.isActive && aAddon.bootstrap;
  6583   });
  6585   this.__defineGetter__("permissions", function AddonWrapper_permisionsGetter() {
  6586     let permissions = 0;
  6588     // Add-ons that aren't installed cannot be modified in any way
  6589     if (!(aAddon.inDatabase))
  6590       return permissions;
  6592     // Experiments can only be uninstalled. An uninstall reflects the user
  6593     // intent of "disable this experiment." This is partially managed by the
  6594     // experiments manager.
  6595     if (aAddon.type == "experiment") {
  6596       return AddonManager.PERM_CAN_UNINSTALL;
  6599     if (!aAddon.appDisabled) {
  6600       if (this.userDisabled) {
  6601         permissions |= AddonManager.PERM_CAN_ENABLE;
  6603       else if (aAddon.type != "theme") {
  6604         permissions |= AddonManager.PERM_CAN_DISABLE;
  6608     // Add-ons that are in locked install locations, or are pending uninstall
  6609     // cannot be upgraded or uninstalled
  6610     if (!aAddon._installLocation.locked && !aAddon.pendingUninstall) {
  6611       // Add-ons that are installed by a file link cannot be upgraded
  6612       if (!aAddon._installLocation.isLinkedAddon(aAddon.id)) {
  6613         permissions |= AddonManager.PERM_CAN_UPGRADE;
  6616       permissions |= AddonManager.PERM_CAN_UNINSTALL;
  6619     return permissions;
  6620   });
  6622   this.__defineGetter__("isActive", function AddonWrapper_isActiveGetter() {
  6623     if (Services.appinfo.inSafeMode)
  6624       return false;
  6625     return aAddon.active;
  6626   });
  6628   this.__defineGetter__("userDisabled", function AddonWrapper_userDisabledGetter() {
  6629     if (XPIProvider._enabledExperiments.has(aAddon.id)) {
  6630       return false;
  6633     return aAddon.softDisabled || aAddon.userDisabled;
  6634   });
  6635   this.__defineSetter__("userDisabled", function AddonWrapper_userDisabledSetter(val) {
  6636     if (val == this.userDisabled) {
  6637       return val;
  6640     if (aAddon.type == "experiment") {
  6641       if (val) {
  6642         XPIProvider._enabledExperiments.delete(aAddon.id);
  6643       } else {
  6644         XPIProvider._enabledExperiments.add(aAddon.id);
  6648     if (aAddon.inDatabase) {
  6649       if (aAddon.type == "theme" && val) {
  6650         if (aAddon.internalName == XPIProvider.defaultSkin)
  6651           throw new Error("Cannot disable the default theme");
  6652         XPIProvider.enableDefaultTheme();
  6654       else {
  6655         XPIProvider.updateAddonDisabledState(aAddon, val);
  6658     else {
  6659       aAddon.userDisabled = val;
  6660       // When enabling remove the softDisabled flag
  6661       if (!val)
  6662         aAddon.softDisabled = false;
  6665     return val;
  6666   });
  6668   this.__defineSetter__("softDisabled", function AddonWrapper_softDisabledSetter(val) {
  6669     if (val == aAddon.softDisabled)
  6670       return val;
  6672     if (aAddon.inDatabase) {
  6673       // When softDisabling a theme just enable the active theme
  6674       if (aAddon.type == "theme" && val && !aAddon.userDisabled) {
  6675         if (aAddon.internalName == XPIProvider.defaultSkin)
  6676           throw new Error("Cannot disable the default theme");
  6677         XPIProvider.enableDefaultTheme();
  6679       else {
  6680         XPIProvider.updateAddonDisabledState(aAddon, undefined, val);
  6683     else {
  6684       // Only set softDisabled if not already disabled
  6685       if (!aAddon.userDisabled)
  6686         aAddon.softDisabled = val;
  6689     return val;
  6690   });
  6692   this.isCompatibleWith = function AddonWrapper_isCompatiblewith(aAppVersion, aPlatformVersion) {
  6693     return aAddon.isCompatibleWith(aAppVersion, aPlatformVersion);
  6694   };
  6696   this.uninstall = function AddonWrapper_uninstall() {
  6697     if (!(aAddon.inDatabase))
  6698       throw new Error("Cannot uninstall an add-on that isn't installed");
  6699     if (aAddon.pendingUninstall)
  6700       throw new Error("Add-on is already marked to be uninstalled");
  6701     XPIProvider.uninstallAddon(aAddon);
  6702   };
  6704   this.cancelUninstall = function AddonWrapper_cancelUninstall() {
  6705     if (!(aAddon.inDatabase))
  6706       throw new Error("Cannot cancel uninstall for an add-on that isn't installed");
  6707     if (!aAddon.pendingUninstall)
  6708       throw new Error("Add-on is not marked to be uninstalled");
  6709     XPIProvider.cancelUninstallAddon(aAddon);
  6710   };
  6712   this.findUpdates = function AddonWrapper_findUpdates(aListener, aReason, aAppVersion, aPlatformVersion) {
  6713     // Short-circuit updates for experiments because updates are handled
  6714     // through the Experiments Manager.
  6715     if (this.type == "experiment") {
  6716       AddonManagerPrivate.callNoUpdateListeners(this, aListener, aReason,
  6717                                                 aAppVersion, aPlatformVersion);
  6718       return;
  6721     new UpdateChecker(aAddon, aListener, aReason, aAppVersion, aPlatformVersion);
  6722   };
  6724   // Returns true if there was an update in progress, false if there was no update to cancel
  6725   this.cancelUpdate = function AddonWrapper_cancelUpdate() {
  6726     if (aAddon._updateCheck) {
  6727       aAddon._updateCheck.cancel();
  6728       return true;
  6730     return false;
  6731   };
  6733   this.hasResource = function AddonWrapper_hasResource(aPath) {
  6734     if (aAddon._hasResourceCache.has(aPath))
  6735       return aAddon._hasResourceCache.get(aPath);
  6737     let bundle = aAddon._sourceBundle.clone();
  6739     // Bundle may not exist any more if the addon has just been uninstalled,
  6740     // but explicitly first checking .exists() results in unneeded file I/O.
  6741     try {
  6742       var isDir = bundle.isDirectory();
  6743     } catch (e) {
  6744       aAddon._hasResourceCache.set(aPath, false);
  6745       return false;
  6748     if (isDir) {
  6749       if (aPath) {
  6750         aPath.split("/").forEach(function(aPart) {
  6751           bundle.append(aPart);
  6752         });
  6754       let result = bundle.exists();
  6755       aAddon._hasResourceCache.set(aPath, result);
  6756       return result;
  6759     let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].
  6760                     createInstance(Ci.nsIZipReader);
  6761     try {
  6762       zipReader.open(bundle);
  6763       let result = zipReader.hasEntry(aPath);
  6764       aAddon._hasResourceCache.set(aPath, result);
  6765       return result;
  6767     catch (e) {
  6768       aAddon._hasResourceCache.set(aPath, false);
  6769       return false;
  6771     finally {
  6772       zipReader.close();
  6774   },
  6776   /**
  6777    * Returns a URI to the selected resource or to the add-on bundle if aPath
  6778    * is null. URIs to the bundle will always be file: URIs. URIs to resources
  6779    * will be file: URIs if the add-on is unpacked or jar: URIs if the add-on is
  6780    * still an XPI file.
  6782    * @param  aPath
  6783    *         The path in the add-on to get the URI for or null to get a URI to
  6784    *         the file or directory the add-on is installed as.
  6785    * @return an nsIURI
  6786    */
  6787   this.getResourceURI = function AddonWrapper_getResourceURI(aPath) {
  6788     if (!aPath)
  6789       return NetUtil.newURI(aAddon._sourceBundle);
  6791     return getURIForResourceInFile(aAddon._sourceBundle, aPath);
  6795 /**
  6796  * An object which identifies a directory install location for add-ons. The
  6797  * location consists of a directory which contains the add-ons installed in the
  6798  * location.
  6800  * Each add-on installed in the location is either a directory containing the
  6801  * add-on's files or a text file containing an absolute path to the directory
  6802  * containing the add-ons files. The directory or text file must have the same
  6803  * name as the add-on's ID.
  6805  * There may also a special directory named "staged" which can contain
  6806  * directories with the same name as an add-on ID. If the directory is empty
  6807  * then it means the add-on will be uninstalled from this location during the
  6808  * next startup. If the directory contains the add-on's files then they will be
  6809  * installed during the next startup.
  6811  * @param  aName
  6812  *         The string identifier for the install location
  6813  * @param  aDirectory
  6814  *         The nsIFile directory for the install location
  6815  * @param  aScope
  6816  *         The scope of add-ons installed in this location
  6817  * @param  aLocked
  6818  *         true if add-ons cannot be installed, uninstalled or upgraded in the
  6819  *         install location
  6820  */
  6821 function DirectoryInstallLocation(aName, aDirectory, aScope, aLocked) {
  6822   this._name = aName;
  6823   this.locked = aLocked;
  6824   this._directory = aDirectory;
  6825   this._scope = aScope
  6826   this._IDToFileMap = {};
  6827   this._FileToIDMap = {};
  6828   this._linkedAddons = [];
  6829   this._stagingDirLock = 0;
  6831   if (!aDirectory.exists())
  6832     return;
  6833   if (!aDirectory.isDirectory())
  6834     throw new Error("Location must be a directory.");
  6836   this._readAddons();
  6839 DirectoryInstallLocation.prototype = {
  6840   _name       : "",
  6841   _directory   : null,
  6842   _IDToFileMap : null,  // mapping from add-on ID to nsIFile
  6843   _FileToIDMap : null,  // mapping from add-on path to add-on ID
  6845   /**
  6846    * Reads a directory linked to in a file.
  6848    * @param   file
  6849    *          The file containing the directory path
  6850    * @return  An nsIFile object representing the linked directory.
  6851    */
  6852   _readDirectoryFromFile: function DirInstallLocation__readDirectoryFromFile(aFile) {
  6853     let fis = Cc["@mozilla.org/network/file-input-stream;1"].
  6854               createInstance(Ci.nsIFileInputStream);
  6855     fis.init(aFile, -1, -1, false);
  6856     let line = { value: "" };
  6857     if (fis instanceof Ci.nsILineInputStream)
  6858       fis.readLine(line);
  6859     fis.close();
  6860     if (line.value) {
  6861       let linkedDirectory = Cc["@mozilla.org/file/local;1"].
  6862                             createInstance(Ci.nsIFile);
  6864       try {
  6865         linkedDirectory.initWithPath(line.value);
  6867       catch (e) {
  6868         linkedDirectory.setRelativeDescriptor(aFile.parent, line.value);
  6871       if (!linkedDirectory.exists()) {
  6872         logger.warn("File pointer " + aFile.path + " points to " + linkedDirectory.path +
  6873              " which does not exist");
  6874         return null;
  6877       if (!linkedDirectory.isDirectory()) {
  6878         logger.warn("File pointer " + aFile.path + " points to " + linkedDirectory.path +
  6879              " which is not a directory");
  6880         return null;
  6883       return linkedDirectory;
  6886     logger.warn("File pointer " + aFile.path + " does not contain a path");
  6887     return null;
  6888   },
  6890   /**
  6891    * Finds all the add-ons installed in this location.
  6892    */
  6893   _readAddons: function DirInstallLocation__readAddons() {
  6894     // Use a snapshot of the directory contents to avoid possible issues with
  6895     // iterating over a directory while removing files from it (the YAFFS2
  6896     // embedded filesystem has this issue, see bug 772238).
  6897     let entries = getDirectoryEntries(this._directory);
  6898     for (let entry of entries) {
  6899       let id = entry.leafName;
  6901       if (id == DIR_STAGE || id == DIR_XPI_STAGE || id == DIR_TRASH)
  6902         continue;
  6904       let directLoad = false;
  6905       if (entry.isFile() &&
  6906           id.substring(id.length - 4).toLowerCase() == ".xpi") {
  6907         directLoad = true;
  6908         id = id.substring(0, id.length - 4);
  6911       if (!gIDTest.test(id)) {
  6912         logger.debug("Ignoring file entry whose name is not a valid add-on ID: " +
  6913              entry.path);
  6914         continue;
  6917       if (entry.isFile() && !directLoad) {
  6918         let newEntry = this._readDirectoryFromFile(entry);
  6919         if (!newEntry) {
  6920           logger.debug("Deleting stale pointer file " + entry.path);
  6921           try {
  6922             entry.remove(true);
  6924           catch (e) {
  6925             logger.warn("Failed to remove stale pointer file " + entry.path, e);
  6926             // Failing to remove the stale pointer file is ignorable
  6928           continue;
  6931         entry = newEntry;
  6932         this._linkedAddons.push(id);
  6935       this._IDToFileMap[id] = entry;
  6936       this._FileToIDMap[entry.path] = id;
  6938   },
  6940   /**
  6941    * Gets the name of this install location.
  6942    */
  6943   get name() {
  6944     return this._name;
  6945   },
  6947   /**
  6948    * Gets the scope of this install location.
  6949    */
  6950   get scope() {
  6951     return this._scope;
  6952   },
  6954   /**
  6955    * Gets an array of nsIFiles for add-ons installed in this location.
  6956    */
  6957   get addonLocations() {
  6958     let locations = [];
  6959     for (let id in this._IDToFileMap) {
  6960       locations.push(this._IDToFileMap[id].clone());
  6962     return locations;
  6963   },
  6965   /**
  6966    * Gets the staging directory to put add-ons that are pending install and
  6967    * uninstall into.
  6969    * @return an nsIFile
  6970    */
  6971   getStagingDir: function DirInstallLocation_getStagingDir() {
  6972     let dir = this._directory.clone();
  6973     dir.append(DIR_STAGE);
  6974     return dir;
  6975   },
  6977   requestStagingDir: function() {
  6978     this._stagingDirLock++;
  6980     if (this._stagingDirPromise)
  6981       return this._stagingDirPromise;
  6983     OS.File.makeDir(this._directory.path);
  6984     let stagepath = OS.Path.join(this._directory.path, DIR_STAGE);
  6985     return this._stagingDirPromise = OS.File.makeDir(stagepath).then(null, (e) => {
  6986       if (e instanceof OS.File.Error && e.becauseExists)
  6987         return;
  6988       logger.error("Failed to create staging directory", e);
  6989       throw e;
  6990     });
  6991   },
  6993   releaseStagingDir: function() {
  6994     this._stagingDirLock--;
  6996     if (this._stagingDirLock == 0) {
  6997       this._stagingDirPromise = null;
  6998       this.cleanStagingDir();
  7001     return Promise.resolve();
  7002   },
  7004   /**
  7005    * Removes the specified files or directories in the staging directory and
  7006    * then if the staging directory is empty attempts to remove it.
  7008    * @param  aLeafNames
  7009    *         An array of file or directory to remove from the directory, the
  7010    *         array may be empty
  7011    */
  7012   cleanStagingDir: function(aLeafNames = []) {
  7013     let dir = this.getStagingDir();
  7015     for (let name of aLeafNames) {
  7016       let file = dir.clone();
  7017       file.append(name);
  7018       recursiveRemove(file);
  7021     if (this._stagingDirLock > 0)
  7022       return;
  7024     let dirEntries = dir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
  7025     try {
  7026       if (dirEntries.nextFile)
  7027         return;
  7029     finally {
  7030       dirEntries.close();
  7033     try {
  7034       setFilePermissions(dir, FileUtils.PERMS_DIRECTORY);
  7035       dir.remove(false);
  7037     catch (e) {
  7038       logger.warn("Failed to remove staging dir", e);
  7039       // Failing to remove the staging directory is ignorable
  7041   },
  7043   /**
  7044    * Gets the directory used by old versions for staging XPI and JAR files ready
  7045    * to be installed.
  7047    * @return an nsIFile
  7048    */
  7049   getXPIStagingDir: function DirInstallLocation_getXPIStagingDir() {
  7050     let dir = this._directory.clone();
  7051     dir.append(DIR_XPI_STAGE);
  7052     return dir;
  7053   },
  7055   /**
  7056    * Returns a directory that is normally on the same filesystem as the rest of
  7057    * the install location and can be used for temporarily storing files during
  7058    * safe move operations. Calling this method will delete the existing trash
  7059    * directory and its contents.
  7061    * @return an nsIFile
  7062    */
  7063   getTrashDir: function DirInstallLocation_getTrashDir() {
  7064     let trashDir = this._directory.clone();
  7065     trashDir.append(DIR_TRASH);
  7066     if (trashDir.exists())
  7067       recursiveRemove(trashDir);
  7068     trashDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
  7069     return trashDir;
  7070   },
  7072   /**
  7073    * Installs an add-on into the install location.
  7075    * @param  aId
  7076    *         The ID of the add-on to install
  7077    * @param  aSource
  7078    *         The source nsIFile to install from
  7079    * @param  aExistingAddonID
  7080    *         The ID of an existing add-on to uninstall at the same time
  7081    * @param  aCopy
  7082    *         If false the source files will be moved to the new location,
  7083    *         otherwise they will only be copied
  7084    * @return an nsIFile indicating where the add-on was installed to
  7085    */
  7086   installAddon: function DirInstallLocation_installAddon(aId, aSource,
  7087                                                          aExistingAddonID,
  7088                                                          aCopy) {
  7089     let trashDir = this.getTrashDir();
  7091     let transaction = new SafeInstallOperation();
  7093     let self = this;
  7094     function moveOldAddon(aId) {
  7095       let file = self._directory.clone();
  7096       file.append(aId);
  7098       if (file.exists())
  7099         transaction.move(file, trashDir);
  7101       file = self._directory.clone();
  7102       file.append(aId + ".xpi");
  7103       if (file.exists()) {
  7104         flushJarCache(file);
  7105         transaction.move(file, trashDir);
  7109     // If any of these operations fails the finally block will clean up the
  7110     // temporary directory
  7111     try {
  7112       moveOldAddon(aId);
  7113       if (aExistingAddonID && aExistingAddonID != aId)
  7114         moveOldAddon(aExistingAddonID);
  7116       if (aCopy) {
  7117         transaction.copy(aSource, this._directory);
  7119       else {
  7120         if (aSource.isFile())
  7121           flushJarCache(aSource);
  7123         transaction.move(aSource, this._directory);
  7126     finally {
  7127       // It isn't ideal if this cleanup fails but it isn't worth rolling back
  7128       // the install because of it.
  7129       try {
  7130         recursiveRemove(trashDir);
  7132       catch (e) {
  7133         logger.warn("Failed to remove trash directory when installing " + aId, e);
  7137     let newFile = this._directory.clone();
  7138     newFile.append(aSource.leafName);
  7139     try {
  7140       newFile.lastModifiedTime = Date.now();
  7141     } catch (e)  {
  7142       logger.warn("failed to set lastModifiedTime on " + newFile.path, e);
  7144     this._FileToIDMap[newFile.path] = aId;
  7145     this._IDToFileMap[aId] = newFile;
  7147     if (aExistingAddonID && aExistingAddonID != aId &&
  7148         aExistingAddonID in this._IDToFileMap) {
  7149       delete this._FileToIDMap[this._IDToFileMap[aExistingAddonID]];
  7150       delete this._IDToFileMap[aExistingAddonID];
  7153     return newFile;
  7154   },
  7156   /**
  7157    * Uninstalls an add-on from this location.
  7159    * @param  aId
  7160    *         The ID of the add-on to uninstall
  7161    * @throws if the ID does not match any of the add-ons installed
  7162    */
  7163   uninstallAddon: function DirInstallLocation_uninstallAddon(aId) {
  7164     let file = this._IDToFileMap[aId];
  7165     if (!file) {
  7166       logger.warn("Attempted to remove " + aId + " from " +
  7167            this._name + " but it was already gone");
  7168       return;
  7171     file = this._directory.clone();
  7172     file.append(aId);
  7173     if (!file.exists())
  7174       file.leafName += ".xpi";
  7176     if (!file.exists()) {
  7177       logger.warn("Attempted to remove " + aId + " from " +
  7178            this._name + " but it was already gone");
  7180       delete this._FileToIDMap[file.path];
  7181       delete this._IDToFileMap[aId];
  7182       return;
  7185     let trashDir = this.getTrashDir();
  7187     if (file.leafName != aId) {
  7188       logger.debug("uninstallAddon: flushing jar cache " + file.path + " for addon " + aId);
  7189       flushJarCache(file);
  7192     let transaction = new SafeInstallOperation();
  7194     try {
  7195       transaction.move(file, trashDir);
  7197     finally {
  7198       // It isn't ideal if this cleanup fails, but it is probably better than
  7199       // rolling back the uninstall at this point
  7200       try {
  7201         recursiveRemove(trashDir);
  7203       catch (e) {
  7204         logger.warn("Failed to remove trash directory when uninstalling " + aId, e);
  7208     delete this._FileToIDMap[file.path];
  7209     delete this._IDToFileMap[aId];
  7210   },
  7212   /**
  7213    * Gets the ID of the add-on installed in the given nsIFile.
  7215    * @param  aFile
  7216    *         The nsIFile to look in
  7217    * @return the ID
  7218    * @throws if the file does not represent an installed add-on
  7219    */
  7220   getIDForLocation: function DirInstallLocation_getIDForLocation(aFile) {
  7221     if (aFile.path in this._FileToIDMap)
  7222       return this._FileToIDMap[aFile.path];
  7223     throw new Error("Unknown add-on location " + aFile.path);
  7224   },
  7226   /**
  7227    * Gets the directory that the add-on with the given ID is installed in.
  7229    * @param  aId
  7230    *         The ID of the add-on
  7231    * @return The nsIFile
  7232    * @throws if the ID does not match any of the add-ons installed
  7233    */
  7234   getLocationForID: function DirInstallLocation_getLocationForID(aId) {
  7235     if (aId in this._IDToFileMap)
  7236       return this._IDToFileMap[aId].clone();
  7237     throw new Error("Unknown add-on ID " + aId);
  7238   },
  7240   /**
  7241    * Returns true if the given addon was installed in this location by a text
  7242    * file pointing to its real path.
  7244    * @param aId
  7245    *        The ID of the addon
  7246    */
  7247   isLinkedAddon: function DirInstallLocation__isLinkedAddon(aId) {
  7248     return this._linkedAddons.indexOf(aId) != -1;
  7250 };
  7252 #ifdef XP_WIN
  7253 /**
  7254  * An object that identifies a registry install location for add-ons. The location
  7255  * consists of a registry key which contains string values mapping ID to the
  7256  * path where an add-on is installed
  7258  * @param  aName
  7259  *         The string identifier of this Install Location.
  7260  * @param  aRootKey
  7261  *         The root key (one of the ROOT_KEY_ values from nsIWindowsRegKey).
  7262  * @param  scope
  7263  *         The scope of add-ons installed in this location
  7264  */
  7265 function WinRegInstallLocation(aName, aRootKey, aScope) {
  7266   this.locked = true;
  7267   this._name = aName;
  7268   this._rootKey = aRootKey;
  7269   this._scope = aScope;
  7270   this._IDToFileMap = {};
  7271   this._FileToIDMap = {};
  7273   let path = this._appKeyPath + "\\Extensions";
  7274   let key = Cc["@mozilla.org/windows-registry-key;1"].
  7275             createInstance(Ci.nsIWindowsRegKey);
  7277   // Reading the registry may throw an exception, and that's ok.  In error
  7278   // cases, we just leave ourselves in the empty state.
  7279   try {
  7280     key.open(this._rootKey, path, Ci.nsIWindowsRegKey.ACCESS_READ);
  7282   catch (e) {
  7283     return;
  7286   this._readAddons(key);
  7287   key.close();
  7290 WinRegInstallLocation.prototype = {
  7291   _name       : "",
  7292   _rootKey    : null,
  7293   _scope      : null,
  7294   _IDToFileMap : null,  // mapping from ID to nsIFile
  7295   _FileToIDMap : null,  // mapping from path to ID
  7297   /**
  7298    * Retrieves the path of this Application's data key in the registry.
  7299    */
  7300   get _appKeyPath() {
  7301     let appVendor = Services.appinfo.vendor;
  7302     let appName = Services.appinfo.name;
  7304 #ifdef MOZ_THUNDERBIRD
  7305     // XXX Thunderbird doesn't specify a vendor string
  7306     if (appVendor == "")
  7307       appVendor = "Mozilla";
  7308 #endif
  7310     // XULRunner-based apps may intentionally not specify a vendor
  7311     if (appVendor != "")
  7312       appVendor += "\\";
  7314     return "SOFTWARE\\" + appVendor + appName;
  7315   },
  7317   /**
  7318    * Read the registry and build a mapping between ID and path for each
  7319    * installed add-on.
  7321    * @param  key
  7322    *         The key that contains the ID to path mapping
  7323    */
  7324   _readAddons: function RegInstallLocation__readAddons(aKey) {
  7325     let count = aKey.valueCount;
  7326     for (let i = 0; i < count; ++i) {
  7327       let id = aKey.getValueName(i);
  7329       let file = Cc["@mozilla.org/file/local;1"].
  7330                 createInstance(Ci.nsIFile);
  7331       file.initWithPath(aKey.readStringValue(id));
  7333       if (!file.exists()) {
  7334         logger.warn("Ignoring missing add-on in " + file.path);
  7335         continue;
  7338       this._IDToFileMap[id] = file;
  7339       this._FileToIDMap[file.path] = id;
  7341   },
  7343   /**
  7344    * Gets the name of this install location.
  7345    */
  7346   get name() {
  7347     return this._name;
  7348   },
  7350   /**
  7351    * Gets the scope of this install location.
  7352    */
  7353   get scope() {
  7354     return this._scope;
  7355   },
  7357   /**
  7358    * Gets an array of nsIFiles for add-ons installed in this location.
  7359    */
  7360   get addonLocations() {
  7361     let locations = [];
  7362     for (let id in this._IDToFileMap) {
  7363       locations.push(this._IDToFileMap[id].clone());
  7365     return locations;
  7366   },
  7368   /**
  7369    * Gets the ID of the add-on installed in the given nsIFile.
  7371    * @param  aFile
  7372    *         The nsIFile to look in
  7373    * @return the ID
  7374    * @throws if the file does not represent an installed add-on
  7375    */
  7376   getIDForLocation: function RegInstallLocation_getIDForLocation(aFile) {
  7377     if (aFile.path in this._FileToIDMap)
  7378       return this._FileToIDMap[aFile.path];
  7379     throw new Error("Unknown add-on location");
  7380   },
  7382   /**
  7383    * Gets the nsIFile that the add-on with the given ID is installed in.
  7385    * @param  aId
  7386    *         The ID of the add-on
  7387    * @return the nsIFile
  7388    */
  7389   getLocationForID: function RegInstallLocation_getLocationForID(aId) {
  7390     if (aId in this._IDToFileMap)
  7391       return this._IDToFileMap[aId].clone();
  7392     throw new Error("Unknown add-on ID");
  7393   },
  7395   /**
  7396    * @see DirectoryInstallLocation
  7397    */
  7398   isLinkedAddon: function RegInstallLocation_isLinkedAddon(aId) {
  7399     return true;
  7401 };
  7402 #endif
  7404 let addonTypes = [
  7405   new AddonManagerPrivate.AddonType("extension", URI_EXTENSION_STRINGS,
  7406                                     STRING_TYPE_NAME,
  7407                                     AddonManager.VIEW_TYPE_LIST, 4000),
  7408   new AddonManagerPrivate.AddonType("theme", URI_EXTENSION_STRINGS,
  7409                                     STRING_TYPE_NAME,
  7410                                     AddonManager.VIEW_TYPE_LIST, 5000),
  7411   new AddonManagerPrivate.AddonType("dictionary", URI_EXTENSION_STRINGS,
  7412                                     STRING_TYPE_NAME,
  7413                                     AddonManager.VIEW_TYPE_LIST, 7000,
  7414                                     AddonManager.TYPE_UI_HIDE_EMPTY),
  7415   new AddonManagerPrivate.AddonType("locale", URI_EXTENSION_STRINGS,
  7416                                     STRING_TYPE_NAME,
  7417                                     AddonManager.VIEW_TYPE_LIST, 8000,
  7418                                     AddonManager.TYPE_UI_HIDE_EMPTY),
  7419 ];
  7421 // We only register experiments support if the application supports them.
  7422 // Ideally, we would install an observer to watch the pref. Installing
  7423 // an observer for this pref is not necessary here and may be buggy with
  7424 // regards to registering this XPIProvider twice.
  7425 if (Prefs.getBoolPref("experiments.supported", false)) {
  7426   addonTypes.push(
  7427     new AddonManagerPrivate.AddonType("experiment",
  7428                                       URI_EXTENSION_STRINGS,
  7429                                       STRING_TYPE_NAME,
  7430                                       AddonManager.VIEW_TYPE_LIST, 11000,
  7431                                       AddonManager.TYPE_UI_HIDE_EMPTY));
  7434 AddonManagerPrivate.registerProvider(XPIProvider, addonTypes);

mercurial