CLOSED TREE: TraceMonkey merge head. (a=blockers)
[mozilla-central.git] / toolkit / mozapps / extensions / XPIProvider.jsm
blobe0f24e7aa8ae670e5581b8bd4f29f403e8b65f2d
1 /*
2 # ***** BEGIN LICENSE BLOCK *****
3 # Version: MPL 1.1/GPL 2.0/LGPL 2.1
5 # The contents of this file are subject to the Mozilla Public License Version
6 # 1.1 (the "License"); you may not use this file except in compliance with
7 # the License. You may obtain a copy of the License at
8 # http://www.mozilla.org/MPL/
10 # Software distributed under the License is distributed on an "AS IS" basis,
11 # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
12 # for the specific language governing rights and limitations under the
13 # License.
15 # The Original Code is the Extension Manager.
17 # The Initial Developer of the Original Code is
18 # the Mozilla Foundation.
19 # Portions created by the Initial Developer are Copyright (C) 2009
20 # the Initial Developer. All Rights Reserved.
22 # Contributor(s):
23 #   Dave Townsend <dtownsend@oxymoronical.com>
25 # Alternatively, the contents of this file may be used under the terms of
26 # either the GNU General Public License Version 2 or later (the "GPL"), or
27 # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
28 # in which case the provisions of the GPL or the LGPL are applicable instead
29 # of those above. If you wish to allow use of your version of this file only
30 # under the terms of either the GPL or the LGPL, and not to allow others to
31 # use your version of this file under the terms of the MPL, indicate your
32 # decision by deleting the provisions above and replace them with the notice
33 # and other provisions required by the GPL or the LGPL. If you do not delete
34 # the provisions above, a recipient may use your version of this file under
35 # the terms of any one of the MPL, the GPL or the LGPL.
37 # ***** END LICENSE BLOCK *****
40 const Cc = Components.classes;
41 const Ci = Components.interfaces;
42 const Cr = Components.results;
44 var EXPORTED_SYMBOLS = [];
46 Components.utils.import("resource://gre/modules/Services.jsm");
47 Components.utils.import("resource://gre/modules/AddonManager.jsm");
48 Components.utils.import("resource://gre/modules/AddonRepository.jsm");
49 Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm");
50 Components.utils.import("resource://gre/modules/FileUtils.jsm");
51 Components.utils.import("resource://gre/modules/NetUtil.jsm");
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_CHECK_COMPATIBILITY     = "extensions.checkCompatibility";
64 const PREF_EM_CHECK_UPDATE_SECURITY   = "extensions.checkUpdateSecurity";
65 const PREF_EM_UPDATE_URL              = "extensions.update.url";
66 const PREF_EM_ENABLED_ADDONS          = "extensions.enabledAddons";
67 const PREF_EM_EXTENSION_FORMAT        = "extensions.";
68 const PREF_EM_ENABLED_SCOPES          = "extensions.enabledScopes";
69 const PREF_EM_SHOW_MISMATCH_UI        = "extensions.showMismatchUI";
70 const PREF_EM_DISABLED_ADDONS_LIST    = "extensions.disabledAddons";
71 const PREF_XPI_ENABLED                = "xpinstall.enabled";
72 const PREF_XPI_WHITELIST_REQUIRED     = "xpinstall.whitelist.required";
73 const PREF_XPI_WHITELIST_PERMISSIONS  = "xpinstall.whitelist.add";
74 const PREF_XPI_BLACKLIST_PERMISSIONS  = "xpinstall.blacklist.add";
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.";
80 const URI_EXTENSION_UPDATE_DIALOG     = "chrome://mozapps/content/extensions/update.xul";
82 const DIR_EXTENSIONS                  = "extensions";
83 const DIR_STAGE                       = "staged";
84 const DIR_XPI_STAGE                   = "staged-xpis";
85 const DIR_TRASH                       = "trash";
87 const FILE_OLD_DATABASE               = "extensions.rdf";
88 const FILE_OLD_CACHE                  = "extensions.cache";
89 const FILE_DATABASE                   = "extensions.sqlite";
90 const FILE_INSTALL_MANIFEST           = "install.rdf";
91 const FILE_XPI_ADDONS_LIST            = "extensions.ini";
93 const KEY_PROFILEDIR                  = "ProfD";
94 const KEY_APPDIR                      = "XCurProcD";
95 const KEY_TEMPDIR                     = "TmpD";
96 const KEY_APP_DISTRIBUTION            = "XREAppDist";
98 const KEY_APP_PROFILE                 = "app-profile";
99 const KEY_APP_GLOBAL                  = "app-global";
100 const KEY_APP_SYSTEM_LOCAL            = "app-system-local";
101 const KEY_APP_SYSTEM_SHARE            = "app-system-share";
102 const KEY_APP_SYSTEM_USER             = "app-system-user";
104 const CATEGORY_UPDATE_PARAMS          = "extension-update-params";
106 const UNKNOWN_XPCOM_ABI               = "unknownABI";
107 const XPI_PERMISSION                  = "install";
109 const PREFIX_ITEM_URI                 = "urn:mozilla:item:";
110 const RDFURI_ITEM_ROOT                = "urn:mozilla:item:root"
111 const RDFURI_INSTALL_MANIFEST_ROOT    = "urn:mozilla:install-manifest";
112 const PREFIX_NS_EM                    = "http://www.mozilla.org/2004/em-rdf#";
114 const TOOLKIT_ID                      = "toolkit@mozilla.org";
116 const BRANCH_REGEXP                   = /^([^\.]+\.[0-9]+[a-z]*).*/gi;
118 const DB_SCHEMA                       = 3;
119 const REQ_VERSION                     = 2;
121 // Properties that exist in the install manifest
122 const PROP_METADATA      = ["id", "version", "type", "internalName", "updateURL",
123                             "updateKey", "optionsURL", "aboutURL", "iconURL",
124                             "icon64URL"];
125 const PROP_LOCALE_SINGLE = ["name", "description", "creator", "homepageURL"];
126 const PROP_LOCALE_MULTI  = ["developers", "translators", "contributors"];
127 const PROP_TARGETAPP     = ["id", "minVersion", "maxVersion"];
129 // Properties that only exist in the database
130 const DB_METADATA        = ["installDate", "updateDate", "size", "sourceURI",
131                             "releaseNotesURI", "applyBackgroundUpdates"];
132 const DB_BOOL_METADATA   = ["visible", "active", "userDisabled", "appDisabled",
133                             "pendingUninstall", "bootstrap", "skinnable"];
135 const BOOTSTRAP_REASONS = {
136   APP_STARTUP     : 1,
137   APP_SHUTDOWN    : 2,
138   ADDON_ENABLE    : 3,
139   ADDON_DISABLE   : 4,
140   ADDON_INSTALL   : 5,
141   ADDON_UNINSTALL : 6,
142   ADDON_UPGRADE   : 7,
143   ADDON_DOWNGRADE : 8
146 // Map new string type identifiers to old style nsIUpdateItem types
147 const TYPES = {
148   extension: 2,
149   theme: 4,
150   locale: 8,
151   multipackage: 32
155  * Valid IDs fit this pattern.
156  */
157 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;
159 ["LOG", "WARN", "ERROR"].forEach(function(aName) {
160   this.__defineGetter__(aName, function() {
161     Components.utils.import("resource://gre/modules/AddonLogging.jsm");
163     LogManager.getLogger("addons.xpi", this);
164     return this[aName];
165   })
166 }, this);
169  * A safe way to install a file or the contents of a directory to a new
170  * directory. The file or directory is moved or copied recursively and if
171  * anything fails an attempt is made to rollback the entire operation. The
172  * operation may also be rolled back to its original state after it has
173  * completed by calling the rollback method.
175  * Operations can be chained. Calling move or copy multiple times will remember
176  * the whole set and if one fails all of the operations will be rolled back.
177  */
178 function SafeInstallOperation() {
179   this._installedFiles = [];
180   this._createdDirs = [];
183 SafeInstallOperation.prototype = {
184   _installedFiles: null,
185   _createdDirs: null,
187   _installFile: function(aFile, aTargetDirectory, aCopy) {
188     let oldFile = aCopy ? null : aFile.clone();
189     let newFile = aFile.clone();
190     try {
191       if (aCopy)
192         newFile.copyTo(aTargetDirectory, null);
193       else
194         newFile.moveTo(aTargetDirectory, null);
195     }
196     catch (e) {
197       ERROR("Failed to " + (aCopy ? "copy" : "move") + " file " + aFile.path +
198             " to " + aTargetDirectory.path, e);
199       throw e;
200     }
201     this._installedFiles.push({ oldFile: null, newFile: newFile });
202   },
204   _installDirectory: function(aDirectory, aTargetDirectory, aCopy) {
205     let newDir = aTargetDirectory.clone();
206     newDir.append(aDirectory.leafName);
207     try {
208       newDir.create(Ci.nsILocalFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
209     }
210     catch (e) {
211       ERROR("Failed to create directory " + newDir.path, e);
212       throw e;
213     }
214     this._createdDirs.push(newDir);
216     let entries = aDirectory.directoryEntries
217                             .QueryInterface(Ci.nsIDirectoryEnumerator);
218     let cacheEntries = [];
219     try {
220       let entry;
221       while (entry = entries.nextFile)
222         cacheEntries.push(entry);
223     }
224     finally {
225       entries.close();
226     }
228     cacheEntries.forEach(function(aEntry) {
229       try {
230         this._installDirEntry(aEntry, newDir, aCopy);
231       }
232       catch (e) {
233         ERROR("Failed to " + (aCopy ? "copy" : "move") + " entry " +
234               aEntry.path, e);
235         throw e;
236       }
237     }, this);
239     // If this is only a copy operation then there is nothing else to do
240     if (aCopy)
241       return;
243     // The directory should be empty by this point. If it isn't this will throw
244     // and all of the operations will be rolled back
245     try {
246       aDirectory.permissions = FileUtils.PERMS_DIRECTORY;
247       aDirectory.remove(false);
248     }
249     catch (e) {
250       ERROR("Failed to remove directory " + aDirectory.path, e);
251       throw e;
252     }
254     // Note we put the directory move in after all the file moves so the
255     // directory is recreated before all the files are moved back
256     this._installedFiles.push({ oldFile: aDirectory, newFile: newDir });
257   },
259   _installDirEntry: function(aDirEntry, aTargetDirectory, aCopy) {
260     try {
261       if (aDirEntry.isDirectory())
262         this._installDirectory(aDirEntry, aTargetDirectory, aCopy);
263       else
264         this._installFile(aDirEntry, aTargetDirectory, aCopy);
265     }
266     catch (e) {
267       ERROR("Failure " + (aCopy ? "copying" : "moving") + " " + aDirEntry.path +
268             " to " + aTargetDirectory.path);
269       throw e;
270     }
271   },
273   /**
274    * Moves a file or directory into a new directory. If an error occurs then all
275    * files that have been moved will be moved back to their original location.
276    *
277    * @param  aFile
278    *         The file or directory to be moved.
279    * @param  aTargetDirectory
280    *         The directory to move into, this is expected to be an empty
281    *         directory.
282    */
283   move: function(aFile, aTargetDirectory) {
284     try {
285       this._installDirEntry(aFile, aTargetDirectory, false);
286     }
287     catch (e) {
288       this.rollback();
289       throw e;
290     }
291   },
293   /**
294    * Copies a file or directory into a new directory. If an error occurs then
295    * all new files that have been created will be removed.
296    *
297    * @param  aFile
298    *         The file or directory to be copied.
299    * @param  aTargetDirectory
300    *         The directory to copy into, this is expected to be an empty
301    *         directory.
302    */
303   copy: function(aFile, aTargetDirectory) {
304     try {
305       this._installDirEntry(aFile, aTargetDirectory, true);
306     }
307     catch (e) {
308       this.rollback();
309       throw e;
310     }
311   },
313   /**
314    * Rolls back all the moves that this operation performed. If an exception
315    * occurs here then both old and new directories are left in an indeterminate
316    * state
317    */
318   rollback: function() {
319     while (this._installedFiles.length > 0) {
320       let move = this._installedFiles.pop();
321       if (move.newFile.isDirectory()) {
322         let oldDir = move.oldFile.parent.clone();
323         oldDir.append(move.oldFile.leafName);
324         oldDir.create(Ci.nsILocalFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
325       }
326       else if (!move.oldFile) {
327         // No old file means this was a copied file
328         move.newFile.remove(true);
329       }
330       else {
331         move.newFile.moveTo(move.oldFile.parent, null);
332       }
333     }
335     while (this._createdDirs.length > 0)
336       recursiveRemove(this._createdDirs.pop());
337   }
341  * Gets the currently selected locale for display.
342  * @return  the selected locale or "en-US" if none is selected
343  */
344 function getLocale() {
345   if (Prefs.getBoolPref(PREF_MATCH_OS_LOCALE, false))
346     return Services.locale.getLocaleComponentForUserAgent();
347   return Prefs.getCharPref(PREF_SELECTED_LOCALE, "en-US");
351  * Selects the closest matching locale from a list of locales.
353  * @param  aLocales
354  *         An array of locales
355  * @return the best match for the currently selected locale
356  */
357 function findClosestLocale(aLocales) {
358   let appLocale = getLocale();
360   // Holds the best matching localized resource
361   var bestmatch = null;
362   // The number of locale parts it matched with
363   var bestmatchcount = 0;
364   // The number of locale parts in the match
365   var bestpartcount = 0;
367   var matchLocales = [appLocale.toLowerCase()];
368   /* If the current locale is English then it will find a match if there is
369      a valid match for en-US so no point searching that locale too. */
370   if (matchLocales[0].substring(0, 3) != "en-")
371     matchLocales.push("en-us");
373   for each (var locale in matchLocales) {
374     var lparts = locale.split("-");
375     for each (var localized in aLocales) {
376       for each (found in localized.locales) {
377         found = found.toLowerCase();
378         // Exact match is returned immediately
379         if (locale == found)
380           return localized;
382         var fparts = found.split("-");
383         /* If we have found a possible match and this one isn't any longer
384            then we dont need to check further. */
385         if (bestmatch && fparts.length < bestmatchcount)
386           continue;
388         // Count the number of parts that match
389         var maxmatchcount = Math.min(fparts.length, lparts.length);
390         var matchcount = 0;
391         while (matchcount < maxmatchcount &&
392                fparts[matchcount] == lparts[matchcount])
393           matchcount++;
395         /* If we matched more than the last best match or matched the same and
396            this locale is less specific than the last best match. */
397         if (matchcount > bestmatchcount ||
398            (matchcount == bestmatchcount && fparts.length < bestpartcount)) {
399           bestmatch = localized;
400           bestmatchcount = matchcount;
401           bestpartcount = fparts.length;
402         }
403       }
404     }
405     // If we found a valid match for this locale return it
406     if (bestmatch)
407       return bestmatch;
408   }
409   return null;
413  * Calculates whether an add-on should be appDisabled or not.
415  * @param  aAddon
416  *         The add-on to check
417  * @return true if the add-on should not be appDisabled
418  */
419 function isUsableAddon(aAddon) {
420   // Hack to ensure the default theme is always usable
421   if (aAddon.type == "theme" && aAddon.internalName == XPIProvider.defaultSkin)
422     return true;
424   if (aAddon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED)
425     return false;
427   if (XPIProvider.checkUpdateSecurity && !aAddon.providesUpdatesSecurely)
428     return false;
430   if (!aAddon.isPlatformCompatible)
431     return false;
433   if (XPIProvider.checkCompatibility) {
434     if (!aAddon.isCompatible)
435       return false;
436   }
437   else {
438     if (!aAddon.matchingTargetApplication)
439       return false;
440   }
442   return true;
445 this.__defineGetter__("gRDF", function() {
446   delete this.gRDF;
447   return this.gRDF = Cc["@mozilla.org/rdf/rdf-service;1"].
448                      getService(Ci.nsIRDFService);
451 function EM_R(aProperty) {
452   return gRDF.GetResource(PREFIX_NS_EM + aProperty);
456  * Converts an RDF literal, resource or integer into a string.
458  * @param  aLiteral
459  *         The RDF object to convert
460  * @return a string if the object could be converted or null
461  */
462 function getRDFValue(aLiteral) {
463   if (aLiteral instanceof Ci.nsIRDFLiteral)
464     return aLiteral.Value;
465   if (aLiteral instanceof Ci.nsIRDFResource)
466     return aLiteral.Value;
467   if (aLiteral instanceof Ci.nsIRDFInt)
468     return aLiteral.Value;
469   return null;
473  * Gets an RDF property as a string
475  * @param  aDs
476  *         The RDF datasource to read the property from
477  * @param  aResource
478  *         The RDF resource to read the property from
479  * @param  aProperty
480  *         The property to read
481  * @return a string if the property existed or null
482  */
483 function getRDFProperty(aDs, aResource, aProperty) {
484   return getRDFValue(aDs.GetTarget(aResource, EM_R(aProperty), true));
488  * Reads an AddonInternal object from an RDF stream.
490  * @param  aUri
491  *         The URI that the manifest is being read from
492  * @param  aStream
493  *         An open stream to read the RDF from
494  * @return an AddonInternal object
495  * @throws if the install manifest in the RDF stream is corrupt or could not
496  *         be read
497  */
498 function loadManifestFromRDF(aUri, aStream) {
499   function getPropertyArray(aDs, aSource, aProperty) {
500     let values = [];
501     let targets = aDs.GetTargets(aSource, EM_R(aProperty), true);
502     while (targets.hasMoreElements())
503       values.push(getRDFValue(targets.getNext()));
505     return values;
506   }
508   /**
509    * Reads locale properties from either the main install manifest root or
510    * an em:localized section in the install manifest.
511    *
512    * @param  aDs
513    *         The nsIRDFDatasource to read from
514    * @param  aSource
515    *         The nsIRDFResource to read the properties from
516    * @param  isDefault
517    *         True if the locale is to be read from the main install manifest
518    *         root
519    * @param  aSeenLocales
520    *         An array of locale names already seen for this install manifest.
521    *         Any locale names seen as a part of this function will be added to
522    *         this array
523    * @return an object containing the locale properties
524    */
525   function readLocale(aDs, aSource, isDefault, aSeenLocales) {
526     let locale = { };
527     if (!isDefault) {
528       locale.locales = [];
529       let targets = ds.GetTargets(aSource, EM_R("locale"), true);
530       while (targets.hasMoreElements()) {
531         let localeName = getRDFValue(targets.getNext());
532         if (!localeName) {
533           WARN("Ignoring empty locale in localized properties");
534           continue;
535         }
536         if (aSeenLocales.indexOf(localeName) != -1) {
537           WARN("Ignoring duplicate locale in localized properties");
538           continue;
539         }
540         aSeenLocales.push(localeName);
541         locale.locales.push(localeName);
542       }
544       if (locale.locales.length == 0) {
545         WARN("Ignoring localized properties with no listed locales");
546         return null;
547       }
548     }
550     PROP_LOCALE_SINGLE.forEach(function(aProp) {
551       locale[aProp] = getRDFProperty(aDs, aSource, aProp);
552     });
554     PROP_LOCALE_MULTI.forEach(function(aProp) {
555       locale[aProp] = getPropertyArray(aDs, aSource,
556                                        aProp.substring(0, aProp.length - 1));
557     });
559     return locale;
560   }
562   let rdfParser = Cc["@mozilla.org/rdf/xml-parser;1"].
563                   createInstance(Ci.nsIRDFXMLParser)
564   let ds = Cc["@mozilla.org/rdf/datasource;1?name=in-memory-datasource"].
565            createInstance(Ci.nsIRDFDataSource);
566   let listener = rdfParser.parseAsync(ds, aUri);
567   let channel = Cc["@mozilla.org/network/input-stream-channel;1"].
568                 createInstance(Ci.nsIInputStreamChannel);
569   channel.setURI(aUri);
570   channel.contentStream = aStream;
571   channel.QueryInterface(Ci.nsIChannel);
572   channel.contentType = "text/xml";
574   listener.onStartRequest(channel, null);
576   try {
577     let pos = 0;
578     let count = aStream.available();
579     while (count > 0) {
580       listener.onDataAvailable(channel, null, aStream, pos, count);
581       pos += count;
582       count = aStream.available();
583     }
584     listener.onStopRequest(channel, null, Components.results.NS_OK);
585   }
586   catch (e) {
587     listener.onStopRequest(channel, null, e.result);
588     throw e;
589   }
591   let root = gRDF.GetResource(RDFURI_INSTALL_MANIFEST_ROOT);
592   let addon = new AddonInternal();
593   PROP_METADATA.forEach(function(aProp) {
594     addon[aProp] = getRDFProperty(ds, root, aProp);
595   });
596   addon.unpack = getRDFProperty(ds, root, "unpack") == "true";
598   if (!addon.type) {
599     addon.type = addon.internalName ? "theme" : "extension";
600   }
601   else {
602     for (let name in TYPES) {
603       if (TYPES[name] == addon.type) {
604         addon.type = name;
605         break;
606       }
607     }
608   }
610   if (!(addon.type in TYPES))
611     throw new Error("Install manifest specifies unknown type: " + addon.type);
613   if (addon.type != "multipackage") {
614     if (!addon.id)
615       throw new Error("No ID in install manifest");
616     if (!gIDTest.test(addon.id))
617       throw new Error("Illegal add-on ID " + addon.id);
618     if (!addon.version)
619       throw new Error("No version in install manifest");
620   }
622   // Only read the bootstrapped property for extensions
623   if (addon.type == "extension") {
624     addon.bootstrap = getRDFProperty(ds, root, "bootstrap") == "true";
625   }
626   else {
627     // Only extensions are allowed to provide an optionsURL or aboutURL. For
628     // all other types they are silently ignored
629     addon.optionsURL = null;
630     addon.aboutURL = null;
632     if (addon.type == "theme") {
633       if (!addon.internalName)
634         throw new Error("Themes must include an internalName property");
635       addon.skinnable = getRDFProperty(ds, root, "skinnable") == "true";
636     }
637   }
639   addon.defaultLocale = readLocale(ds, root, true);
641   let seenLocales = [];
642   addon.locales = [];
643   let targets = ds.GetTargets(root, EM_R("localized"), true);
644   while (targets.hasMoreElements()) {
645     let target = targets.getNext().QueryInterface(Ci.nsIRDFResource);
646     let locale = readLocale(ds, target, false, seenLocales);
647     if (locale)
648       addon.locales.push(locale);
649   }
651   let seenApplications = [];
652   addon.targetApplications = [];
653   targets = ds.GetTargets(root, EM_R("targetApplication"), true);
654   while (targets.hasMoreElements()) {
655     let target = targets.getNext().QueryInterface(Ci.nsIRDFResource);
656     let targetAppInfo = {};
657     PROP_TARGETAPP.forEach(function(aProp) {
658       targetAppInfo[aProp] = getRDFProperty(ds, target, aProp);
659     });
660     if (!targetAppInfo.id || !targetAppInfo.minVersion ||
661         !targetAppInfo.maxVersion) {
662       WARN("Ignoring invalid targetApplication entry in install manifest");
663       continue;
664     }
665     if (seenApplications.indexOf(targetAppInfo.id) != -1) {
666       WARN("Ignoring duplicate targetApplication entry for " + targetAppInfo.id +
667            " in install manifest");
668       continue;
669     }
670     seenApplications.push(targetAppInfo.id);
671     addon.targetApplications.push(targetAppInfo);
672   }
674   // Note that we don't need to check for duplicate targetPlatform entries since
675   // the RDF service coalesces them for us.
676   let targetPlatforms = getPropertyArray(ds, root, "targetPlatform");
677   addon.targetPlatforms = [];
678   targetPlatforms.forEach(function(aPlatform) {
679     let platform = {
680       os: null,
681       abi: null
682     };
684     let pos = aPlatform.indexOf("_");
685     if (pos != -1) {
686       platform.os = aPlatform.substring(0, pos);
687       platform.abi = aPlatform.substring(pos + 1);
688     }
689     else {
690       platform.os = aPlatform;
691     }
693     addon.targetPlatforms.push(platform);
694   });
696   // A theme's userDisabled value is true if the theme is not the selected skin
697   // or if there is an active lightweight theme. We ignore whether softblocking
698   // is in effect since it would change the active theme.
699   if (addon.type == "theme") {
700     addon.userDisabled = !!LightweightThemeManager.currentTheme ||
701                          addon.internalName != XPIProvider.selectedSkin;
702   }
703   else {
704     addon.userDisabled = addon.blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED;
705   }
707   addon.appDisabled = !isUsableAddon(addon);
709   addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT;
711   return addon;
715  * Loads an AddonInternal object from an add-on extracted in a directory.
717  * @param  aDir
718  *         The nsIFile directory holding the add-on
719  * @return an AddonInternal object
720  * @throws if the directory does not contain a valid install manifest
721  */
722 function loadManifestFromDir(aDir) {
723   function getFileSize(aFile) {
724     if (aFile.isSymlink())
725       return 0;
727     if (!aFile.isDirectory())
728       return aFile.fileSize;
730     let size = 0;
731     let entries = aFile.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
732     let entry;
733     while (entry = entries.nextFile)
734       size += getFileSize(entry);
735     entries.close();
736     return size;
737   }
739   let file = aDir.clone();
740   file.append(FILE_INSTALL_MANIFEST);
741   if (!file.exists() || !file.isFile())
742     throw new Error("Directory " + aDir.path + " does not contain a valid " +
743                     "install manifest");
745   let fis = Cc["@mozilla.org/network/file-input-stream;1"].
746             createInstance(Ci.nsIFileInputStream);
747   fis.init(file, -1, -1, false);
748   let bis = Cc["@mozilla.org/network/buffered-input-stream;1"].
749             createInstance(Ci.nsIBufferedInputStream);
750   bis.init(fis, 4096);
752   try {
753     let addon = loadManifestFromRDF(Services.io.newFileURI(file), bis);
754     addon._sourceBundle = aDir.clone().QueryInterface(Ci.nsILocalFile);
755     addon.size = getFileSize(aDir);
756     return addon;
757   }
758   finally {
759     bis.close();
760     fis.close();
761   }
765  * Loads an AddonInternal object from an nsIZipReader for an add-on.
767  * @param  aZipReader
768  *         An open nsIZipReader for the add-on's files
769  * @return an AddonInternal object
770  * @throws if the XPI file does not contain a valid install manifest
771  */
772 function loadManifestFromZipReader(aZipReader) {
773   let zis = aZipReader.getInputStream(FILE_INSTALL_MANIFEST);
774   let bis = Cc["@mozilla.org/network/buffered-input-stream;1"].
775             createInstance(Ci.nsIBufferedInputStream);
776   bis.init(zis, 4096);
778   try {
779     let uri = buildJarURI(aZipReader.file, FILE_INSTALL_MANIFEST);
780     let addon = loadManifestFromRDF(uri, bis);
781     addon._sourceBundle = aZipReader.file;
783     addon.size = 0;
784     let entries = aZipReader.findEntries(null);
785     while (entries.hasMore())
786       addon.size += aZipReader.getEntry(entries.getNext()).realSize;
788     return addon;
789   }
790   finally {
791     bis.close();
792     zis.close();
793   }
797  * Loads an AddonInternal object from an add-on in an XPI file.
799  * @param  aXPIFile
800  *         An nsIFile pointing to the add-on's XPI file
801  * @return an AddonInternal object
802  * @throws if the XPI file does not contain a valid install manifest
803  */
804 function loadManifestFromZipFile(aXPIFile) {
805   let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].
806                   createInstance(Ci.nsIZipReader);
807   try {
808     zipReader.open(aXPIFile);
810     return loadManifestFromZipReader(zipReader);
811   }
812   finally {
813     zipReader.close();
814   }
817 function loadManifestFromFile(aFile) {
818   if (aFile.isFile())
819     return loadManifestFromZipFile(aFile);
820   else
821     return loadManifestFromDir(aFile);
825  * Creates a jar: URI for a file inside a ZIP file.
827  * @param  aJarfile
828  *         The ZIP file as an nsIFile
829  * @param  aPath
830  *         The path inside the ZIP file
831  * @return an nsIURI for the file
832  */
833 function buildJarURI(aJarfile, aPath) {
834   let uri = Services.io.newFileURI(aJarfile);
835   uri = "jar:" + uri.spec + "!/" + aPath;
836   return NetUtil.newURI(uri);
840  * Creates and returns a new unique temporary file. The caller should delete
841  * the file when it is no longer needed.
843  * @return an nsIFile that points to a randomly named, initially empty file in
844  *         the OS temporary files directory
845  */
846 function getTemporaryFile() {
847   let file = FileUtils.getDir(KEY_TEMPDIR, []);
848   let random = Math.random().toString(36).replace(/0./, '').substr(-3);
849   file.append("tmp-" + random + ".xpi");
850   file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
852   return file;
856  * Extracts files from a ZIP file into a directory.
858  * @param  aZipFile
859  *         The source ZIP file that contains the add-on.
860  * @param  aDir
861  *         The nsIFile to extract to.
862  */
863 function extractFiles(aZipFile, aDir) {
864   function getTargetFile(aDir, entry) {
865     let target = aDir.clone();
866     entry.split("/").forEach(function(aPart) {
867       target.append(aPart);
868     });
869     return target;
870   }
872   let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].
873                   createInstance(Ci.nsIZipReader);
874   zipReader.open(aZipFile);
876   try {
877     // create directories first
878     let entries = zipReader.findEntries("*/");
879     while (entries.hasMore()) {
880       var entryName = entries.getNext();
881       let target = getTargetFile(aDir, entryName);
882       if (!target.exists()) {
883         try {
884           target.create(Ci.nsILocalFile.DIRECTORY_TYPE,
885                         FileUtils.PERMS_DIRECTORY);
886         }
887         catch (e) {
888           ERROR("extractFiles: failed to create target directory for " +
889                 "extraction file = " + target.path, e);
890         }
891       }
892     }
894     entries = zipReader.findEntries(null);
895     while (entries.hasMore()) {
896       let entryName = entries.getNext();
897       let target = getTargetFile(aDir, entryName);
898       if (target.exists())
899         continue;
901       zipReader.extract(entryName, target);
902       target.permissions |= FileUtils.PERMS_FILE;
903     }
904   }
905   finally {
906     zipReader.close();
907   }
911  * Verifies that a zip file's contents are all signed by the same principal.
912  * Directory entries and anything in the META-INF directory are not checked.
914  * @param  aZip
915  *         A nsIZipReader to check
916  * @param  aPrincipal
917  *         The nsIPrincipal to compare against
918  * @return true if all the contents that should be signed were signed by the
919  *         principal
920  */
921 function verifyZipSigning(aZip, aPrincipal) {
922   var count = 0;
923   var entries = aZip.findEntries(null);
924   while (entries.hasMore()) {
925     var entry = entries.getNext();
926     // Nothing in META-INF is in the manifest.
927     if (entry.substr(0, 9) == "META-INF/")
928       continue;
929     // Directory entries aren't in the manifest.
930     if (entry.substr(-1) == "/")
931       continue;
932     count++;
933     var entryPrincipal = aZip.getCertificatePrincipal(entry);
934     if (!entryPrincipal || !aPrincipal.equals(entryPrincipal))
935       return false;
936   }
937   return aZip.manifestEntriesCount == count;
941  * Replaces %...% strings in an addon url (update and updateInfo) with
942  * appropriate values.
944  * @param  aAddon
945  *         The AddonInternal representing the add-on
946  * @param  aUri
947  *         The uri to escape
948  * @param  aUpdateType
949  *         An optional number representing the type of update, only applicable
950  *         when creating a url for retrieving an update manifest
951  * @param  aAppVersion
952  *         The optional application version to use for %APP_VERSION%
953  * @return the appropriately escaped uri.
954  */
955 function escapeAddonURI(aAddon, aUri, aUpdateType, aAppVersion)
957   var addonStatus = aAddon.userDisabled ? "userDisabled" : "userEnabled";
959   if (!aAddon.isCompatible)
960     addonStatus += ",incompatible";
961   if (aAddon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED)
962     addonStatus += ",blocklisted";
963   if (aAddon.blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED)
964     addonStatus += ",softblocked";
966   try {
967     var xpcomABI = Services.appinfo.XPCOMABI;
968   } catch (ex) {
969     xpcomABI = UNKNOWN_XPCOM_ABI;
970   }
972   let uri = aUri.replace(/%ITEM_ID%/g, aAddon.id);
973   uri = uri.replace(/%ITEM_VERSION%/g, aAddon.version);
974   uri = uri.replace(/%ITEM_STATUS%/g, addonStatus);
975   uri = uri.replace(/%APP_ID%/g, Services.appinfo.ID);
976   uri = uri.replace(/%APP_VERSION%/g, aAppVersion ? aAppVersion :
977                                                     Services.appinfo.version);
978   uri = uri.replace(/%REQ_VERSION%/g, REQ_VERSION);
979   uri = uri.replace(/%APP_OS%/g, Services.appinfo.OS);
980   uri = uri.replace(/%APP_ABI%/g, xpcomABI);
981   uri = uri.replace(/%APP_LOCALE%/g, getLocale());
982   uri = uri.replace(/%CURRENT_APP_VERSION%/g, Services.appinfo.version);
984   // If there is an updateType then replace the UPDATE_TYPE string
985   if (aUpdateType)
986     uri = uri.replace(/%UPDATE_TYPE%/g, aUpdateType);
988   // If this add-on has compatibility information for either the current
989   // application or toolkit then replace the ITEM_MAXAPPVERSION with the
990   // maxVersion
991   let app = aAddon.matchingTargetApplication;
992   if (app)
993     var maxVersion = app.maxVersion;
994   else
995     maxVersion = "";
996   uri = uri.replace(/%ITEM_MAXAPPVERSION%/g, maxVersion);
998   // Replace custom parameters (names of custom parameters must have at
999   // least 3 characters to prevent lookups for something like %D0%C8)
1000   var catMan = null;
1001   uri = uri.replace(/%(\w{3,})%/g, function(aMatch, aParam) {
1002     if (!catMan) {
1003       catMan = Cc["@mozilla.org/categorymanager;1"].
1004                getService(Ci.nsICategoryManager);
1005     }
1007     try {
1008       var contractID = catMan.getCategoryEntry(CATEGORY_UPDATE_PARAMS, aParam);
1009       var paramHandler = Cc[contractID].getService(Ci.nsIPropertyBag2);
1010       return paramHandler.getPropertyAsAString(aParam);
1011     }
1012     catch(e) {
1013       return aMatch;
1014     }
1015   });
1017   // escape() does not properly encode + symbols in any embedded FVF strings.
1018   return uri.replace(/\+/g, "%2B");
1022  * Copies properties from one object to another. If no target object is passed
1023  * a new object will be created and returned.
1025  * @param  aObject
1026  *         An object to copy from
1027  * @param  aProperties
1028  *         An array of properties to be copied
1029  * @param  aTarget
1030  *         An optional target object to copy the properties to
1031  * @return the object that the properties were copied onto
1032  */
1033 function copyProperties(aObject, aProperties, aTarget) {
1034   if (!aTarget)
1035     aTarget = {};
1036   aProperties.forEach(function(aProp) {
1037     aTarget[aProp] = aObject[aProp];
1038   });
1039   return aTarget;
1043  * Copies properties from a mozIStorageRow to an object. If no target object is
1044  * passed a new object will be created and returned.
1046  * @param  aRow
1047  *         A mozIStorageRow to copy from
1048  * @param  aProperties
1049  *         An array of properties to be copied
1050  * @param  aTarget
1051  *         An optional target object to copy the properties to
1052  * @return the object that the properties were copied onto
1053  */
1054 function copyRowProperties(aRow, aProperties, aTarget) {
1055   if (!aTarget)
1056     aTarget = {};
1057   aProperties.forEach(function(aProp) {
1058     aTarget[aProp] = aRow.getResultByName(aProp);
1059   });
1060   return aTarget;
1064  * A generator to synchronously return result rows from an mozIStorageStatement.
1066  * @param  aStatement
1067  *         The statement to execute
1068  */
1069 function resultRows(aStatement) {
1070   try {
1071     while (stepStatement(aStatement))
1072       yield aStatement.row;
1073   }
1074   finally {
1075     aStatement.reset();
1076   }
1080  * Removes the specified files or directories in a staging directory and then if
1081  * the staging directory is empty attempts to remove it.
1083  * @param  aDir
1084  *         nsIFile for the staging directory to clean up
1085  * @param  aLeafNames
1086  *         An array of file or directory to remove from the directory, the
1087  *         array may be empty
1088  */
1089 function cleanStagingDir(aDir, aLeafNames) {
1090   aLeafNames.forEach(function(aName) {
1091     let file = aDir.clone();
1092     file.append(aName);
1093     if (file.exists())
1094       recursiveRemove(file);
1095   });
1097   let dirEntries = aDir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
1098   try {
1099     if (dirEntries.nextFile)
1100       return;
1101   }
1102   finally {
1103     dirEntries.close();
1104   }
1106   try {
1107     aDir.permissions = FileUtils.PERMS_DIRECTORY;
1108     aDir.remove(false);
1109   }
1110   catch (e) {
1111     // Failing to remove the staging directory is ignorable
1112   }
1116  * Recursively removes a directory or file fixing permissions when necessary.
1118  * @param  aFile
1119  *         The nsIFile to remove
1120  */
1121 function recursiveRemove(aFile) {
1122   aFile.permissions = aFile.isDirectory() ? FileUtils.PERMS_DIRECTORY
1123                                           : FileUtils.PERMS_FILE;
1125   try {
1126     aFile.remove(true);
1127     return;
1128   }
1129   catch (e) {
1130     if (!aFile.isDirectory()) {
1131       ERROR("Failed to remove file " + aFile.path, e);
1132       throw e;
1133     }
1134   }
1136   let entry;
1137   let dirEntries = aFile.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
1138   try {
1139     while (entry = dirEntries.nextFile)
1140       recursiveRemove(entry);
1141     aFile.remove(true);
1142   }
1143   finally {
1144     dirEntries.close();
1145   }
1149  * Returns the timestamp of the most recently modified file in a directory,
1150  * or simply the file's own timestamp if it is not a directory.
1152  * @param  aFile
1153  *         A non-null nsIFile object
1154  * @return Epoch time, as described above. 0 for an empty directory.
1155  */
1156 function recursiveLastModifiedTime(aFile) {
1157   if (aFile.isFile())
1158     return aFile.lastModifiedTime;
1160   if (aFile.isDirectory()) {
1161     let entries = aFile.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
1162     let entry, time;
1163     let maxTime = aFile.lastModifiedTime;
1164     while (entry = entries.nextFile) {
1165       time = recursiveLastModifiedTime(entry);
1166       maxTime = Math.max(time, maxTime);
1167     }
1168     entries.close();
1169     return maxTime;
1170   }
1172   // If the file is something else, just ignore it.
1173   return 0;
1177  * A helpful wrapper around the prefs service that allows for default values
1178  * when requested values aren't set.
1179  */
1180 var Prefs = {
1181   /**
1182    * Gets a preference from the default branch ignoring user-set values.
1183    *
1184    * @param  aName
1185    *         The name of the preference
1186    * @param  aDefaultValue
1187    *         A value to return if the preference does not exist
1188    * @return the default value of the preference or aDefaultValue if there is
1189    *         none
1190    */
1191   getDefaultCharPref: function(aName, aDefaultValue) {
1192     try {
1193       return Services.prefs.getDefaultBranch("").getCharPref(aName);
1194     }
1195     catch (e) {
1196     }
1197     return aDefaultValue;
1198   },
1200   /**
1201    * Gets a string preference.
1202    *
1203    * @param  aName
1204    *         The name of the preference
1205    * @param  aDefaultValue
1206    *         A value to return if the preference does not exist
1207    * @return the value of the preference or aDefaultValue if there is none
1208    */
1209   getCharPref: function(aName, aDefaultValue) {
1210     try {
1211       return Services.prefs.getCharPref(aName);
1212     }
1213     catch (e) {
1214     }
1215     return aDefaultValue;
1216   },
1218   /**
1219    * Gets a boolean preference.
1220    *
1221    * @param  aName
1222    *         The name of the preference
1223    * @param  aDefaultValue
1224    *         A value to return if the preference does not exist
1225    * @return the value of the preference or aDefaultValue if there is none
1226    */
1227   getBoolPref: function(aName, aDefaultValue) {
1228     try {
1229       return Services.prefs.getBoolPref(aName);
1230     }
1231     catch (e) {
1232     }
1233     return aDefaultValue;
1234   },
1236   /**
1237    * Gets an integer preference.
1238    *
1239    * @param  aName
1240    *         The name of the preference
1241    * @param  defaultValue
1242    *         A value to return if the preference does not exist
1243    * @return the value of the preference or defaultValue if there is none
1244    */
1245   getIntPref: function(aName, defaultValue) {
1246     try {
1247       return Services.prefs.getIntPref(aName);
1248     }
1249     catch (e) {
1250     }
1251     return defaultValue;
1252   }
1255 var XPIProvider = {
1256   // An array of known install locations
1257   installLocations: null,
1258   // A dictionary of known install locations by name
1259   installLocationsByName: null,
1260   // An array of currently active AddonInstalls
1261   installs: null,
1262   // The default skin for the application
1263   defaultSkin: "classic/1.0",
1264   // The current skin used by the application
1265   currentSkin: null,
1266   // The selected skin to be used by the application when it is restarted. This
1267   // will be the same as currentSkin when it is the skin to be used when the
1268   // application is restarted
1269   selectedSkin: null,
1270   // The name of the checkCompatibility preference for the current application
1271   // version
1272   checkCompatibilityPref: null,
1273   // The value of the checkCompatibility preference
1274   checkCompatibility: true,
1275   // The value of the checkUpdateSecurity preference
1276   checkUpdateSecurity: true,
1277   // A dictionary of the file descriptors for bootstrappable add-ons by ID
1278   bootstrappedAddons: {},
1279   // A dictionary of JS scopes of loaded bootstrappable add-ons by ID
1280   bootstrapScopes: {},
1281   // True if the platform could have activated extensions
1282   extensionsActive: false,
1284   // True if all of the add-ons found during startup were installed in the
1285   // application install location
1286   allAppGlobal: true,
1287   // A string listing the enabled add-ons for annotating crash reports
1288   enabledAddons: null,
1289   // An array of add-on IDs of add-ons that were inactive during startup
1290   inactiveAddonIDs: [],
1291   // A cache of the add-on IDs of add-ons that had changes performed to them
1292   // during this session's startup. This is preliminary work, hopefully it will
1293   // be expanded on in the future and an API made to get at it from the
1294   // application.
1295   startupChanges: {
1296     // Add-ons that became disabled for compatibility reasons
1297     appDisabled: []
1298   },
1300   /**
1301    * Starts the XPI provider initializes the install locations and prefs.
1302    *
1303    * @param  aAppChanged
1304    *         A tri-state value. Undefined means the current profile was created
1305    *         for this session, true means the profile already existed but was
1306    *         last used with an application with a different version number,
1307    *         false means that the profile was last used by this version of the
1308    *         application.
1309    */
1310   startup: function XPI_startup(aAppChanged) {
1311     LOG("startup");
1312     this.installs = [];
1313     this.installLocations = [];
1314     this.installLocationsByName = {};
1316     function addDirectoryInstallLocation(aName, aKey, aPaths, aScope, aLocked) {
1317       try {
1318         var dir = FileUtils.getDir(aKey, aPaths);
1319       }
1320       catch (e) {
1321         // Some directories aren't defined on some platforms, ignore them
1322         LOG("Skipping unavailable install location " + aName);
1323         return;
1324       }
1326       try {
1327         var location = new DirectoryInstallLocation(aName, dir, aScope, aLocked);
1328       }
1329       catch (e) {
1330         WARN("Failed to add directory install location " + aName, e);
1331         return;
1332       }
1334       XPIProvider.installLocations.push(location);
1335       XPIProvider.installLocationsByName[location.name] = location;
1336     }
1338     function addRegistryInstallLocation(aName, aRootkey, aScope) {
1339       try {
1340         var location = new WinRegInstallLocation(aName, aRootkey, aScope);
1341       }
1342       catch (e) {
1343         WARN("Failed to add registry install location " + aName, e);
1344         return;
1345       }
1347       XPIProvider.installLocations.push(location);
1348       XPIProvider.installLocationsByName[location.name] = location;
1349     }
1351     let hasRegistry = ("nsIWindowsRegKey" in Ci);
1353     let enabledScopes = Prefs.getIntPref(PREF_EM_ENABLED_SCOPES,
1354                                          AddonManager.SCOPE_ALL);
1356     // These must be in order of priority for processFileChanges etc. to work
1357     if (enabledScopes & AddonManager.SCOPE_SYSTEM) {
1358       if (hasRegistry) {
1359         addRegistryInstallLocation("winreg-app-global",
1360                                    Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
1361                                    AddonManager.SCOPE_SYSTEM);
1362       }
1363       addDirectoryInstallLocation(KEY_APP_SYSTEM_LOCAL, "XRESysLExtPD",
1364                                   [Services.appinfo.ID],
1365                                   AddonManager.SCOPE_SYSTEM, true);
1366       addDirectoryInstallLocation(KEY_APP_SYSTEM_SHARE, "XRESysSExtPD",
1367                                   [Services.appinfo.ID],
1368                                   AddonManager.SCOPE_SYSTEM, true);
1369     }
1371     if (enabledScopes & AddonManager.SCOPE_APPLICATION) {
1372       addDirectoryInstallLocation(KEY_APP_GLOBAL, KEY_APPDIR,
1373                                   [DIR_EXTENSIONS],
1374                                   AddonManager.SCOPE_APPLICATION, true);
1375     }
1377     if (enabledScopes & AddonManager.SCOPE_USER) {
1378       if (hasRegistry) {
1379         addRegistryInstallLocation("winreg-app-user",
1380                                    Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
1381                                    AddonManager.SCOPE_USER);
1382       }
1383       addDirectoryInstallLocation(KEY_APP_SYSTEM_USER, "XREUSysExt",
1384                                   [Services.appinfo.ID],
1385                                   AddonManager.SCOPE_USER, true);
1386     }
1388     // The profile location is always enabled
1389     addDirectoryInstallLocation(KEY_APP_PROFILE, KEY_PROFILEDIR,
1390                                 [DIR_EXTENSIONS],
1391                                 AddonManager.SCOPE_PROFILE, false);
1393     this.defaultSkin = Prefs.getDefaultCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN,
1394                                                 "classic/1.0");
1395     this.currentSkin = Prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN,
1396                                          this.defaultSkin);
1397     this.selectedSkin = this.currentSkin;
1398     this.applyThemeChange();
1400     var version = Services.appinfo.version.replace(BRANCH_REGEXP, "$1");
1401     this.checkCompatibilityPref = PREF_EM_CHECK_COMPATIBILITY + "." + version;
1402     this.checkCompatibility = Prefs.getBoolPref(this.checkCompatibilityPref,
1403                                                 true)
1404     this.checkUpdateSecurity = Prefs.getBoolPref(PREF_EM_CHECK_UPDATE_SECURITY,
1405                                                  true)
1406     this.enabledAddons = [];
1408     Services.prefs.addObserver(this.checkCompatibilityPref, this, false);
1409     Services.prefs.addObserver(PREF_EM_CHECK_UPDATE_SECURITY, this, false);
1411     let flushCaches = this.checkForChanges(aAppChanged);
1413     // Changes to installed extensions may have changed which theme is selected
1414     this.applyThemeChange();
1416     if (Services.prefs.prefHasUserValue(PREF_EM_DISABLED_ADDONS_LIST))
1417       Services.prefs.clearUserPref(PREF_EM_DISABLED_ADDONS_LIST);
1419     // If the application has been upgraded and there are add-ons outside the
1420     // application directory then we may need to synchronize compatibility
1421     // information
1422     if (aAppChanged && !this.allAppGlobal) {
1423       // Should we show a UI or just pass the list via a pref?
1424       if (Prefs.getBoolPref(PREF_EM_SHOW_MISMATCH_UI, true)) {
1425         this.showMismatchWindow();
1426         flushCaches = true;
1427       }
1428       else if (this.startupChanges.appDisabled.length > 0) {
1429         // Remember the list of add-ons that were disabled this startup so
1430         // the application can notify the user however it wants to
1431         Services.prefs.setCharPref(PREF_EM_DISABLED_ADDONS_LIST,
1432                                    this.startupChanges.appDisabled.join(","));
1433       }
1434     }
1436     if (flushCaches) {
1437       // Init this, so it will get the notification.
1438       let xulPrototypeCache = Cc["@mozilla.org/xul/xul-prototype-cache;1"].getService(Ci.nsISupports);
1439       Services.obs.notifyObservers(null, "startupcache-invalidate", null);
1440     }
1442     this.enabledAddons = Prefs.getCharPref(PREF_EM_ENABLED_ADDONS, "");
1443     if ("nsICrashReporter" in Ci &&
1444         Services.appinfo instanceof Ci.nsICrashReporter) {
1445       // Annotate the crash report with relevant add-on information.
1446       try {
1447         Services.appinfo.annotateCrashReport("Theme", this.currentSkin);
1448       } catch (e) { }
1449       try {
1450         Services.appinfo.annotateCrashReport("EMCheckCompatibility",
1451                                              this.checkCompatibility);
1452       } catch (e) { }
1453       this.addAddonsToCrashReporter();
1454     }
1456     for (let id in this.bootstrappedAddons) {
1457       let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
1458       file.persistentDescriptor = this.bootstrappedAddons[id].descriptor;
1459       this.callBootstrapMethod(id, this.bootstrappedAddons[id].version, file,
1460                                "startup", BOOTSTRAP_REASONS.APP_STARTUP);
1461     }
1463     // Let these shutdown a little earlier when they still have access to most
1464     // of XPCOM
1465     Services.obs.addObserver({
1466       observe: function(aSubject, aTopic, aData) {
1467         Services.prefs.setCharPref(PREF_BOOTSTRAP_ADDONS,
1468                                    JSON.stringify(XPIProvider.bootstrappedAddons));
1469         for (let id in XPIProvider.bootstrappedAddons) {
1470           let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
1471           file.persistentDescriptor = XPIProvider.bootstrappedAddons[id].descriptor;
1472           XPIProvider.callBootstrapMethod(id, XPIProvider.bootstrappedAddons[id].version,
1473                                           file, "shutdown",
1474                                           BOOTSTRAP_REASONS.APP_SHUTDOWN);
1475         }
1476         Services.obs.removeObserver(this, "quit-application-granted");
1477       }
1478     }, "quit-application-granted", false);
1480     this.extensionsActive = true;
1481   },
1483   /**
1484    * Shuts down the database and releases all references.
1485    */
1486   shutdown: function XPI_shutdown() {
1487     LOG("shutdown");
1489     Services.prefs.removeObserver(this.checkCompatibilityPref, this);
1490     Services.prefs.removeObserver(PREF_EM_CHECK_UPDATE_SECURITY, this);
1492     this.bootstrappedAddons = {};
1493     this.bootstrapScopes = {};
1494     this.enabledAddons = null;
1495     this.allAppGlobal = true;
1497     for (let type in this.startupChanges)
1498       this.startupChanges[type] = [];
1500     this.inactiveAddonIDs = [];
1502     // If there are pending operations then we must update the list of active
1503     // add-ons
1504     if (Prefs.getBoolPref(PREF_PENDING_OPERATIONS, false)) {
1505       XPIDatabase.updateActiveAddons();
1506       XPIDatabase.writeAddonsList();
1507       Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, false);
1508     }
1510     this.installs = null;
1511     this.installLocations = null;
1512     this.installLocationsByName = null;
1514     // This is needed to allow xpcshell tests to simulate a restart
1515     this.extensionsActive = false;
1517     XPIDatabase.shutdown(function() {
1518       Services.obs.notifyObservers(null, "xpi-provider-shutdown", null);
1519     });
1520   },
1522   /**
1523    * Applies any pending theme change to the preferences.
1524    */
1525   applyThemeChange: function XPI_applyThemeChange() {
1526     if (!Prefs.getBoolPref(PREF_DSS_SWITCHPENDING, false))
1527       return;
1529     // Tell the Chrome Registry which Skin to select
1530     try {
1531       this.selectedSkin = Prefs.getCharPref(PREF_DSS_SKIN_TO_SELECT);
1532       Services.prefs.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN,
1533                                  this.selectedSkin);
1534       Services.prefs.clearUserPref(PREF_DSS_SKIN_TO_SELECT);
1535       LOG("Changed skin to " + this.selectedSkin);
1536       this.currentSkin = this.selectedSkin;
1537     }
1538     catch (e) {
1539       ERROR("Error applying theme change", e);
1540     }
1541     Services.prefs.clearUserPref(PREF_DSS_SWITCHPENDING);
1542   },
1544   /**
1545    * Shows the "Compatibility Updates" UI
1546    */
1547   showMismatchWindow: function XPI_showMismatchWindow() {
1548     var variant = Cc["@mozilla.org/variant;1"].
1549                   createInstance(Ci.nsIWritableVariant);
1550     variant.setFromVariant(this.inactiveAddonIDs);
1552     // This *must* be modal as it has to block startup.
1553     var features = "chrome,centerscreen,dialog,titlebar,modal";
1554     var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].
1555              getService(Ci.nsIWindowWatcher);
1556     ww.openWindow(null, URI_EXTENSION_UPDATE_DIALOG, "", features, variant);
1558     // Ensure any changes to the add-ons list are flushed to disk
1559     XPIDatabase.writeAddonsList([]);
1560     Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, false);
1561   },
1563   /**
1564    * Adds a list of currently active add-ons to the next crash report.
1565    */
1566   addAddonsToCrashReporter: function XPI_addAddonsToCrashReporter() {
1567     if (!("nsICrashReporter" in Ci) ||
1568         !(Services.appinfo instanceof Ci.nsICrashReporter))
1569       return;
1571     // In safe mode no add-ons are loaded so we should not include them in the
1572     // crash report
1573     if (Services.appinfo.inSafeMode)
1574       return;
1576     let data = this.enabledAddons;
1577     for (let id in this.bootstrappedAddons)
1578       data += (data ? "," : "") + id + ":" + this.bootstrappedAddons[id].version;
1580     try {
1581       Services.appinfo.annotateCrashReport("Add-ons", data);
1582     }
1583     catch (e) { }
1584   },
1586   /**
1587    * Gets the add-on states for an install location.
1588    * This function may be expensive because of the recursiveLastModifiedTime call.
1589    *
1590    * @param  location
1591    *         The install location to retrieve the add-on states for
1592    * @return a dictionary mapping add-on IDs to objects with a descriptor
1593    *         property which contains the add-ons dir/file descriptor and an
1594    *         mtime property which contains the add-on's last modified time as
1595    *         the number of milliseconds since the epoch.
1596    */
1597   getAddonStates: function XPI_getAddonStates(aLocation) {
1598     let addonStates = {};
1599     aLocation.addonLocations.forEach(function(file) {
1600       let id = aLocation.getIDForLocation(file);
1601       addonStates[id] = {
1602         descriptor: file.persistentDescriptor,
1603         mtime: recursiveLastModifiedTime(file)
1604       };
1605     });
1607     return addonStates;
1608   },
1610   /**
1611    * Gets an array of install location states which uniquely describes all
1612    * installed add-ons with the add-on's InstallLocation name and last modified
1613    * time. This function may be expensive because of the getAddonStates() call.
1614    *
1615    * @return an array of add-on states for each install location. Each state
1616    *         is an object with a name property holding the location's name and
1617    *         an addons property holding the add-on states for the location
1618    */
1619   getInstallLocationStates: function XPI_getInstallLocationStates() {
1620     let states = [];
1621     this.installLocations.forEach(function(aLocation) {
1622       let addons = aLocation.addonLocations;
1623       if (addons.length == 0)
1624         return;
1626       let locationState = {
1627         name: aLocation.name,
1628         addons: this.getAddonStates(aLocation)
1629       };
1631       states.push(locationState);
1632     }, this);
1633     return states;
1634   },
1636   /**
1637    * Check the staging directories of install locations for any add-ons to be
1638    * installed or add-ons to be uninstalled.
1639    *
1640    * @param  aManifests
1641    *         A dictionary to add detected install manifests to for the purpose
1642    *         of passing through updated compatibility information
1643    * @return true if an add-on was installed or uninstalled
1644    */
1645   processPendingFileChanges: function XPI_processPendingFileChanges(aManifests) {
1646     let changed = false;
1647     this.installLocations.forEach(function(aLocation) {
1648       aManifests[aLocation.name] = {};
1649       // We can't install or uninstall anything in locked locations
1650       if (aLocation.locked)
1651         return;
1653       let stagedXPIDir = aLocation.getXPIStagingDir();
1654       let stagingDir = aLocation.getStagingDir();
1656       if (stagedXPIDir.exists() && stagedXPIDir.isDirectory()) {
1657         let entries = stagedXPIDir.directoryEntries
1658                                   .QueryInterface(Ci.nsIDirectoryEnumerator);
1659         while (entries.hasMoreElements()) {
1660           let stageDirEntry = entries.nextFile;
1662           if (!stageDirEntry.isDirectory()) {
1663             WARN("Ignoring file in XPI staging directory: " + stageDirEntry.path);
1664             continue;
1665           }
1667           // Find the last added XPI file in the directory
1668           let stagedXPI = null;
1669           var xpiEntries = stageDirEntry.directoryEntries
1670                                         .QueryInterface(Ci.nsIDirectoryEnumerator);
1671           while (xpiEntries.hasMoreElements()) {
1672             let file = xpiEntries.nextFile;
1673             if (!(file instanceof Ci.nsILocalFile))
1674               continue;
1675             if (file.isDirectory())
1676               continue;
1678             let extension = file.leafName;
1679             extension = extension.substring(extension.length - 4);
1681             if (extension != ".xpi" && extension != ".jar")
1682               continue;
1684             stagedXPI = file;
1685           }
1686           xpiEntries.close();
1688           if (!stagedXPI)
1689             continue;
1691           let addon = null;
1692           try {
1693             addon = loadManifestFromZipFile(stagedXPI);
1694           }
1695           catch (e) {
1696             ERROR("Unable to read add-on manifest for " + stagedXPI.leafName +
1697                   " in XPI stage of " + aLocation.name, e);
1698             continue;
1699           }
1701           LOG("Migrating staged install of " + addon.id + " in " + aLocation.name);
1703           if (addon.unpack || Prefs.getBoolPref(PREF_XPI_UNPACK, false)) {
1704             let targetDir = stagingDir.clone();
1705             targetDir.append(addon.id);
1706             try {
1707               targetDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
1708             }
1709             catch (e) {
1710               ERROR("Failed to create staging directory for add-on " + id, e);
1711               continue;
1712             }
1714             try {
1715               extractFiles(stagedXPI, targetDir);
1716             }
1717             catch (e) {
1718               ERROR("Failed to extract staged XPI for add-on " + id + " in " +
1719                     aLocation.name, e);
1720             }
1721           }
1722           else {
1723             try {
1724               stagedXPI.moveTo(stagingDir, addon.id + ".xpi");
1725             }
1726             catch (e) {
1727               ERROR("Failed to move staged XPI for add-on " + id + " in " +
1728                     aLocation.name, e);
1729             }
1730           }
1731         }
1732         entries.close();
1733       }
1735       if (stagedXPIDir.exists()) {
1736         try {
1737           recursiveRemove(stagedXPIDir);
1738         }
1739         catch (e) {
1740           // Non-critical, just saves some perf on startup if we clean this up.
1741           LOG("Error removing XPI staging dir " + stagedXPIDir.path, e);
1742         }
1743       }
1745       if (!stagingDir || !stagingDir.exists() || !stagingDir.isDirectory())
1746         return;
1748       entries = stagingDir.directoryEntries
1749                           .QueryInterface(Ci.nsIDirectoryEnumerator);
1750       while (entries.hasMoreElements()) {
1751         let stageDirEntry = entries.getNext().QueryInterface(Ci.nsILocalFile);
1753         let id = stageDirEntry.leafName;
1754         if (!stageDirEntry.isDirectory()) {
1755           if (id.substring(id.length - 4).toLowerCase() == ".xpi") {
1756             id = id.substring(0, id.length - 4);
1757           }
1758           else {
1759             if (id.substring(id.length - 5).toLowerCase() != ".json")
1760               WARN("Ignoring file: " + stageDirEntry.path);
1761             continue;
1762           }
1763         }
1765         // Check that the directory's name is a valid ID.
1766         if (!gIDTest.test(id)) {
1767           WARN("Ignoring directory whose name is not a valid add-on ID: " +
1768                stageDirEntry.path);
1769           continue;
1770         }
1772         changed = true;
1774         if (stageDirEntry.isDirectory()) {
1775           // Check if the directory contains an install manifest.
1776           let manifest = stageDirEntry.clone();
1777           manifest.append(FILE_INSTALL_MANIFEST);
1779           // If the install manifest doesn't exist uninstall this add-on in this
1780           // install location.
1781           if (!manifest.exists()) {
1782             LOG("Processing uninstall of " + id + " in " + aLocation.name);
1783             try {
1784               aLocation.uninstallAddon(id);
1785             }
1786             catch (e) {
1787               ERROR("Failed to uninstall add-on " + id + " in " + aLocation.name, e);
1788             }
1789             // The file check later will spot the removal and cleanup the database
1790             continue;
1791           }
1792         }
1794         aManifests[aLocation.name][id] = null;
1795         let existingAddonID = null;
1797         // Check for a cached AddonInternal for this add-on, it may contain
1798         // updated compatibility information
1799         let jsonfile = stagingDir.clone();
1800         jsonfile.append(id + ".json");
1801         if (jsonfile.exists()) {
1802           LOG("Found updated manifest for " + id + " in " + aLocation.name);
1803           let fis = Cc["@mozilla.org/network/file-input-stream;1"].
1804                        createInstance(Ci.nsIFileInputStream);
1805           let json = Cc["@mozilla.org/dom/json;1"].
1806                      createInstance(Ci.nsIJSON);
1808           try {
1809             fis.init(jsonfile, -1, 0, 0);
1810             aManifests[aLocation.name][id] = json.decodeFromStream(fis,
1811                                                                    jsonfile.fileSize);
1812             existingAddonID = aManifests[aLocation.name][id].existingAddonID;
1813           }
1814           catch (e) {
1815             ERROR("Unable to read add-on manifest for " + id + " in " +
1816                   aLocation.name, e);
1817           }
1818           finally {
1819             fis.close();
1820           }
1821         }
1823         LOG("Processing install of " + id + " in " + aLocation.name);
1824         try {
1825           var addonInstallLocation = aLocation.installAddon(id, stageDirEntry,
1826                                                             existingAddonID);
1827           if (aManifests[aLocation.name][id])
1828             aManifests[aLocation.name][id]._sourceBundle = addonInstallLocation;
1829         }
1830         catch (e) {
1831           ERROR("Failed to install staged add-on " + id + " in " + aLocation.name,
1832                 e);
1833           delete aManifests[aLocation.name][id];
1834           continue;
1835         }
1836       }
1837       entries.close();
1839       try {
1840         recursiveRemove(stagingDir);
1841       }
1842       catch (e) {
1843         // Non-critical, just saves some perf on startup if we clean this up.
1844         LOG("Error removing staging dir " + stagingDir.path, e);
1845       }
1846     });
1847     return changed;
1848   },
1850   /**
1851    * Installs any add-ons located in the extensions directory of the
1852    * application's distribution specific directory into the profile unless a
1853    * newer version already exists or the user has previously uninstalled the
1854    * distributed add-on.
1855    *
1856    * @param  aManifests
1857    *         A dictionary to add new install manifests to to save having to
1858    *         reload them later
1859    * @return true if any new add-ons were installed
1860    */
1861   installDistributionAddons: function XPI_installDistributionAddons(aManifests) {
1862     let distroDir;
1863     try {
1864       distroDir = FileUtils.getDir(KEY_APP_DISTRIBUTION, [DIR_EXTENSIONS]);
1865     }
1866     catch (e) {
1867       return false;
1868     }
1870     if (!distroDir.exists())
1871       return false;
1873     if (!distroDir.isDirectory())
1874       return false;
1876     let changed = false;
1877     let profileLocation = this.installLocationsByName[KEY_APP_PROFILE];
1879     let entries = distroDir.directoryEntries
1880                            .QueryInterface(Ci.nsIDirectoryEnumerator);
1881     let entry;
1882     while (entry = entries.nextFile) {
1883       // Should never happen really
1884       if (!(entry instanceof Ci.nsILocalFile))
1885         continue;
1887       let id = entry.leafName;
1889       if (entry.isFile()) {
1890         if (id.substring(id.length - 4).toLowerCase() == ".xpi") {
1891           id = id.substring(0, id.length - 4);
1892         }
1893         else {
1894           LOG("Ignoring distribution add-on that isn't an XPI: " + entry.path);
1895           continue;
1896         }
1897       }
1898       else if (!entry.isDirectory()) {
1899         LOG("Ignoring distribution add-on that isn't a file or directory: " +
1900             entry.path);
1901         continue;
1902       }
1904       if (!gIDTest.test(id)) {
1905         LOG("Ignoring distribution add-on whose name is not a valid add-on ID: " +
1906             entry.path);
1907         continue;
1908       }
1910       let addon;
1911       try {
1912         addon = loadManifestFromFile(entry);
1913       }
1914       catch (e) {
1915         WARN("File entry " + entry.path + " contains an invalid add-on", e);
1916         continue;
1917       }
1919       if (addon.id != id) {
1920         WARN("File entry " + entry.path + " contains an add-on with an " +
1921              "incorrect ID")
1922         continue;
1923       }
1925       let existingEntry = null;
1926       try {
1927         existingEntry = profileLocation.getLocationForID(id);
1928       }
1929       catch (e) {
1930       }
1932       if (existingEntry) {
1933         let existingAddon;
1934         try {
1935           existingAddon = loadManifestFromFile(existingEntry);
1937           if (Services.vc.compare(addon.version, existingAddon.version) <= 0)
1938             continue;
1939         }
1940         catch (e) {
1941           // Bad add-on in the profile so just proceed and install over the top
1942           WARN("Profile contains an add-on with a bad or missing install " +
1943                "manifest at " + existingEntry.path + ", overwriting", e);
1944         }
1945       }
1946       else if (Prefs.getBoolPref(PREF_BRANCH_INSTALLED_ADDON + id, false)) {
1947         continue;
1948       }
1950       // Install the add-on
1951       try {
1952         profileLocation.installAddon(id, entry, null, true);
1953         LOG("Installed distribution add-on " + id);
1955         Services.prefs.setBoolPref(PREF_BRANCH_INSTALLED_ADDON + id, true)
1957         // aManifests may contain a copy of a newly installed add-on's manifest
1958         // and we'll have overwritten that so instead cache our install manifest
1959         // which will later be put into the database in processFileChanges
1960         if (!(KEY_APP_PROFILE in aManifests))
1961           aManifests[KEY_APP_PROFILE] = {};
1962         aManifests[KEY_APP_PROFILE][id] = addon;
1963         changed = true;
1964       }
1965       catch (e) {
1966         ERROR("Failed to install distribution add-on " + entry.path, e);
1967       }
1968     }
1970     entries.close();
1972     return changed;
1973   },
1975   /**
1976    * Compares the add-ons that are currently installed to those that were
1977    * known to be installed when the application last ran and applies any
1978    * changes found to the database. Also sends "startupcache-invalidate" signal to
1979    * observerservice if it detects that data may have changed.
1980    *
1981    * @param  aState
1982    *         The array of current install location states
1983    * @param  aManifests
1984    *         A dictionary of cached AddonInstalls for add-ons that have been
1985    *         installed
1986    * @param  aUpdateCompatibility
1987    *         true to update add-ons appDisabled property when the application
1988    *         version has changed
1989    * @param  aMigrateData
1990    *         an object generated from a previous version of the database
1991    *         holding information about what add-ons were previously userDisabled
1992    *         and updated compatibility information if present
1993    * @param  aActiveBundles
1994    *         When performing recovery after startup this will be an array of
1995    *         persistent descriptors of add-ons that are known to be active,
1996    *         otherwise it will be null
1997    * @return true if a change requiring a restart was detected
1998    */
1999   processFileChanges: function XPI_processFileChanges(aState, aManifests,
2000                                                       aUpdateCompatibility,
2001                                                       aMigrateData,
2002                                                       aActiveBundles) {
2003     let visibleAddons = {};
2004     let oldBootstrappedAddons = this.bootstrappedAddons;
2005     this.bootstrappedAddons = {};
2007     /**
2008      * Updates an add-on's metadata and determines if a restart of the
2009      * application is necessary. This is called when either the add-on's
2010      * install directory path or last modified time has changed.
2011      *
2012      * @param  aInstallLocation
2013      *         The install location containing the add-on
2014      * @param  aOldAddon
2015      *         The AddonInternal as it appeared the last time the application
2016      *         ran
2017      * @param  aAddonState
2018      *         The new state of the add-on
2019      * @return true if restarting the application is required to complete
2020      *         changing this add-on
2021      */
2022     function updateMetadata(aInstallLocation, aOldAddon, aAddonState) {
2023       LOG("Add-on " + aOldAddon.id + " modified in " + aInstallLocation.name);
2025       // Check if there is an updated install manifest for this add-on
2026       let newAddon = aManifests[aInstallLocation.name][aOldAddon.id];
2028       try {
2029         // If not load it
2030         if (!newAddon) {
2031           let file = aInstallLocation.getLocationForID(aOldAddon.id);
2032           newAddon = loadManifestFromFile(file);
2033           // Carry over the userDisabled setting for add-ons that just appeared
2034           newAddon.userDisabled = aOldAddon.userDisabled;
2035         }
2037         // The ID in the manifest that was loaded must match the ID of the old
2038         // add-on.
2039         if (newAddon.id != aOldAddon.id)
2040           throw new Error("Incorrect id in install manifest");
2041       }
2042       catch (e) {
2043         WARN("Add-on is invalid", e);
2044         XPIDatabase.removeAddonMetadata(aOldAddon);
2045         if (!aInstallLocation.locked)
2046           aInstallLocation.uninstallAddon(aOldAddon.id);
2047         else
2048           WARN("Could not uninstall invalid item from locked install location");
2049         // If this was an active add-on then we must force a restart
2050         if (aOldAddon.active)
2051           return true;
2053         return false;
2054       }
2056       // Set the additional properties on the new AddonInternal
2057       newAddon._installLocation = aInstallLocation;
2058       newAddon.updateDate = aAddonState.mtime;
2059       newAddon.visible = !(newAddon.id in visibleAddons);
2061       // Update the database
2062       XPIDatabase.updateAddonMetadata(aOldAddon, newAddon, aAddonState.descriptor);
2063       if (newAddon.visible) {
2064         visibleAddons[newAddon.id] = newAddon;
2066         // If the new add-on is bootstrapped and active then call its install method
2067         if (newAddon.active && newAddon.bootstrap) {
2068           let installReason = Services.vc.compare(aOldAddon.version, newAddon.version) < 0 ?
2069                               BOOTSTRAP_REASONS.ADDON_UPGRADE :
2070                               BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
2072           let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
2073           file.persistentDescriptor = aAddonState.descriptor;
2074           XPIProvider.callBootstrapMethod(newAddon.id, newAddon.version, file,
2075                                           "install", installReason);
2076           return false;
2077         }
2079         // Otherwise the caches will need to be invalidated
2080         return true;
2081       }
2083       return false;
2084     }
2086     /**
2087      * Called when no change has been detected for an add-on's metadata. The
2088      * add-on may have become visible due to other add-ons being removed or
2089      * the add-on may need to be updated when the application version has
2090      * changed.
2091      *
2092      * @param  aInstallLocation
2093      *         The install location containing the add-on
2094      * @param  aOldAddon
2095      *         The AddonInternal as it appeared the last time the application
2096      *         ran
2097      * @param  aAddonState
2098      *         The new state of the add-on
2099      * @return a boolean indicating if restarting the application is required
2100      *         to complete changing this add-on
2101      */
2102     function updateVisibilityAndCompatibility(aInstallLocation, aOldAddon,
2103                                               aAddonState) {
2104       let changed = false;
2106       // This add-ons metadata has not changed but it may have become visible
2107       if (!(aOldAddon.id in visibleAddons)) {
2108         visibleAddons[aOldAddon.id] = aOldAddon;
2110         if (!aOldAddon.visible) {
2111           XPIDatabase.makeAddonVisible(aOldAddon);
2113           if (aOldAddon.bootstrap) {
2114             // The add-on is bootstrappable so call its install script
2115             let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
2116             file.persistentDescriptor = aAddonState.descriptor;
2117             XPIProvider.callBootstrapMethod(aOldAddon.id, aOldAddon.version, file,
2118                                             "install",
2119                                             BOOTSTRAP_REASONS.ADDON_INSTALL);
2121             // If it should be active then mark it as active otherwise unload
2122             // its scope
2123             if (!aOldAddon.appDisabled && !aOldAddon.userDisabled) {
2124               aOldAddon.active = true;
2125               XPIDatabase.updateAddonActive(aOldAddon);
2126             }
2127             else {
2128               XPIProvider.unloadBootstrapScope(newAddon.id);
2129             }
2130           }
2131           else {
2132             // Otherwise a restart is necessary
2133             changed = true;
2134           }
2135         }
2136       }
2138       // App version changed, we may need to update the appDisabled property.
2139       if (aUpdateCompatibility) {
2140         let appDisabled = !isUsableAddon(aOldAddon);
2141         let userDisabled = aOldAddon.userDisabled;
2142         // Sync the userDisabled flag to the selectedSkin
2143         if (aOldAddon.type == "theme")
2144           userDisabled = aOldAddon.internalName != XPIProvider.selectedSkin;
2145         let wasDisabled = aOldAddon.appDisabled || aOldAddon.userDisabled;
2146         let isDisabled = appDisabled || userDisabled;
2148         // Remember add-ons that became appDisabled by the application change
2149         if (aOldAddon.visible && appDisabled && !aOldAddon.appDisabled)
2150           XPIProvider.startupChanges.appDisabled.push(aOldAddon.id);
2152         // If either property has changed update the database.
2153         if (appDisabled != aOldAddon.appDisabled ||
2154             userDisabled != aOldAddon.userDisabled) {
2155           LOG("Add-on " + aOldAddon.id + " changed appDisabled state to " +
2156               appDisabled + " and userDisabled state to " + userDisabled);
2157           XPIDatabase.setAddonProperties(aOldAddon, {
2158             appDisabled: appDisabled,
2159             userDisabled: userDisabled
2160           });
2161         }
2163         // If this is a visible add-on and it has changed disabled state then we
2164         // may need a restart or to update the bootstrap list.
2165         if (aOldAddon.visible && wasDisabled != isDisabled) {
2166           if (aOldAddon.bootstrap) {
2167             // Update the add-ons active state
2168             aOldAddon.active = !isDisabled;
2169             XPIDatabase.updateAddonActive(aOldAddon);
2170           }
2171           else {
2172             changed = true;
2173           }
2174         }
2175       }
2177       if (aOldAddon.visible && aOldAddon.active && aOldAddon.bootstrap) {
2178         XPIProvider.bootstrappedAddons[aOldAddon.id] = {
2179           version: aOldAddon.version,
2180           descriptor: aAddonState.descriptor
2181         };
2182       }
2184       return changed;
2185     }
2187     /**
2188      * Called when an add-on has been removed.
2189      *
2190      * @param  aInstallLocation
2191      *         The install location containing the add-on
2192      * @param  aOldAddon
2193      *         The AddonInternal as it appeared the last time the application
2194      *         ran
2195      * @return a boolean indicating if restarting the application is required
2196      *         to complete changing this add-on
2197      */
2198     function removeMetadata(aInstallLocation, aOldAddon) {
2199       // This add-on has disappeared
2200       LOG("Add-on " + aOldAddon.id + " removed from " + aInstallLocation.name);
2201       XPIDatabase.removeAddonMetadata(aOldAddon);
2202       if (aOldAddon.active) {
2204         // Enable the default theme if the previously active theme has been
2205         // removed
2206         if (aOldAddon.type == "theme")
2207           XPIProvider.enableDefaultTheme();
2209         // If this was not a bootstrapped add-on then we must force a restart.
2210         if (!aOldAddon.bootstrap)
2211           return true;
2212       }
2214       return false;
2215     }
2217     /**
2218      * Called when a new add-on has been detected.
2219      *
2220      * @param  aInstallLocation
2221      *         The install location containing the add-on
2222      * @param  aId
2223      *         The ID of the add-on
2224      * @param  aAddonState
2225      *         The new state of the add-on
2226      * @param  aMigrateData
2227      *         If during startup the database had to be upgraded this will
2228      *         contain data that used to be held about this add-on
2229      * @return a boolean indicating if restarting the application is required
2230      *         to complete changing this add-on
2231      */
2232     function addMetadata(aInstallLocation, aId, aAddonState, aMigrateData) {
2233       LOG("New add-on " + aId + " installed in " + aInstallLocation.name);
2235       let newAddon = null;
2236       // Check the updated manifests lists for the install location, If there
2237       // is no manifest for the add-on ID then newAddon will be undefined
2238       if (aInstallLocation.name in aManifests)
2239         newAddon = aManifests[aInstallLocation.name][aId];
2241       try {
2242         // Otherwise load the manifest from the add-on
2243         if (!newAddon) {
2244           let file = aInstallLocation.getLocationForID(aId);
2245           newAddon = loadManifestFromFile(file);
2246         }
2247         // The add-on in the manifest should match the add-on ID.
2248         if (newAddon.id != aId)
2249           throw new Error("Incorrect id in install manifest");
2250       }
2251       catch (e) {
2252         WARN("Add-on is invalid", e);
2254         // Remove the invalid add-on from the install location if the install
2255         // location isn't locked, no restart will be necessary
2256         if (!aInstallLocation.locked)
2257           aInstallLocation.uninstallAddon(aId);
2258         else
2259           WARN("Could not uninstall invalid item from locked install location");
2260         return false;
2261       }
2263       // Update the AddonInternal properties.
2264       newAddon._installLocation = aInstallLocation;
2265       newAddon.visible = !(newAddon.id in visibleAddons);
2266       newAddon.installDate = aAddonState.mtime;
2267       newAddon.updateDate = aAddonState.mtime;
2269       // If there is migration data then apply it.
2270       if (aMigrateData) {
2271         // A theme's disabled state is determined by the selected theme
2272         // preference which is read in loadManifestFromRDF
2273         if (newAddon.type != "theme")
2274           newAddon.userDisabled = aMigrateData.userDisabled;
2275         if ("installDate" in aMigrateData)
2276           newAddon.installDate = aMigrateData.installDate;
2278         // Some properties should only be migrated if the add-on hasn't changed.
2279         // The version property isn't a perfect check for this but covers the
2280         // vast majority of cases.
2281         if (aMigrateData.version == newAddon.version) {
2282           if ("targetApplications" in aMigrateData)
2283             newAddon.applyCompatibilityUpdate(aMigrateData, true);
2284         }
2285       }
2287       // If we have a list of what add-ons should be marked as active then use it
2288       if (aActiveBundles) {
2289         // For themes we know which is active by the current skin setting
2290         if (newAddon.type == "theme")
2291           newAddon.active = newAddon.internalName == XPIProvider.currentSkin;
2292         else
2293           newAddon.active = aActiveBundles.indexOf(aAddonState.descriptor) != -1;
2295         // If the add-on isn't active and it isn't appDisabled then it is
2296         // probably userDisabled
2297         if (!newAddon.active && newAddon.visible && !newAddon.appDisabled)
2298           newAddon.userDisabled = true;
2299       }
2300       else {
2301         newAddon.active = (newAddon.visible && !newAddon.userDisabled &&
2302                            !newAddon.appDisabled)
2303       }
2305       try {
2306         // Update the database.
2307         XPIDatabase.addAddonMetadata(newAddon, aAddonState.descriptor);
2308       }
2309       catch (e) {
2310         // Failing to write the add-on into the database is non-fatal, the
2311         // add-on will just be unavailable until we try again in a subsequent
2312         // startup
2313         ERROR("Failed to add add-on " + aId + " in " + aInstallLocation.name +
2314               " to database", e);
2315         return false;
2316       }
2318       if (newAddon.visible) {
2319         // Note if any visible add-on is not in the application install location
2320         if (newAddon._installLocation.name != KEY_APP_GLOBAL)
2321           XPIProvider.allAppGlobal = false;
2323         visibleAddons[newAddon.id] = newAddon;
2325         let installReason = BOOTSTRAP_REASONS.ADDON_INSTALL;
2327         // If we're hiding a bootstrapped add-on then call its uninstall method
2328         if (newAddon.id in oldBootstrappedAddons) {
2329           let oldBootstrap = oldBootstrappedAddons[newAddon.id];
2330           XPIProvider.bootstrappedAddons[newAddon.id] = oldBootstrap;
2332           installReason = Services.vc.compare(oldBootstrap.version, newAddon.version) < 0 ?
2333                           BOOTSTRAP_REASONS.ADDON_UPGRADE :
2334                           BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
2336           let oldAddonFile = Cc["@mozilla.org/file/local;1"].
2337                              createInstance(Ci.nsILocalFile);
2338           oldAddonFile.persistentDescriptor = oldBootstrap.descriptor;
2339           XPIProvider.callBootstrapMethod(newAddon.id, oldBootstrap.version,
2340                                           oldAddonFile, "uninstall",
2341                                           installReason);
2342           XPIProvider.unloadBootstrapScope(newAddon.id);
2343         }
2345         if (!newAddon.bootstrap)
2346           return true;
2348         // Visible bootstrapped add-ons need to have their install method called
2349         let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
2350         file.persistentDescriptor = aAddonState.descriptor;
2351         XPIProvider.callBootstrapMethod(newAddon.id, newAddon.version, file,
2352                                         "install", installReason);
2353         if (!newAddon.active)
2354           XPIProvider.unloadBootstrapScope(newAddon.id);
2355       }
2357       return false;
2358     }
2360     let changed = false;
2361     let knownLocations = XPIDatabase.getInstallLocations();
2363     // The install locations are iterated in reverse order of priority so when
2364     // there are multiple add-ons installed with the same ID the one that
2365     // should be visible is the first one encountered.
2366     aState.reverse().forEach(function(aSt) {
2368       // We can't include the install location directly in the state as it has
2369       // to be cached as JSON.
2370       let installLocation = this.installLocationsByName[aSt.name];
2371       let addonStates = aSt.addons;
2373       // Check if the database knows about any add-ons in this install location.
2374       let pos = knownLocations.indexOf(installLocation.name);
2375       if (pos >= 0) {
2376         knownLocations.splice(pos, 1);
2377         let addons = XPIDatabase.getAddonsInLocation(installLocation.name);
2378         // Iterate through the add-ons installed the last time the application
2379         // ran
2380         addons.forEach(function(aOldAddon) {
2381           // Check if the add-on is still installed
2382           if (aOldAddon.id in addonStates) {
2383             let addonState = addonStates[aOldAddon.id];
2384             delete addonStates[aOldAddon.id];
2386             // Remember add-ons that were inactive during startup
2387             if (aOldAddon.visible && !aOldAddon.active)
2388               XPIProvider.inactiveAddonIDs.push(aOldAddon.id);
2390             // The add-on has changed if the modification time has changed, or
2391             // the directory it is installed in has changed or we have an
2392             // updated manifest for it. Also reload the metadata for add-ons
2393             // in the application directory when the application version has
2394             // changed
2395             if (aOldAddon.id in aManifests[installLocation.name] ||
2396                 aOldAddon.updateDate != addonState.mtime ||
2397                 aOldAddon._descriptor != addonState.descriptor ||
2398                 (aUpdateCompatibility && installLocation.name == KEY_APP_GLOBAL)) {
2399               changed = updateMetadata(installLocation, aOldAddon, addonState) ||
2400                         changed;
2401             }
2402             else {
2403               changed = updateVisibilityAndCompatibility(installLocation,
2404                                                          aOldAddon, addonState) ||
2405                         changed;
2406             }
2407             if (aOldAddon.visible && aOldAddon._installLocation.name != KEY_APP_GLOBAL)
2408               XPIProvider.allAppGlobal = false;
2409           }
2410           else {
2411             changed = removeMetadata(installLocation, aOldAddon) || changed;
2412           }
2413         }, this);
2414       }
2416       // All the remaining add-ons in this install location must be new.
2418       // Get the migration data for this install location.
2419       let locMigrateData = {};
2420       if (aMigrateData && installLocation.name in aMigrateData)
2421         locMigrateData = aMigrateData[installLocation.name];
2422       for (let id in addonStates) {
2423         changed = addMetadata(installLocation, id, addonStates[id],
2424                               locMigrateData[id]) || changed;
2425       }
2426     }, this);
2428     // The remaining locations that had add-ons installed in them no longer
2429     // have any add-ons installed in them, or the locations no longer exist.
2430     // The metadata for the add-ons that were in them must be removed from the
2431     // database.
2432     knownLocations.forEach(function(aLocation) {
2433       let addons = XPIDatabase.getAddonsInLocation(aLocation);
2434       addons.forEach(function(aOldAddon) {
2435         changed = removeMetadata(aLocation, aOldAddon) || changed;
2436       }, this);
2437     }, this);
2439     // Cache the new install location states
2440     cache = JSON.stringify(this.getInstallLocationStates());
2441     Services.prefs.setCharPref(PREF_INSTALL_CACHE, cache);
2443     return changed;
2444   },
2446   /**
2447    * Imports the xpinstall permissions from preferences into the permissions
2448    * manager for the user to change later.
2449    */
2450   importPermissions: function XPI_importPermissions() {
2451     function importList(aPrefBranch, aAction) {
2452       let list = Services.prefs.getChildList(aPrefBranch, {});
2453       list.forEach(function(aPref) {
2454         let hosts = Prefs.getCharPref(aPref, "");
2455         if (!hosts)
2456           return;
2458         hosts.split(",").forEach(function(aHost) {
2459           Services.perms.add(NetUtil.newURI("http://" + aHost), XPI_PERMISSION,
2460                              aAction);
2461         });
2463         Services.prefs.setCharPref(aPref, "");
2464       });
2465     }
2467     importList(PREF_XPI_WHITELIST_PERMISSIONS,
2468                Ci.nsIPermissionManager.ALLOW_ACTION);
2469     importList(PREF_XPI_BLACKLIST_PERMISSIONS,
2470                Ci.nsIPermissionManager.DENY_ACTION);
2471   },
2473   /**
2474    * Checks for any changes that have occurred since the last time the
2475    * application was launched.
2476    *
2477    * @param  aAppChanged
2478    *         A tri-state value. Undefined means the current profile was created
2479    *         for this session, true means the profile already existed but was
2480    *         last used with an application with a different version number,
2481    *         false means that the profile was last used by this version of the
2482    *         application.
2483    * @return true if a change requiring a restart was detected
2484    */
2485   checkForChanges: function XPI_checkForChanges(aAppChanged) {
2486     LOG("checkForChanges");
2488     // Import the website installation permissions if the application has changed
2489     if (aAppChanged !== false)
2490       this.importPermissions();
2492     // If the application version has changed then the database information
2493     // needs to be updated
2494     let updateDatabase = aAppChanged;
2496     // First install any new add-ons into the locations, if there are any
2497     // changes then we must update the database with the information in the
2498     // install locations
2499     let manifests = {};
2500     updateDatabase = this.processPendingFileChanges(manifests) | updateDatabase;
2502     // This will be true if the previous session made changes that affect the
2503     // active state of add-ons but didn't commit them properly (normally due
2504     // to the application crashing)
2505     let hasPendingChanges = Prefs.getBoolPref(PREF_PENDING_OPERATIONS);
2507     // If the schema appears to have changed then we should update the database
2508     updateDatabase |= DB_SCHEMA != Prefs.getIntPref(PREF_DB_SCHEMA, 0);
2510     // If the application has changed then check for new distribution add-ons
2511     if (aAppChanged !== false &&
2512         Prefs.getBoolPref(PREF_INSTALL_DISTRO_ADDONS, true))
2513       updateDatabase = this.installDistributionAddons(manifests) | updateDatabase;
2515     // Load the list of bootstrapped add-ons first so processFileChanges can
2516     // modify it
2517     this.bootstrappedAddons = JSON.parse(Prefs.getCharPref(PREF_BOOTSTRAP_ADDONS,
2518                                          "{}"));
2520     let state = this.getInstallLocationStates();
2522     // If the database exists then the previous file cache can be trusted
2523     // otherwise the database needs to be recreated
2524     let dbFile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_DATABASE], true);
2525     updateDatabase |= !dbFile.exists();
2526     if (!updateDatabase) {
2527       // If the state has changed then we must update the database
2528       let cache = Prefs.getCharPref(PREF_INSTALL_CACHE, null);
2529       updateDatabase |= cache != JSON.stringify(state);
2530     }
2532     if (!updateDatabase) {
2533       let bootstrapDescriptors = [this.bootstrappedAddons[b].descriptor
2534                                   for (b in this.bootstrappedAddons)];
2536       state.forEach(function(aInstallLocationState) {
2537         for (let id in aInstallLocationState.addons) {
2538           let pos = bootstrapDescriptors.indexOf(aInstallLocationState.addons[id].descriptor);
2539           if (pos != -1)
2540             bootstrapDescriptors.splice(pos, 1);
2541         }
2542       });
2543   
2544       if (bootstrapDescriptors.length > 0) {
2545         WARN("Bootstrap state is invalid (missing add-ons: " + bootstrapDescriptors.toSource() + ")");
2546         updateDatabase = true;
2547       }
2548     }
2550     // Catch any errors during the main startup and rollback the database changes
2551     XPIDatabase.beginTransaction();
2552     try {
2553       let extensionListChanged = false;
2554       // If the database needs to be updated then open it and then update it
2555       // from the filesystem
2556       if (updateDatabase || hasPendingChanges) {
2557         let migrateData = XPIDatabase.openConnection(false);
2559         try {
2560           extensionListChanged = this.processFileChanges(state, manifests,
2561                                                          aAppChanged,
2562                                                          migrateData, null);
2563         }
2564         catch (e) {
2565           ERROR("Error processing file changes", e);
2566         }
2567       }
2569       if (aAppChanged) {
2570         // When upgrading the app and using a custom skin make sure it is still
2571         // compatible otherwise switch back the default
2572         if (this.currentSkin != this.defaultSkin) {
2573           let oldSkin = XPIDatabase.getVisibleAddonForInternalName(this.currentSkin);
2574           if (!oldSkin || oldSkin.appDisabled)
2575             this.enableDefaultTheme();
2576         }
2578         // When upgrading remove the old extensions cache to force older
2579         // versions to rescan the entire list of extensions
2580         let oldCache = FileUtils.getFile(KEY_PROFILEDIR, [FILE_OLD_CACHE], true);
2581         if (oldCache.exists())
2582           oldCache.remove(true);
2583       }
2585       // If the application crashed before completing any pending operations then
2586       // we should perform them now.
2587       if (extensionListChanged || hasPendingChanges) {
2588         LOG("Updating database with changes to installed add-ons");
2589         XPIDatabase.updateActiveAddons();
2590         XPIDatabase.commitTransaction();
2591         XPIDatabase.writeAddonsList();
2592         Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, false);
2593         Services.prefs.setCharPref(PREF_BOOTSTRAP_ADDONS,
2594                                    JSON.stringify(this.bootstrappedAddons));
2595         return true;
2596       }
2598       LOG("No changes found");
2599       XPIDatabase.commitTransaction();
2600     }
2601     catch (e) {
2602       ERROR("Error during startup file checks, rolling back any database " +
2603             "changes", e);
2604       XPIDatabase.rollbackTransaction();
2605     }
2607     // Check that the add-ons list still exists
2608     let addonsList = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST],
2609                                        true);
2610     if (!addonsList.exists()) {
2611       LOG("Add-ons list is missing, recreating");
2612       XPIDatabase.writeAddonsList();
2613     }
2615     return false;
2616   },
2618   /**
2619    * Called to test whether this provider supports installing a particular
2620    * mimetype.
2621    *
2622    * @param  aMimetype
2623    *         The mimetype to check for
2624    * @return true if the mimetype is application/x-xpinstall
2625    */
2626   supportsMimetype: function XPI_supportsMimetype(aMimetype) {
2627     return aMimetype == "application/x-xpinstall";
2628   },
2630   /**
2631    * Called to test whether installing XPI add-ons is enabled.
2632    *
2633    * @return true if installing is enabled
2634    */
2635   isInstallEnabled: function XPI_isInstallEnabled() {
2636     // Default to enabled if the preference does not exist
2637     return Prefs.getBoolPref(PREF_XPI_ENABLED, true);
2638   },
2640   /**
2641    * Called to test whether installing XPI add-ons from a URI is allowed.
2642    *
2643    * @param  aUri
2644    *         The URI being installed from
2645    * @return true if installing is allowed
2646    */
2647   isInstallAllowed: function XPI_isInstallAllowed(aUri) {
2648     if (!this.isInstallEnabled())
2649       return false;
2651     if (!aUri)
2652       return true;
2654     // file: and chrome: don't need whitelisted hosts
2655     if (aUri.schemeIs("chrome") || aUri.schemeIs("file"))
2656       return true;
2659     let permission = Services.perms.testPermission(aUri, XPI_PERMISSION);
2660     if (permission == Ci.nsIPermissionManager.DENY_ACTION)
2661       return false;
2663     let requireWhitelist = Prefs.getBoolPref(PREF_XPI_WHITELIST_REQUIRED, true);
2664     if (requireWhitelist && (permission != Ci.nsIPermissionManager.ALLOW_ACTION))
2665       return false;
2667     return true;
2668   },
2670   /**
2671    * Called to get an AddonInstall to download and install an add-on from a URL.
2672    *
2673    * @param  aUrl
2674    *         The URL to be installed
2675    * @param  aHash
2676    *         A hash for the install
2677    * @param  aName
2678    *         A name for the install
2679    * @param  aIconURL
2680    *         An icon URL for the install
2681    * @param  aVersion
2682    *         A version for the install
2683    * @param  aLoadGroup
2684    *         An nsILoadGroup to associate requests with
2685    * @param  aCallback
2686    *         A callback to pass the AddonInstall to
2687    */
2688   getInstallForURL: function XPI_getInstallForURL(aUrl, aHash, aName, aIconURL,
2689                                                   aVersion, aLoadGroup, aCallback) {
2690     AddonInstall.createDownload(function(aInstall) {
2691       aCallback(aInstall.wrapper);
2692     }, aUrl, aHash, aName, aIconURL, aVersion, aLoadGroup);
2693   },
2695   /**
2696    * Called to get an AddonInstall to install an add-on from a local file.
2697    *
2698    * @param  aFile
2699    *         The file to be installed
2700    * @param  aCallback
2701    *         A callback to pass the AddonInstall to
2702    */
2703   getInstallForFile: function XPI_getInstallForFile(aFile, aCallback) {
2704     AddonInstall.createInstall(function(aInstall) {
2705       if (aInstall)
2706         aCallback(aInstall.wrapper);
2707       else
2708         aCallback(null);
2709     }, aFile);
2710   },
2712   /**
2713    * Removes an AddonInstall from the list of active installs.
2714    *
2715    * @param  install
2716    *         The AddonInstall to remove
2717    */
2718   removeActiveInstall: function XPI_removeActiveInstall(aInstall) {
2719     this.installs = this.installs.filter(function(i) i != aInstall);
2720   },
2722   /**
2723    * Called to get an Addon with a particular ID.
2724    *
2725    * @param  aId
2726    *         The ID of the add-on to retrieve
2727    * @param  aCallback
2728    *         A callback to pass the Addon to
2729    */
2730   getAddonByID: function XPI_getAddonByID(aId, aCallback) {
2731     XPIDatabase.getVisibleAddonForID(aId, function(aAddon) {
2732       if (aAddon)
2733         aCallback(createWrapper(aAddon));
2734       else
2735         aCallback(null);
2736     });
2737   },
2739   /**
2740    * Called to get Addons of a particular type.
2741    *
2742    * @param  aTypes
2743    *         An array of types to fetch. Can be null to get all types.
2744    * @param  aCallback
2745    *         A callback to pass an array of Addons to
2746    */
2747   getAddonsByTypes: function XPI_getAddonsByTypes(aTypes, aCallback) {
2748     XPIDatabase.getVisibleAddons(aTypes, function(aAddons) {
2749       aCallback([createWrapper(a) for each (a in aAddons)]);
2750     });
2751   },
2753   /**
2754    * Called to get Addons that have pending operations.
2755    *
2756    * @param  aTypes
2757    *         An array of types to fetch. Can be null to get all types
2758    * @param  aCallback
2759    *         A callback to pass an array of Addons to
2760    */
2761   getAddonsWithOperationsByTypes:
2762   function XPI_getAddonsWithOperationsByTypes(aTypes, aCallback) {
2763     XPIDatabase.getVisibleAddonsWithPendingOperations(aTypes, function(aAddons) {
2764       let results = [createWrapper(a) for each (a in aAddons)];
2765       XPIProvider.installs.forEach(function(aInstall) {
2766         if (aInstall.state == AddonManager.STATE_INSTALLED &&
2767             !(aInstall.addon instanceof DBAddonInternal))
2768           results.push(createWrapper(aInstall.addon));
2769       });
2770       aCallback(results);
2771     });
2772   },
2774   /**
2775    * Called to get the current AddonInstalls, optionally limiting to a list of
2776    * types.
2777    *
2778    * @param  aTypes
2779    *         An array of types or null to get all types
2780    * @param  aCallback
2781    *         A callback to pass the array of AddonInstalls to
2782    */
2783   getInstallsByTypes: function XPI_getInstallsByTypes(aTypes, aCallback) {
2784     let results = [];
2785     this.installs.forEach(function(aInstall) {
2786       if (!aTypes || aTypes.indexOf(aInstall.type) >= 0)
2787         results.push(aInstall.wrapper);
2788     });
2789     aCallback(results);
2790   },
2792   /**
2793    * Called when a new add-on has been enabled when only one add-on of that type
2794    * can be enabled.
2795    *
2796    * @param  aId
2797    *         The ID of the newly enabled add-on
2798    * @param  aType
2799    *         The type of the newly enabled add-on
2800    * @param  aPendingRestart
2801    *         true if the newly enabled add-on will only become enabled after a
2802    *         restart
2803    */
2804   addonChanged: function XPI_addonChanged(aId, aType, aPendingRestart) {
2805     // We only care about themes in this provider
2806     if (aType != "theme")
2807       return;
2809     if (!aId) {
2810       // Fallback to the default theme when no theme was enabled
2811       this.enableDefaultTheme();
2812       return;
2813     }
2815     // Look for the previously enabled theme and find the internalName of the
2816     // currently selected theme
2817     let previousTheme = null;
2818     let newSkin = this.defaultSkin;
2819     let addons = XPIDatabase.getAddonsByType("theme");
2820     addons.forEach(function(aTheme) {
2821       if (!aTheme.visible)
2822         return;
2823       if (aTheme.id == aId)
2824         newSkin = aTheme.internalName;
2825       else if (aTheme.userDisabled == false && !aTheme.pendingUninstall)
2826         previousTheme = aTheme;
2827     }, this);
2829     if (aPendingRestart) {
2830       Services.prefs.setBoolPref(PREF_DSS_SWITCHPENDING, true);
2831       Services.prefs.setCharPref(PREF_DSS_SKIN_TO_SELECT, newSkin);
2832     }
2833     else if (newSkin == this.currentSkin) {
2834       try {
2835         Services.prefs.clearUserPref(PREF_DSS_SWITCHPENDING);
2836       }
2837       catch (e) { }
2838       try {
2839         Services.prefs.clearUserPref(PREF_DSS_SKIN_TO_SELECT);
2840       }
2841       catch (e) { }
2842     }
2843     else {
2844       Services.prefs.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN, newSkin);
2845       this.currentSkin = newSkin;
2846     }
2847     this.selectedSkin = newSkin;
2849     // Flush the preferences to disk so they don't get out of sync with the
2850     // database
2851     Services.prefs.savePrefFile(null);
2853     // Mark the previous theme as disabled. This won't cause recursion since
2854     // only enabled calls notifyAddonChanged.
2855     if (previousTheme)
2856       this.updateAddonDisabledState(previousTheme, true);
2857   },
2859   /**
2860    * Update the appDisabled property for all add-ons.
2861    */
2862   updateAddonAppDisabledStates: function XPI_updateAddonAppDisabledStates() {
2863     let addons = XPIDatabase.getAddons();
2864     addons.forEach(function(aAddon) {
2865       this.updateAddonDisabledState(aAddon);
2866     }, this);
2867   },
2869   /**
2870    * When the previously selected theme is removed this method will be called
2871    * to enable the default theme.
2872    */
2873   enableDefaultTheme: function XPI_enableDefaultTheme() {
2874     LOG("Activating default theme");
2875     let addon = XPIDatabase.getVisibleAddonForInternalName(this.defaultSkin);
2876     if (addon)
2877       this.updateAddonDisabledState(addon, false);
2878     else
2879       WARN("Unable to activate the default theme");
2880   },
2882   /**
2883    * Notified when a preference we're interested in has changed.
2884    *
2885    * @see nsIObserver
2886    */
2887   observe: function XPI_observe(aSubject, aTopic, aData) {
2888     switch (aData) {
2889     case this.checkCompatibilityPref:
2890     case PREF_EM_CHECK_UPDATE_SECURITY:
2891       this.checkCompatibility = Prefs.getBoolPref(this.checkCompatibilityPref,
2892                                                   true);
2893       this.checkUpdateSecurity = Prefs.getBoolPref(PREF_EM_CHECK_UPDATE_SECURITY,
2894                                                    true);
2895       this.updateAllAddonDisabledStates();
2896       break;
2897     }
2898   },
2900   /**
2901    * Tests whether enabling an add-on will require a restart.
2902    *
2903    * @param  aAddon
2904    *         The add-on to test
2905    * @return true if the operation requires a restart
2906    */
2907   enableRequiresRestart: function XPI_enableRequiresRestart(aAddon) {
2908     // If the platform couldn't have activated extensions then we can make
2909     // changes without any restart.
2910     if (!this.extensionsActive)
2911       return false;
2913     // If the application is in safe mode then any change can be made without
2914     // restarting
2915     if (Services.appinfo.inSafeMode)
2916       return false;
2918     // Anything that is active is already enabled
2919     if (aAddon.active)
2920       return false;
2922     if (aAddon.type == "theme") {
2923       // If dynamic theme switching is enabled then switching themes does not
2924       // require a restart
2925       if (Prefs.getBoolPref(PREF_EM_DSS_ENABLED))
2926         return false;
2928       // If the theme is already the theme in use then no restart is necessary.
2929       // This covers the case where the default theme is in use but a
2930       // lightweight theme is considered active.
2931       return aAddon.internalName != this.currentSkin;
2932     }
2934     return !aAddon.bootstrap;
2935   },
2937   /**
2938    * Tests whether disabling an add-on will require a restart.
2939    *
2940    * @param  aAddon
2941    *         The add-on to test
2942    * @return true if the operation requires a restart
2943    */
2944   disableRequiresRestart: function XPI_disableRequiresRestart(aAddon) {
2945     // If the platform couldn't have activated up extensions then we can make
2946     // changes without any restart.
2947     if (!this.extensionsActive)
2948       return false;
2950     // If the application is in safe mode then any change can be made without
2951     // restarting
2952     if (Services.appinfo.inSafeMode)
2953       return false;
2955     // Anything that isn't active is already disabled
2956     if (!aAddon.active)
2957       return false;
2959     if (aAddon.type == "theme") {
2960       // If dynamic theme switching is enabled then switching themes does not
2961       // require a restart
2962       if (Prefs.getBoolPref(PREF_EM_DSS_ENABLED))
2963         return false;
2965       // Non-default themes always require a restart to disable since it will
2966       // be switching from one theme to another or to the default theme and a
2967       // lightweight theme.
2968       if (aAddon.internalName != this.defaultSkin)
2969         return true;
2971       // The default theme requires a restart to disable if we are in the
2972       // process of switching to a different theme. Note that this makes the
2973       // disabled flag of operationsRequiringRestart incorrect for the default
2974       // theme (it will be false most of the time). Bug 520124 would be required
2975       // to fix it. For the UI this isn't a problem since we never try to
2976       // disable or uninstall the default theme.
2977       return this.selectedSkin != this.currentSkin;
2978     }
2980     return !aAddon.bootstrap;
2981   },
2983   /**
2984    * Tests whether installing an add-on will require a restart.
2985    *
2986    * @param  aAddon
2987    *         The add-on to test
2988    * @return true if the operation requires a restart
2989    */
2990   installRequiresRestart: function XPI_installRequiresRestart(aAddon) {
2991     // If the platform couldn't have activated up extensions then we can make
2992     // changes without any restart.
2993     if (!this.extensionsActive)
2994       return false;
2996     // If the application is in safe mode then any change can be made without
2997     // restarting
2998     if (Services.appinfo.inSafeMode)
2999       return false;
3001     // Add-ons that are already installed don't require a restart to install.
3002     // This wouldn't normally be called for an already installed add-on (except
3003     // for forming the operationsRequiringRestart flags) so is really here as
3004     // a safety measure.
3005     if (aAddon instanceof DBAddonInternal)
3006       return false;
3008     // If we have an AddonInstall for this add-on then we can see if there is
3009     // an existing installed add-on with the same ID
3010     if ("_install" in aAddon && aAddon._install) {
3011       // If there is an existing installed add-on and uninstalling it would
3012       // require a restart then installing the update will also require a
3013       // restart
3014       let existingAddon = aAddon._install.existingAddon;
3015       if (existingAddon && this.uninstallRequiresRestart(existingAddon))
3016         return true;
3017     }
3019     // If the add-on is not going to be active after installation then it
3020     // doesn't require a restart to install.
3021     if (aAddon.userDisabled || aAddon.appDisabled)
3022       return false;
3024     // Themes will require a restart (even if dynamic switching is enabled due
3025     // to some caching issues) and non-bootstrapped add-ons will require a
3026     // restart
3027     return aAddon.type == "theme" || !aAddon.bootstrap;
3028   },
3030   /**
3031    * Tests whether uninstalling an add-on will require a restart.
3032    *
3033    * @param  aAddon
3034    *         The add-on to test
3035    * @return true if the operation requires a restart
3036    */
3037   uninstallRequiresRestart: function XPI_uninstallRequiresRestart(aAddon) {
3038     // If the platform couldn't have activated up extensions then we can make
3039     // changes without any restart.
3040     if (!this.extensionsActive)
3041       return false;
3043     // If the application is in safe mode then any change can be made without
3044     // restarting
3045     if (Services.appinfo.inSafeMode)
3046       return false;
3048     // If the add-on can be disabled without a restart then it can also be
3049     // uninstalled without a restart
3050     return this.disableRequiresRestart(aAddon);
3051   },
3053   /**
3054    * Loads a bootstrapped add-on's bootstrap.js into a sandbox and the reason
3055    * values as constants in the scope. This will also add information about the
3056    * add-on to the bootstrappedAddons dictionary and notify the crash reporter
3057    * that new add-ons have been loaded.
3058    *
3059    * @param  aId
3060    *         The add-on's ID
3061    * @param  aFile
3062    *         The nsILocalFile for the add-on
3063    * @param  aVersion
3064    *         The add-on's version
3065    * @return a JavaScript scope
3066    */
3067   loadBootstrapScope: function XPI_loadBootstrapScope(aId, aFile, aVersion) {
3068     LOG("Loading bootstrap scope from " + aFile.path);
3069     // Mark the add-on as active for the crash reporter before loading
3070     this.bootstrappedAddons[aId] = {
3071       version: aVersion,
3072       descriptor: aFile.persistentDescriptor
3073     };
3074     this.addAddonsToCrashReporter();
3076     let principal = Cc["@mozilla.org/systemprincipal;1"].
3077                     createInstance(Ci.nsIPrincipal);
3078     this.bootstrapScopes[aId] = new Components.utils.Sandbox(principal);
3080     let bootstrap = aFile.clone();
3081     let name = aFile.leafName;
3082     let spec;
3084     if (!bootstrap.exists()) {
3085       ERROR("Attempted to load bootstrap scope from missing directory " + bootstrap.path);
3086       return;
3087     }
3089     if (bootstrap.isDirectory()) {
3090       bootstrap.append("bootstrap.js");
3091       let uri = Services.io.newFileURI(bootstrap);
3092       spec = uri.spec;
3093     } else {
3094       spec = buildJarURI(bootstrap, "bootstrap.js").spec;
3095     }
3096     if (bootstrap.exists()) {
3097       let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
3098                    createInstance(Ci.mozIJSSubScriptLoader);
3100       try {
3101         // As we don't want our caller to control the JS version used for the
3102         // bootstrap file, we run loadSubScript within the context of the
3103         // sandbox with the latest JS version set explicitly.
3104         this.bootstrapScopes[aId].__SCRIPT_URI_SPEC__ = spec;
3105         Components.utils.evalInSandbox(
3106           "Components.classes['@mozilla.org/moz/jssubscript-loader;1'] \
3107                      .createInstance(Components.interfaces.mozIJSSubScriptLoader) \
3108                      .loadSubScript(__SCRIPT_URI_SPEC__);", this.bootstrapScopes[aId], "ECMAv5");
3109       }
3110       catch (e) {
3111         WARN("Error loading bootstrap.js for " + aId, e);
3112       }
3114       // Copy the reason values from the global object into the bootstrap scope.
3115       for (let name in BOOTSTRAP_REASONS)
3116         this.bootstrapScopes[aId][name] = BOOTSTRAP_REASONS[name];
3117     }
3118     else {
3119       WARN("Bootstrap missing for " + aId);
3120     }
3121   },
3123   /**
3124    * Unloads a bootstrap scope by dropping all references to it and then
3125    * updating the list of active add-ons with the crash reporter.
3126    *
3127    * @param  aId
3128    *         The add-on's ID
3129    */
3130   unloadBootstrapScope: function XPI_unloadBootstrapScope(aId) {
3131     delete this.bootstrapScopes[aId];
3132     delete this.bootstrappedAddons[aId];
3133     this.addAddonsToCrashReporter();
3134   },
3136   /**
3137    * Calls a bootstrap method for an add-on.
3138    *
3139    * @param  aId
3140    *         The ID of the add-on
3141    * @param  aVersion
3142    *         The version of the add-on
3143    * @param  aFile
3144    *         The nsILocalFile for the add-on
3145    * @param  aMethod
3146    *         The name of the bootstrap method to call
3147    * @param  aReason
3148    *         The reason flag to pass to the bootstrap's startup method
3149    */
3150   callBootstrapMethod: function XPI_callBootstrapMethod(aId, aVersion, aFile,
3151                                                         aMethod, aReason) {
3152     // Never call any bootstrap methods in safe mode
3153     if (Services.appinfo.inSafeMode)
3154       return;
3156     // Load the scope if it hasn't already been loaded
3157     if (!(aId in this.bootstrapScopes))
3158       this.loadBootstrapScope(aId, aFile, aVersion);
3160     if (!(aMethod in this.bootstrapScopes[aId])) {
3161       WARN("Add-on " + aId + " is missing bootstrap method " + aMethod);
3162       return;
3163     }
3165     let params = {
3166       id: aId,
3167       version: aVersion,
3168       installPath: aFile.clone()
3169     };
3171     LOG("Calling bootstrap method " + aMethod + " on " + aId + " version " +
3172         aVersion);
3173     try {
3174       this.bootstrapScopes[aId][aMethod](params, aReason);
3175     }
3176     catch (e) {
3177       WARN("Exception running bootstrap method " + aMethod + " on " +
3178            aId, e);
3179     }
3180   },
3182   /**
3183    * Updates the appDisabled property for all add-ons.
3184    */
3185   updateAllAddonDisabledStates: function XPI_updateAllAddonDisabledStates() {
3186     let addons = XPIDatabase.getAddons();
3187     addons.forEach(function(aAddon) {
3188       this.updateAddonDisabledState(aAddon);
3189     }, this);
3190   },
3192   /**
3193    * Updates the disabled state for an add-on. Its appDisabled property will be
3194    * calculated and if the add-on is changed appropriate notifications will be
3195    * sent out to the registered AddonListeners.
3196    *
3197    * @param  aAddon
3198    *         The DBAddonInternal to update
3199    * @param  aUserDisabled
3200    *         Value for the userDisabled property. If undefined the value will
3201    *         not change
3202    * @throws if addon is not a DBAddonInternal
3203    */
3204   updateAddonDisabledState: function XPI_updateAddonDisabledState(aAddon,
3205                                                                   aUserDisabled) {
3206     if (!(aAddon instanceof DBAddonInternal))
3207       throw new Error("Can only update addon states for installed addons.");
3209     if (aUserDisabled === undefined)
3210       aUserDisabled = aAddon.userDisabled;
3212     let appDisabled = !isUsableAddon(aAddon);
3213     // No change means nothing to do here
3214     if (aAddon.userDisabled == aUserDisabled &&
3215         aAddon.appDisabled == appDisabled)
3216       return;
3218     let wasDisabled = aAddon.userDisabled || aAddon.appDisabled;
3219     let isDisabled = aUserDisabled || appDisabled;
3221     // Update the properties in the database
3222     XPIDatabase.setAddonProperties(aAddon, {
3223       userDisabled: aUserDisabled,
3224       appDisabled: appDisabled
3225     });
3227     // If the add-on is not visible or the add-on is not changing state then
3228     // there is no need to do anything else
3229     if (!aAddon.visible || (wasDisabled == isDisabled))
3230       return;
3232     // Flag that active states in the database need to be updated on shutdown
3233     Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
3235     let wrapper = createWrapper(aAddon);
3236     // Have we just gone back to the current state?
3237     if (isDisabled != aAddon.active) {
3238       AddonManagerPrivate.callAddonListeners("onOperationCancelled", wrapper);
3239     }
3240     else {
3241       if (isDisabled) {
3242         var needsRestart = this.disableRequiresRestart(aAddon);
3243         AddonManagerPrivate.callAddonListeners("onDisabling", wrapper,
3244                                                needsRestart);
3245       }
3246       else {
3247         needsRestart = this.enableRequiresRestart(aAddon);
3248         AddonManagerPrivate.callAddonListeners("onEnabling", wrapper,
3249                                                needsRestart);
3250       }
3252       if (!needsRestart) {
3253         aAddon.active = !isDisabled;
3254         XPIDatabase.updateAddonActive(aAddon);
3255         if (isDisabled) {
3256           if (aAddon.bootstrap) {
3257             let file = aAddon._installLocation.getLocationForID(aAddon.id);
3258             this.callBootstrapMethod(aAddon.id, aAddon.version, file, "shutdown",
3259                                      BOOTSTRAP_REASONS.ADDON_DISABLE);
3260             this.unloadBootstrapScope(aAddon.id);
3261           }
3262           AddonManagerPrivate.callAddonListeners("onDisabled", wrapper);
3263         }
3264         else {
3265           if (aAddon.bootstrap) {
3266             let file = aAddon._installLocation.getLocationForID(aAddon.id);
3267             this.callBootstrapMethod(aAddon.id, aAddon.version, file, "startup",
3268                                      BOOTSTRAP_REASONS.ADDON_ENABLE);
3269           }
3270           AddonManagerPrivate.callAddonListeners("onEnabled", wrapper);
3271         }
3272       }
3273     }
3275     // Notify any other providers that a new theme has been enabled
3276     if (aAddon.type == "theme" && !isDisabled)
3277       AddonManagerPrivate.notifyAddonChanged(aAddon.id, aAddon.type, needsRestart);
3278   },
3280   /**
3281    * Uninstalls an add-on, immediately if possible or marks it as pending
3282    * uninstall if not.
3283    *
3284    * @param  aAddon
3285    *         The DBAddonInternal to uninstall
3286    * @throws if the addon cannot be uninstalled because it is in an install
3287    *         location that does not allow it
3288    */
3289   uninstallAddon: function XPI_uninstallAddon(aAddon) {
3290     if (!(aAddon instanceof DBAddonInternal))
3291       throw new Error("Can only uninstall installed addons.");
3293     if (aAddon._installLocation.locked)
3294       throw new Error("Cannot uninstall addons from locked install locations");
3296     // Inactive add-ons don't require a restart to uninstall
3297     let requiresRestart = this.uninstallRequiresRestart(aAddon);
3299     if (requiresRestart) {
3300       // We create an empty directory in the staging directory to indicate that
3301       // an uninstall is necessary on next startup.
3302       let stage = aAddon._installLocation.getStagingDir();
3303       stage.append(aAddon.id);
3304       if (!stage.exists())
3305         stage.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
3307       XPIDatabase.setAddonProperties(aAddon, {
3308         pendingUninstall: true
3309       });
3310       Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
3311     }
3313     // If the add-on is not visible then there is no need to notify listeners.
3314     if (!aAddon.visible)
3315       return;
3317     let wrapper = createWrapper(aAddon);
3318     AddonManagerPrivate.callAddonListeners("onUninstalling", wrapper,
3319                                            requiresRestart);
3321     if (!requiresRestart) {
3322       if (aAddon.bootstrap) {
3323         let file = aAddon._installLocation.getLocationForID(aAddon.id);
3324         if (aAddon.active) {
3325           this.callBootstrapMethod(aAddon.id, aAddon.version, file, "shutdown",
3326                                    BOOTSTRAP_REASONS.ADDON_UNINSTALL);
3327         }
3328         this.callBootstrapMethod(aAddon.id, aAddon.version, file, "uninstall",
3329                                  BOOTSTRAP_REASONS.ADDON_UNINSTALL);
3330         this.unloadBootstrapScope(aAddon.id);
3331       }
3332       aAddon._installLocation.uninstallAddon(aAddon.id);
3333       XPIDatabase.removeAddonMetadata(aAddon);
3334       AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper);
3336       // Reveal the highest priority add-on with the same ID
3337       function revealAddon(aAddon) {
3338         XPIDatabase.makeAddonVisible(aAddon);
3340         let wrappedAddon = createWrapper(aAddon);
3341         AddonManagerPrivate.callAddonListeners("onInstalling", wrappedAddon, false);
3343         if (!aAddon.userDisabled && !aAddon.appDisabled &&
3344             !XPIProvider.enableRequiresRestart(aAddon)) {
3345           aAddon.active = true;
3346           XPIDatabase.updateAddonActive(aAddon);
3347         }
3349         if (aAddon.bootstrap) {
3350           let file = aAddon._installLocation.getLocationForID(aAddon.id);
3351           XPIProvider.callBootstrapMethod(aAddon.id, aAddon.version, file,
3352                                           "install", BOOTSTRAP_REASONS.ADDON_INSTALL);
3354           if (aAddon.active) {
3355             XPIProvider.callBootstrapMethod(aAddon.id, aAddon.version, file,
3356                                             "startup", BOOTSTRAP_REASONS.ADDON_INSTALL);
3357           }
3358           else {
3359             XPIProvider.unloadBootstrapScope(aAddon.id);
3360           }
3361         }
3363         // We always send onInstalled even if a restart is required to enable
3364         // the revealed add-on
3365         AddonManagerPrivate.callAddonListeners("onInstalled", wrappedAddon);
3366       }
3368       function checkInstallLocation(aPos) {
3369         if (aPos < 0)
3370           return;
3372         let location = XPIProvider.installLocations[aPos];
3373         XPIDatabase.getAddonInLocation(aAddon.id, location.name, function(aNewAddon) {
3374           if (aNewAddon)
3375             revealAddon(aNewAddon);
3376           else
3377             checkInstallLocation(aPos - 1);
3378         })
3379       }
3381       checkInstallLocation(this.installLocations.length - 1);
3382     }
3384     // Notify any other providers that a new theme has been enabled
3385     if (aAddon.type == "theme" && aAddon.active)
3386       AddonManagerPrivate.notifyAddonChanged(null, aAddon.type, requiresRestart);
3387   },
3389   /**
3390    * Cancels the pending uninstall of an add-on.
3391    *
3392    * @param  aAddon
3393    *         The DBAddonInternal to cancel uninstall for
3394    */
3395   cancelUninstallAddon: function XPI_cancelUninstallAddon(aAddon) {
3396     if (!(aAddon instanceof DBAddonInternal))
3397       throw new Error("Can only cancel uninstall for installed addons.");
3399     cleanStagingDir(aAddon._installLocation.getStagingDir(), [aAddon.id]);
3401     XPIDatabase.setAddonProperties(aAddon, {
3402       pendingUninstall: false
3403     });
3405     if (!aAddon.visible)
3406       return;
3408     Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
3410     // TODO hide hidden add-ons (bug 557710)
3411     let wrapper = createWrapper(aAddon);
3412     AddonManagerPrivate.callAddonListeners("onOperationCancelled", wrapper);
3414     // Notify any other providers that this theme is now enabled again.
3415     if (aAddon.type == "theme" && aAddon.active)
3416       AddonManagerPrivate.notifyAddonChanged(aAddon.id, aAddon.type, false);
3417   }
3420 const FIELDS_ADDON = "internal_id, id, location, version, type, internalName, " +
3421                      "updateURL, updateKey, optionsURL, aboutURL, iconURL, " +
3422                      "icon64URL, defaultLocale, visible, active, userDisabled, " +
3423                      "appDisabled, pendingUninstall, descriptor, installDate, " +
3424                      "updateDate, applyBackgroundUpdates, bootstrap, skinnable, " +
3425                      "size, sourceURI, releaseNotesURI";
3428  * A helper function to log an SQL error.
3430  * @param  aError
3431  *         The storage error code associated with the error
3432  * @param  aErrorString
3433  *         An error message
3434  */
3435 function logSQLError(aError, aErrorString) {
3436   ERROR("SQL error " + aError + ": " + aErrorString);
3440  * A helper function to log any errors that occur during async statements.
3442  * @param  aError
3443  *         A mozIStorageError to log
3444  */
3445 function asyncErrorLogger(aError) {
3446   logSQLError(aError.result, aError.message);
3450  * A helper function to execute a statement synchronously and log any error
3451  * that occurs.
3453  * @param  aStatement
3454  *         A mozIStorageStatement to execute
3455  */
3456 function executeStatement(aStatement) {
3457   try {
3458     aStatement.execute();
3459   }
3460   catch (e) {
3461     logSQLError(XPIDatabase.connection.lastError,
3462                 XPIDatabase.connection.lastErrorString);
3463     throw e;
3464   }
3468  * A helper function to step a statement synchronously and log any error that
3469  * occurs.
3471  * @param  aStatement
3472  *         A mozIStorageStatement to execute
3473  */
3474 function stepStatement(aStatement) {
3475   try {
3476     return aStatement.executeStep();
3477   }
3478   catch (e) {
3479     logSQLError(XPIDatabase.connection.lastError,
3480                 XPIDatabase.connection.lastErrorString);
3481     throw e;
3482   }
3486  * A mozIStorageStatementCallback that will asynchronously build DBAddonInternal
3487  * instances from the results it receives. Once the statement has completed
3488  * executing and all of the metadata for all of the add-ons has been retrieved
3489  * they will be passed as an array to aCallback.
3491  * @param  aCallback
3492  *         A callback function to pass the array of DBAddonInternals to
3493  */
3494 function AsyncAddonListCallback(aCallback) {
3495   this.callback = aCallback;
3496   this.addons = [];
3499 AsyncAddonListCallback.prototype = {
3500   callback: null,
3501   complete: false,
3502   count: 0,
3503   addons: null,
3505   handleResult: function(aResults) {
3506     let row = null;
3507     while (row = aResults.getNextRow()) {
3508       this.count++;
3509       let self = this;
3510       XPIDatabase.makeAddonFromRowAsync(row, function(aAddon) {
3511         function completeAddon(aRepositoryAddon) {
3512           aAddon._repositoryAddon = aRepositoryAddon;
3513           self.addons.push(aAddon);
3514           if (self.complete && self.addons.length == self.count)
3515            self.callback(self.addons);
3516         }
3518         if ("getCachedAddonByID" in AddonRepository)
3519           AddonRepository.getCachedAddonByID(aAddon.id, completeAddon);
3520         else
3521           completeAddon(null);
3522       });
3523     }
3524   },
3526   handleError: asyncErrorLogger,
3528   handleCompletion: function(aReason) {
3529     this.complete = true;
3530     if (this.addons.length == this.count)
3531       this.callback(this.addons);
3532   }
3535 var XPIDatabase = {
3536   // true if the database connection has been opened
3537   initialized: false,
3538   // A cache of statements that are used and need to be finalized on shutdown
3539   statementCache: {},
3540   // A cache of weak referenced DBAddonInternals so we can reuse objects where
3541   // possible
3542   addonCache: [],
3543   // The nested transaction count
3544   transactionCount: 0,
3546   // The statements used by the database
3547   statements: {
3548     _getDefaultLocale: "SELECT id, name, description, creator, homepageURL " +
3549                        "FROM locale WHERE id=:id",
3550     _getLocales: "SELECT addon_locale.locale, locale.id, locale.name, " +
3551                  "locale.description, locale.creator, locale.homepageURL " +
3552                  "FROM addon_locale JOIN locale ON " +
3553                  "addon_locale.locale_id=locale.id WHERE " +
3554                  "addon_internal_id=:internal_id",
3555     _getTargetApplications: "SELECT addon_internal_id, id, minVersion, " +
3556                             "maxVersion FROM targetApplication WHERE " +
3557                             "addon_internal_id=:internal_id",
3558     _getTargetPlatforms: "SELECT os, abi FROM targetPlatform WHERE " +
3559                          "addon_internal_id=:internal_id",
3560     _readLocaleStrings: "SELECT locale_id, type, value FROM locale_strings " +
3561                         "WHERE locale_id=:id",
3563     addAddonMetadata_addon: "INSERT INTO addon VALUES (NULL, :id, :location, " +
3564                             ":version, :type, :internalName, :updateURL, " +
3565                             ":updateKey, :optionsURL, :aboutURL, :iconURL, " +
3566                             ":icon64URL, :locale, :visible, :active, " +
3567                             ":userDisabled, :appDisabled, :pendingUninstall, " +
3568                             ":descriptor, :installDate, :updateDate, " +
3569                             ":applyBackgroundUpdates, :bootstrap, :skinnable, " +
3570                             ":size, :sourceURI, :releaseNotesURI)",
3571     addAddonMetadata_addon_locale: "INSERT INTO addon_locale VALUES " +
3572                                    "(:internal_id, :name, :locale)",
3573     addAddonMetadata_locale: "INSERT INTO locale (name, description, creator, " +
3574                              "homepageURL) VALUES (:name, :description, " +
3575                              ":creator, :homepageURL)",
3576     addAddonMetadata_strings: "INSERT INTO locale_strings VALUES (:locale, " +
3577                               ":type, :value)",
3578     addAddonMetadata_targetApplication: "INSERT INTO targetApplication VALUES " +
3579                                         "(:internal_id, :id, :minVersion, " +
3580                                         ":maxVersion)",
3581     addAddonMetadata_targetPlatform: "INSERT INTO targetPlatform VALUES " +
3582                                      "(:internal_id, :os, :abi)",
3584     clearVisibleAddons: "UPDATE addon SET visible=0 WHERE id=:id",
3585     updateAddonActive: "UPDATE addon SET active=:active WHERE " +
3586                        "internal_id=:internal_id",
3588     getActiveAddons: "SELECT " + FIELDS_ADDON + " FROM addon WHERE active=1 AND " +
3589                      "type<>'theme' AND bootstrap=0",
3590     getActiveTheme: "SELECT " + FIELDS_ADDON + " FROM addon WHERE " +
3591                     "internalName=:internalName AND type='theme'",
3592     getThemes: "SELECT " + FIELDS_ADDON + " FROM addon WHERE type='theme'",
3594     getAddonInLocation: "SELECT " + FIELDS_ADDON + " FROM addon WHERE id=:id " +
3595                         "AND location=:location",
3596     getAddons: "SELECT " + FIELDS_ADDON + " FROM addon",
3597     getAddonsByType: "SELECT " + FIELDS_ADDON + " FROM addon WHERE type=:type",
3598     getAddonsInLocation: "SELECT " + FIELDS_ADDON + " FROM addon WHERE " +
3599                          "location=:location",
3600     getInstallLocations: "SELECT DISTINCT location FROM addon",
3601     getVisibleAddonForID: "SELECT " + FIELDS_ADDON + " FROM addon WHERE " +
3602                           "visible=1 AND id=:id",
3603     getVisibleAddoForInternalName: "SELECT " + FIELDS_ADDON + " FROM addon " +
3604                                    "WHERE visible=1 AND internalName=:internalName",
3605     getVisibleAddons: "SELECT " + FIELDS_ADDON + " FROM addon WHERE visible=1",
3606     getVisibleAddonsWithPendingOperations: "SELECT " + FIELDS_ADDON + " FROM " +
3607                                            "addon WHERE visible=1 " +
3608                                            "AND (pendingUninstall=1 OR " +
3609                                            "MAX(userDisabled,appDisabled)=active)",
3611     makeAddonVisible: "UPDATE addon SET visible=1 WHERE internal_id=:internal_id",
3612     removeAddonMetadata: "DELETE FROM addon WHERE internal_id=:internal_id",
3613     // Equates to active = visible && !userDisabled && !appDisabled
3614     setActiveAddons: "UPDATE addon SET active=MIN(visible, 1 - userDisabled, " +
3615                      "1 - appDisabled)",
3616     setAddonProperties: "UPDATE addon SET userDisabled=:userDisabled, " +
3617                         "appDisabled=:appDisabled, " +
3618                         "pendingUninstall=:pendingUninstall, " +
3619                         "applyBackgroundUpdates=:applyBackgroundUpdates WHERE " +
3620                         "internal_id=:internal_id",
3621     updateTargetApplications: "UPDATE targetApplication SET " +
3622                               "minVersion=:minVersion, maxVersion=:maxVersion " +
3623                               "WHERE addon_internal_id=:internal_id AND id=:id",
3625     createSavepoint: "SAVEPOINT 'default'",
3626     releaseSavepoint: "RELEASE SAVEPOINT 'default'",
3627     rollbackSavepoint: "ROLLBACK TO SAVEPOINT 'default'"
3628   },
3630   /**
3631    * Begins a new transaction in the database. Transactions may be nested. Data
3632    * written by an inner transaction may be rolled back on its own. Rolling back
3633    * an outer transaction will rollback all the changes made by inner
3634    * transactions even if they were committed. No data is written to the disk
3635    * until the outermost transaction is committed. Transactions can be started
3636    * even when the database is not yet open in which case they will be started
3637    * when the database is first opened.
3638    */
3639   beginTransaction: function XPIDB_beginTransaction() {
3640     if (this.initialized)
3641       this.getStatement("createSavepoint").execute();
3642     this.transactionCount++;
3643   },
3645   /**
3646    * Commits the most recent transaction. The data may still be rolled back if
3647    * an outer transaction is rolled back.
3648    */
3649   commitTransaction: function XPIDB_commitTransaction() {
3650     if (this.transactionCount == 0) {
3651       ERROR("Attempt to commit one transaction too many.");
3652       return;
3653     }
3655     if (this.initialized)
3656       this.getStatement("releaseSavepoint").execute();
3657     this.transactionCount--;
3658   },
3660   /**
3661    * Rolls back the most recent transaction. The database will return to its
3662    * state when the transaction was started.
3663    */
3664   rollbackTransaction: function XPIDB_rollbackTransaction() {
3665     if (this.transactionCount == 0) {
3666       ERROR("Attempt to rollback one transaction too many.");
3667       return;
3668     }
3670     if (this.initialized) {
3671       this.getStatement("rollbackSavepoint").execute();
3672       this.getStatement("releaseSavepoint").execute();
3673     }
3674     this.transactionCount--;
3675   },
3677   /**
3678    * Attempts to open the database file. If it fails it will try to delete the
3679    * existing file and create an empty database. If that fails then it will
3680    * open an in-memory database that can be used during this session.
3681    *
3682    * @param  aDBFile
3683    *         The nsIFile to open
3684    * @return the mozIStorageConnection for the database
3685    */
3686   openDatabaseFile: function XPIDB_openDatabaseFile(aDBFile) {
3687     LOG("Opening database");
3688     let connection = null;
3690     // Attempt to open the database
3691     try {
3692       connection = Services.storage.openUnsharedDatabase(aDBFile);
3693     }
3694     catch (e) {
3695       ERROR("Failed to open database (1st attempt)", e);
3696       try {
3697         aDBFile.remove(true);
3698       }
3699       catch (e) {
3700         ERROR("Failed to remove database that could not be opened", e);
3701       }
3702       try {
3703         connection = Services.storage.openUnsharedDatabase(aDBFile);
3704       }
3705       catch (e) {
3706         ERROR("Failed to open database (2nd attempt)", e);
3708         // If we have got here there seems to be no way to open the real
3709         // database, instead open a temporary memory database so things will
3710         // work for this session
3711         return Services.storage.openSpecialDatabase("memory");
3712       }
3713     }
3715     connection.executeSimpleSQL("PRAGMA synchronous = FULL");
3716     connection.executeSimpleSQL("PRAGMA locking_mode = EXCLUSIVE");
3718     return connection;
3719   },
3721   /**
3722    * Opens a new connection to the database file.
3723    *
3724    * @param  aRebuildOnError
3725    *         A boolean indicating whether add-on information should be loaded
3726    *         from the install locations if the database needs to be rebuilt.
3727    * @return the migration data from the database if it was an old schema or
3728    *         null otherwise.
3729    */
3730   openConnection: function XPIDB_openConnection(aRebuildOnError) {
3731     this.initialized = true;
3732     let dbfile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_DATABASE], true);
3733     delete this.connection;
3735     this.connection = this.openDatabaseFile(dbfile);
3737     let migrateData = null;
3738     // If the database was corrupt or missing then the new blank database will
3739     // have a schema version of 0.
3740     let schemaVersion = this.connection.schemaVersion;
3741     if (schemaVersion != DB_SCHEMA) {
3742       // A non-zero schema version means that a schema has been successfully
3743       // created in the database in the past so we might be able to get useful
3744       // information from it
3745       if (schemaVersion != 0) {
3746         LOG("Migrating data from schema " + schemaVersion);
3747         migrateData = this.getMigrateDataFromDatabase();
3749         // Delete the existing database
3750         this.connection.close();
3751         try {
3752           if (dbfile.exists())
3753             dbfile.remove(true);
3755           // Reopen an empty database
3756           this.connection = this.openDatabaseFile(dbfile);
3757         }
3758         catch (e) {
3759           ERROR("Failed to remove old database", e);
3760           // If the file couldn't be deleted then fall back to an in-memory
3761           // database
3762           this.connection = Services.storage.openSpecialDatabase("memory");
3763         }
3764       }
3765       else if (Prefs.getIntPref(PREF_DB_SCHEMA, 0) == 0) {
3766         // Only migrate data from the RDF if we haven't done it before
3767         LOG("Migrating data from extensions.rdf");
3768         migrateData = this.getMigrateDataFromRDF();
3769       }
3771       // At this point the database should be completely empty
3772       this.createSchema();
3774       if (aRebuildOnError) {
3775         let activeBundles = this.getActiveBundles();
3776         WARN("Rebuilding add-ons database from installed extensions.");
3777         this.beginTransaction();
3778         try {
3779           let state = XPIProvider.getInstallLocationStates();
3780           XPIProvider.processFileChanges(state, {}, false, migrateData, activeBundles)
3781           // Make sure to update the active add-ons and add-ons list on shutdown
3782           Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
3783           this.commitTransaction();
3784         }
3785         catch (e) {
3786           ERROR("Error processing file changes", e);
3787           this.rollbackTransaction();
3788         }
3789       }
3790     }
3792     // If the database connection has a file open then it has the right schema
3793     // by now so make sure the preferences reflect that. If not then there is
3794     // an in-memory database open which means a problem opening and deleting the
3795     // real database, clear the schema preference to force trying to load the
3796     // database on the next startup
3797     if (this.connection.databaseFile) {
3798       Services.prefs.setIntPref(PREF_DB_SCHEMA, DB_SCHEMA);
3799     }
3800     else {
3801       try {
3802         Services.prefs.clearUserPref(PREF_DB_SCHEMA);
3803       }
3804       catch (e) {
3805         // The preference may not be defined
3806       }
3807     }
3808     Services.prefs.savePrefFile(null);
3810     // Begin any pending transactions
3811     for (let i = 0; i < this.transactionCount; i++)
3812       this.connection.executeSimpleSQL("SAVEPOINT 'default'");
3813     return migrateData;
3814   },
3816   /**
3817    * A lazy getter for the database connection.
3818    */
3819   get connection() {
3820     this.openConnection(true);
3821     return this.connection;
3822   },
3824   /**
3825    * Gets the list of file descriptors of active extension directories or XPI
3826    * files from the add-ons list. This must be loaded from disk since the
3827    * directory service gives no easy way to get both directly. This list doesn't
3828    * include themes as preferences already say which theme is currently active
3829    *
3830    * @return an array of persisitent descriptors for the directories
3831    */
3832   getActiveBundles: function XPIDB_getActiveBundles() {
3833     let bundles = [];
3835     let addonsList = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST],
3836                                        true);
3838     let iniFactory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"].
3839                      getService(Ci.nsIINIParserFactory);
3840     let parser = iniFactory.createINIParser(addonsList);
3842     let keys = parser.getKeys("ExtensionDirs");
3844     while (keys.hasMore())
3845       bundles.push(parser.getString("ExtensionDirs", keys.getNext()));
3847     // Also include the list of active bootstrapped extensions
3848     for (let id in XPIProvider.bootstrappedAddons)
3849       bundles.push(XPIProvider.bootstrappedAddons[id].descriptor);
3851     return bundles;
3852   },
3854   /**
3855    * Retrieves migration data from the old extensions.rdf database.
3856    *
3857    * @return an object holding information about what add-ons were previously
3858    *         userDisabled and any updated compatibility information
3859    */
3860   getMigrateDataFromRDF: function XPIDB_getMigrateDataFromRDF(aDbWasMissing) {
3861     let migrateData = {};
3863     // Migrate data from extensions.rdf
3864     let rdffile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_OLD_DATABASE], true);
3865     if (rdffile.exists()) {
3866       let ds = gRDF.GetDataSourceBlocking(Services.io.newFileURI(rdffile).spec);
3867       let root = Cc["@mozilla.org/rdf/container;1"].
3868                  createInstance(Ci.nsIRDFContainer);
3869       root.Init(ds, gRDF.GetResource(RDFURI_ITEM_ROOT));
3870       let elements = root.GetElements();
3871       while (elements.hasMoreElements()) {
3872         let source = elements.getNext().QueryInterface(Ci.nsIRDFResource);
3874         let location = getRDFProperty(ds, source, "installLocation");
3875         if (location) {
3876           if (!(location in migrateData))
3877             migrateData[location] = {};
3878           let id = source.ValueUTF8.substring(PREFIX_ITEM_URI.length);
3879           migrateData[location][id] = {
3880             version: getRDFProperty(ds, source, "version"),
3881             userDisabled: false,
3882             targetApplications: []
3883           }
3885           let disabled = getRDFProperty(ds, source, "userDisabled");
3886           if (disabled == "true" || disabled == "needs-disable")
3887             migrateData[location][id].userDisabled = true;
3889           let targetApps = ds.GetTargets(source, EM_R("targetApplication"),
3890                                          true);
3891           while (targetApps.hasMoreElements()) {
3892             let targetApp = targetApps.getNext()
3893                                       .QueryInterface(Ci.nsIRDFResource);
3894             let appInfo = {
3895               id: getRDFProperty(ds, targetApp, "id")
3896             };
3898             let minVersion = getRDFProperty(ds, targetApp, "updatedMinVersion");
3899             if (minVersion) {
3900               appInfo.minVersion = minVersion;
3901               appInfo.maxVersion = getRDFProperty(ds, targetApp, "updatedMaxVersion");
3902             }
3903             else {
3904               appInfo.minVersion = getRDFProperty(ds, targetApp, "minVersion");
3905               appInfo.maxVersion = getRDFProperty(ds, targetApp, "maxVersion");
3906             }
3907             migrateData[location][id].targetApplications.push(appInfo);
3908           }
3909         }
3910       }
3911     }
3913     return migrateData;
3914   },
3916   /**
3917    * Retrieves migration data from a database that has an older or newer schema.
3918    *
3919    * @return an object holding information about what add-ons were previously
3920    *         userDisabled and any updated compatibility information
3921    */
3922   getMigrateDataFromDatabase: function XPIDB_getMigrateDataFromDatabase() {
3923     let migrateData = {};
3925     // Attempt to migrate data from a different (even future!) version of the
3926     // database
3927     try {
3928       var stmt = this.connection.createStatement("SELECT internal_id, id, " +
3929                                                  "location, userDisabled, " +
3930                                                  "installDate, version " +
3931                                                  "FROM addon");
3932       for (let row in resultRows(stmt)) {
3933         if (!(row.location in migrateData))
3934           migrateData[row.location] = {};
3935         migrateData[row.location][row.id] = {
3936           internal_id: row.internal_id,
3937           version: row.version,
3938           installDate: row.installDate,
3939           userDisabled: row.userDisabled == 1,
3940           targetApplications: []
3941         };
3942       }
3944       var taStmt = this.connection.createStatement("SELECT id, minVersion, " +
3945                                                    "maxVersion FROM " +
3946                                                    "targetApplication WHERE " +
3947                                                    "addon_internal_id=:internal_id");
3949       for (let location in migrateData) {
3950         for (let id in migrateData[location]) {
3951           taStmt.params.internal_id = migrateData[location][id].internal_id;
3952           delete migrateData[location][id].internal_id;
3953           for (let row in resultRows(taStmt)) {
3954             migrateData[location][id].targetApplications.push({
3955               id: row.id,
3956               minVersion: row.minVersion,
3957               maxVersion: row.maxVersion
3958             });
3959           }
3960         }
3961       }
3962     }
3963     catch (e) {
3964       // An error here means the schema is too different to read
3965       ERROR("Error migrating data", e);
3966     }
3967     finally {
3968       if (taStmt)
3969         taStmt.finalize();
3970       if (stmt)
3971         stmt.finalize();
3972     }
3974     return migrateData;
3975   },
3977   /**
3978    * Shuts down the database connection and releases all cached objects.
3979    */
3980   shutdown: function XPIDB_shutdown(aCallback) {
3981     if (this.initialized) {
3982       for each (let stmt in this.statementCache)
3983         stmt.finalize();
3984       this.statementCache = {};
3985       this.addonCache = [];
3987       if (this.transactionCount > 0) {
3988         ERROR(this.transactionCount + " outstanding transactions, rolling back.");
3989         while (this.transactionCount > 0)
3990           this.rollbackTransaction();
3991       }
3993       this.initialized = false;
3994       let connection = this.connection;
3995       delete this.connection;
3997       // Re-create the connection smart getter to allow the database to be
3998       // re-loaded during testing.
3999       this.__defineGetter__("connection", function() {
4000         this.openConnection(true);
4001         return this.connection;
4002       });
4004       connection.asyncClose(aCallback);
4005     }
4006     else {
4007       if (aCallback)
4008         aCallback();
4009     }
4010   },
4012   /**
4013    * Gets a cached statement or creates a new statement if it doesn't already
4014    * exist.
4015    *
4016    * @param  key
4017    *         A unique key to reference the statement
4018    * @param  aSql
4019    *         An optional SQL string to use for the query, otherwise a
4020    *         predefined sql string for the key will be used.
4021    * @return a mozIStorageStatement for the passed SQL
4022    */
4023   getStatement: function XPIDB_getStatement(aKey, aSql) {
4024     if (aKey in this.statementCache)
4025       return this.statementCache[aKey];
4026     if (!aSql)
4027       aSql = this.statements[aKey];
4029     try {
4030       return this.statementCache[aKey] = this.connection.createStatement(aSql);
4031     }
4032     catch (e) {
4033       ERROR("Error creating statement " + aKey + " (" + aSql + ")");
4034       throw e;
4035     }
4036   },
4038   /**
4039    * Creates the schema in the database.
4040    */
4041   createSchema: function XPIDB_createSchema() {
4042     LOG("Creating database schema");
4043     this.beginTransaction();
4045     // Any errors in here should rollback the transaction
4046     try {
4047       this.connection.createTable("addon",
4048                                   "internal_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
4049                                   "id TEXT, location TEXT, version TEXT, " +
4050                                   "type TEXT, internalName TEXT, updateURL TEXT, " +
4051                                   "updateKey TEXT, optionsURL TEXT, aboutURL TEXT, " +
4052                                   "iconURL TEXT, icon64URL TEXT, " +
4053                                   "defaultLocale INTEGER, " +
4054                                   "visible INTEGER, active INTEGER, " +
4055                                   "userDisabled INTEGER, appDisabled INTEGER, " +
4056                                   "pendingUninstall INTEGER, descriptor TEXT, " +
4057                                   "installDate INTEGER, updateDate INTEGER, " +
4058                                   "applyBackgroundUpdates INTEGER, " +
4059                                   "bootstrap INTEGER, skinnable INTEGER, " +
4060                                   "size INTEGER, sourceURI TEXT, " +
4061                                   "releaseNotesURI TEXT, UNIQUE (id, location)");
4062       this.connection.createTable("targetApplication",
4063                                   "addon_internal_id INTEGER, " +
4064                                   "id TEXT, minVersion TEXT, maxVersion TEXT, " +
4065                                   "UNIQUE (addon_internal_id, id)");
4066       this.connection.createTable("targetPlatform",
4067                                   "addon_internal_id INTEGER, " +
4068                                   "os, abi TEXT, " +
4069                                   "UNIQUE (addon_internal_id, os, abi)");
4070       this.connection.createTable("addon_locale",
4071                                   "addon_internal_id INTEGER, "+
4072                                   "locale TEXT, locale_id INTEGER, " +
4073                                   "UNIQUE (addon_internal_id, locale)");
4074       this.connection.createTable("locale",
4075                                   "id INTEGER PRIMARY KEY AUTOINCREMENT, " +
4076                                   "name TEXT, description TEXT, creator TEXT, " +
4077                                   "homepageURL TEXT");
4078       this.connection.createTable("locale_strings",
4079                                   "locale_id INTEGER, type TEXT, value TEXT");
4080       this.connection.executeSimpleSQL("CREATE TRIGGER delete_addon AFTER DELETE " +
4081         "ON addon BEGIN " +
4082         "DELETE FROM targetApplication WHERE addon_internal_id=old.internal_id; " +
4083         "DELETE FROM targetPlatform WHERE addon_internal_id=old.internal_id; " +
4084         "DELETE FROM addon_locale WHERE addon_internal_id=old.internal_id; " +
4085         "DELETE FROM locale WHERE id=old.defaultLocale; " +
4086         "END");
4087       this.connection.executeSimpleSQL("CREATE TRIGGER delete_addon_locale AFTER " +
4088         "DELETE ON addon_locale WHEN NOT EXISTS " +
4089         "(SELECT * FROM addon_locale WHERE locale_id=old.locale_id) BEGIN " +
4090         "DELETE FROM locale WHERE id=old.locale_id; " +
4091         "END");
4092       this.connection.executeSimpleSQL("CREATE TRIGGER delete_locale AFTER " +
4093         "DELETE ON locale BEGIN " +
4094         "DELETE FROM locale_strings WHERE locale_id=old.id; " +
4095         "END");
4096       this.connection.schemaVersion = DB_SCHEMA;
4097       this.commitTransaction();
4098     }
4099     catch (e) {
4100       ERROR("Failed to create database schema", e);
4101       logSQLError(this.connection.lastError, this.connection.lastErrorString);
4102       this.rollbackTransaction();
4103       this.connection.close();
4104       this.connection = null;
4105       throw e;
4106     }
4107   },
4109   /**
4110    * Synchronously reads the multi-value locale strings for a locale
4111    *
4112    * @param  aLocale
4113    *         The locale object to read into
4114    */
4115   _readLocaleStrings: function XPIDB__readLocaleStrings(aLocale) {
4116     let stmt = this.getStatement("_readLocaleStrings");
4118     stmt.params.id = aLocale.id;
4119     for (let row in resultRows(stmt)) {
4120       if (!(row.type in aLocale))
4121         aLocale[row.type] = [];
4122       aLocale[row.type].push(row.value);
4123     }
4124   },
4126   /**
4127    * Synchronously reads the locales for an add-on
4128    *
4129    * @param  aAddon
4130    *         The DBAddonInternal to read the locales for
4131    * @return the array of locales
4132    */
4133   _getLocales: function XPIDB__getLocales(aAddon) {
4134     let stmt = this.getStatement("_getLocales");
4136     let locales = [];
4137     stmt.params.internal_id = aAddon._internal_id;
4138     for (let row in resultRows(stmt)) {
4139       let locale = {
4140         id: row.id,
4141         locales: [row.locale]
4142       };
4143       copyProperties(row, PROP_LOCALE_SINGLE, locale);
4144       locales.push(locale);
4145     }
4146     locales.forEach(function(aLocale) {
4147       this._readLocaleStrings(aLocale);
4148     }, this);
4149     return locales;
4150   },
4152   /**
4153    * Synchronously reads the default locale for an add-on
4154    *
4155    * @param  aAddon
4156    *         The DBAddonInternal to read the default locale for
4157    * @return the default locale for the add-on
4158    * @throws if the database does not contain the default locale information
4159    */
4160   _getDefaultLocale: function XPIDB__getDefaultLocale(aAddon) {
4161     let stmt = this.getStatement("_getDefaultLocale");
4163     stmt.params.id = aAddon._defaultLocale;
4164     if (!stepStatement(stmt))
4165       throw new Error("Missing default locale for " + aAddon.id);
4166     let locale = copyProperties(stmt.row, PROP_LOCALE_SINGLE);
4167     locale.id = aAddon._defaultLocale;
4168     stmt.reset();
4169     this._readLocaleStrings(locale);
4170     return locale;
4171   },
4173   /**
4174    * Synchronously reads the target application entries for an add-on
4175    *
4176    * @param  aAddon
4177    *         The DBAddonInternal to read the target applications for
4178    * @return an array of target applications
4179    */
4180   _getTargetApplications: function XPIDB__getTargetApplications(aAddon) {
4181     let stmt = this.getStatement("_getTargetApplications");
4183     stmt.params.internal_id = aAddon._internal_id;
4184     return [copyProperties(row, PROP_TARGETAPP) for each (row in resultRows(stmt))];
4185   },
4187   /**
4188    * Synchronously reads the target platform entries for an add-on
4189    *
4190    * @param  aAddon
4191    *         The DBAddonInternal to read the target platforms for
4192    * @return an array of target platforms
4193    */
4194   _getTargetPlatforms: function XPIDB__getTargetPlatforms(aAddon) {
4195     let stmt = this.getStatement("_getTargetPlatforms");
4197     stmt.params.internal_id = aAddon._internal_id;
4198     return [copyProperties(row, ["os", "abi"]) for each (row in resultRows(stmt))];
4199   },
4201   /**
4202    * Synchronously makes a DBAddonInternal from a storage row or returns one
4203    * from the cache.
4204    *
4205    * @param  aRow
4206    *         The storage row to make the DBAddonInternal from
4207    * @return a DBAddonInternal
4208    */
4209   makeAddonFromRow: function XPIDB_makeAddonFromRow(aRow) {
4210     if (this.addonCache[aRow.internal_id]) {
4211       let addon = this.addonCache[aRow.internal_id].get();
4212       if (addon)
4213         return addon;
4214     }
4216     let addon = new DBAddonInternal();
4217     addon._internal_id = aRow.internal_id;
4218     addon._installLocation = XPIProvider.installLocationsByName[aRow.location];
4219     addon._descriptor = aRow.descriptor;
4220     addon._defaultLocale = aRow.defaultLocale;
4221     copyProperties(aRow, PROP_METADATA, addon);
4222     copyProperties(aRow, DB_METADATA, addon);
4223     DB_BOOL_METADATA.forEach(function(aProp) {
4224       addon[aProp] = aRow[aProp] != 0;
4225     });
4226     try {
4227       addon._sourceBundle = addon._installLocation.getLocationForID(addon.id);
4228     }
4229     catch (e) {
4230       // An exception will be thrown if the add-on appears in the database but
4231       // not on disk. In general this should only happen during startup as
4232       // this change is being detected.
4233     }
4235     this.addonCache[aRow.internal_id] = Components.utils.getWeakReference(addon);
4236     return addon;
4237   },
4239   /**
4240    * Asynchronously fetches additional metadata for a DBAddonInternal.
4241    *
4242    * @param  aAddon
4243    *         The DBAddonInternal
4244    * @param  aCallback
4245    *         The callback to call when the metadata is completely retrieved
4246    */
4247   fetchAddonMetadata: function XPIDB_fetchAddonMetadata(aAddon) {
4248     function readLocaleStrings(aLocale, aCallback) {
4249       let stmt = XPIDatabase.getStatement("_readLocaleStrings");
4251       stmt.params.id = aLocale.id;
4252       stmt.executeAsync({
4253         handleResult: function(aResults) {
4254           let row = null;
4255           while (row = aResults.getNextRow()) {
4256             let type = row.getResultByName("type");
4257             if (!(type in aLocale))
4258               aLocale[type] = [];
4259             aLocale[type].push(row.getResultByName("value"));
4260           }
4261         },
4263         handleError: asyncErrorLogger,
4265         handleCompletion: function(aReason) {
4266           aCallback();
4267         }
4268       });
4269     }
4271     function readDefaultLocale() {
4272       delete aAddon.defaultLocale;
4273       let stmt = XPIDatabase.getStatement("_getDefaultLocale");
4275       stmt.params.id = aAddon._defaultLocale;
4276       stmt.executeAsync({
4277         handleResult: function(aResults) {
4278           aAddon.defaultLocale = copyRowProperties(aResults.getNextRow(),
4279                                                    PROP_LOCALE_SINGLE);
4280           aAddon.defaultLocale.id = aAddon._defaultLocale;
4281         },
4283         handleError: asyncErrorLogger,
4285         handleCompletion: function(aReason) {
4286           if (aAddon.defaultLocale) {
4287             readLocaleStrings(aAddon.defaultLocale, readLocales);
4288           }
4289           else {
4290             ERROR("Missing default locale for " + aAddon.id);
4291             readLocales();
4292           }
4293         }
4294       });
4295     }
4297     function readLocales() {
4298       delete aAddon.locales;
4299       aAddon.locales = [];
4300       let stmt = XPIDatabase.getStatement("_getLocales");
4302       stmt.params.internal_id = aAddon._internal_id;
4303       stmt.executeAsync({
4304         handleResult: function(aResults) {
4305           let row = null;
4306           while (row = aResults.getNextRow()) {
4307             let locale = {
4308               id: row.getResultByName("id"),
4309               locales: [row.getResultByName("locale")]
4310             };
4311             copyRowProperties(row, PROP_LOCALE_SINGLE, locale);
4312             aAddon.locales.push(locale);
4313           }
4314         },
4316         handleError: asyncErrorLogger,
4318         handleCompletion: function(aReason) {
4319           let pos = 0;
4320           function readNextLocale() {
4321             if (pos < aAddon.locales.length)
4322               readLocaleStrings(aAddon.locales[pos++], readNextLocale);
4323             else
4324               readTargetApplications();
4325           }
4327           readNextLocale();
4328         }
4329       });
4330     }
4332     function readTargetApplications() {
4333       delete aAddon.targetApplications;
4334       aAddon.targetApplications = [];
4335       let stmt = XPIDatabase.getStatement("_getTargetApplications");
4337       stmt.params.internal_id = aAddon._internal_id;
4338       stmt.executeAsync({
4339         handleResult: function(aResults) {
4340           let row = null;
4341           while (row = aResults.getNextRow())
4342             aAddon.targetApplications.push(copyRowProperties(row, PROP_TARGETAPP));
4343         },
4345         handleError: asyncErrorLogger,
4347         handleCompletion: function(aReason) {
4348           readTargetPlatforms();
4349         }
4350       });
4351     }
4353     function readTargetPlatforms() {
4354       delete aAddon.targetPlatforms;
4355       aAddon.targetPlatforms = [];
4356       let stmt = XPIDatabase.getStatement("_getTargetPlatforms");
4358       stmt.params.internal_id = aAddon._internal_id;
4359       stmt.executeAsync({
4360         handleResult: function(aResults) {
4361           let row = null;
4362           while (row = aResults.getNextRow())
4363             aAddon.targetPlatforms.push(copyRowProperties(row, ["os", "abi"]));
4364         },
4366         handleError: asyncErrorLogger,
4368         handleCompletion: function(aReason) {
4369           let callbacks = aAddon._pendingCallbacks;
4370           delete aAddon._pendingCallbacks;
4371           callbacks.forEach(function(aCallback) {
4372             aCallback(aAddon);
4373           });
4374         }
4375       });
4376     }
4378     readDefaultLocale();
4379   },
4381   /**
4382    * Synchronously makes a DBAddonInternal from a mozIStorageRow or returns one
4383    * from the cache.
4384    *
4385    * @param  aRow
4386    *         The mozIStorageRow to make the DBAddonInternal from
4387    * @return a DBAddonInternal
4388    */
4389   makeAddonFromRowAsync: function XPIDB_makeAddonFromRowAsync(aRow, aCallback) {
4390     let internal_id = aRow.getResultByName("internal_id");
4391     if (this.addonCache[internal_id]) {
4392       let addon = this.addonCache[internal_id].get();
4393       if (addon) {
4394         // If metadata is still pending for this instance add our callback to
4395         // the list to be called when complete, otherwise pass the addon to
4396         // our callback
4397         if ("_pendingCallbacks" in addon)
4398           addon._pendingCallbacks.push(aCallback);
4399         else
4400           aCallback(addon);
4401         return;
4402       }
4403     }
4405     let addon = new DBAddonInternal();
4406     addon._internal_id = internal_id;
4407     let location = aRow.getResultByName("location");
4408     addon._installLocation = XPIProvider.installLocationsByName[location];
4409     addon._descriptor = aRow.getResultByName("descriptor");
4410     copyRowProperties(aRow, PROP_METADATA, addon);
4411     addon._defaultLocale = aRow.getResultByName("defaultLocale");
4412     copyRowProperties(aRow, DB_METADATA, addon);
4413     DB_BOOL_METADATA.forEach(function(aProp) {
4414       addon[aProp] = aRow.getResultByName(aProp) != 0;
4415     });
4416     try {
4417       addon._sourceBundle = addon._installLocation.getLocationForID(addon.id);
4418     }
4419     catch (e) {
4420       // An exception will be thrown if the add-on appears in the database but
4421       // not on disk. In general this should only happen during startup as
4422       // this change is being detected.
4423     }
4425     this.addonCache[internal_id] = Components.utils.getWeakReference(addon);
4426     addon._pendingCallbacks = [aCallback];
4427     this.fetchAddonMetadata(addon);
4428   },
4430   /**
4431    * Synchronously reads all install locations known about by the database. This
4432    * is often a a subset of the total install locations when not all have
4433    * installed add-ons, occasionally a superset when an install location no
4434    * longer exists.
4435    *
4436    * @return  an array of names of install locations
4437    */
4438   getInstallLocations: function XPIDB_getInstallLocations() {
4439     let stmt = this.getStatement("getInstallLocations");
4441     return [row.location for each (row in resultRows(stmt))];
4442   },
4444   /**
4445    * Synchronously reads all the add-ons in a particular install location.
4446    *
4447    * @param  location
4448    *         The name of the install location
4449    * @return an array of DBAddonInternals
4450    */
4451   getAddonsInLocation: function XPIDB_getAddonsInLocation(aLocation) {
4452     let stmt = this.getStatement("getAddonsInLocation");
4454     stmt.params.location = aLocation;
4455     return [this.makeAddonFromRow(row) for each (row in resultRows(stmt))];
4456   },
4458   /**
4459    * Asynchronously gets an add-on with a particular ID in a particular
4460    * install location.
4461    *
4462    * @param  aId
4463    *         The ID of the add-on to retrieve
4464    * @param  aLocation
4465    *         The name of the install location
4466    * @param  aCallback
4467    *         A callback to pass the DBAddonInternal to
4468    */
4469   getAddonInLocation: function XPIDB_getAddonInLocation(aId, aLocation, aCallback) {
4470     let stmt = this.getStatement("getAddonInLocation");
4472     stmt.params.id = aId;
4473     stmt.params.location = aLocation;
4474     stmt.executeAsync(new AsyncAddonListCallback(function(aAddons) {
4475       if (aAddons.length == 0) {
4476         aCallback(null);
4477         return;
4478       }
4479       // This should never happen but indicates invalid data in the database if
4480       // it does
4481       if (aAddons.length > 1)
4482         ERROR("Multiple addons with ID " + aId + " found in location " + aLocation);
4483       aCallback(aAddons[0]);
4484     }));
4485   },
4487   /**
4488    * Asynchronously gets the add-on with an ID that is visible.
4489    *
4490    * @param  aId
4491    *         The ID of the add-on to retrieve
4492    * @param  aCallback
4493    *         A callback to pass the DBAddonInternal to
4494    */
4495   getVisibleAddonForID: function XPIDB_getVisibleAddonForID(aId, aCallback) {
4496     let stmt = this.getStatement("getVisibleAddonForID");
4498     stmt.params.id = aId;
4499     stmt.executeAsync(new AsyncAddonListCallback(function(aAddons) {
4500       if (aAddons.length == 0) {
4501         aCallback(null);
4502         return;
4503       }
4504       // This should never happen but indicates invalid data in the database if
4505       // it does
4506       if (aAddons.length > 1)
4507         ERROR("Multiple visible addons with ID " + aId + " found");
4508       aCallback(aAddons[0]);
4509     }));
4510   },
4512   /**
4513    * Asynchronously gets the visible add-ons, optionally restricting by type.
4514    *
4515    * @param  aTypes
4516    *         An array of types to include or null to include all types
4517    * @param  aCallback
4518    *         A callback to pass the array of DBAddonInternals to
4519    */
4520   getVisibleAddons: function XPIDB_getVisibleAddons(aTypes, aCallback) {
4521     let stmt = null;
4522     if (!aTypes || aTypes.length == 0) {
4523       stmt = this.getStatement("getVisibleAddons");
4524     }
4525     else {
4526       let sql = "SELECT * FROM addon WHERE visible=1 AND type IN (";
4527       for (let i = 1; i <= aTypes.length; i++) {
4528         sql += "?" + i;
4529         if (i < aTypes.length)
4530           sql += ",";
4531       }
4532       sql += ")";
4534       // Note that binding to index 0 sets the value for the ?1 parameter
4535       stmt = this.getStatement("getVisibleAddons_" + aTypes.length, sql);
4536       for (let i = 0; i < aTypes.length; i++)
4537         stmt.bindStringParameter(i, aTypes[i]);
4538     }
4540     stmt.executeAsync(new AsyncAddonListCallback(aCallback));
4541   },
4543   /**
4544    * Synchronously gets all add-ons of a particular type.
4545    *
4546    * @param  aType
4547    *         The type of add-on to retrieve
4548    * @return an array of DBAddonInternals
4549    */
4550   getAddonsByType: function XPIDB_getAddonsByType(aType) {
4551     let stmt = this.getStatement("getAddonsByType");
4553     stmt.params.type = aType;
4554     return [this.makeAddonFromRow(row) for each (row in resultRows(stmt))];;
4555   },
4557   /**
4558    * Synchronously gets an add-on with a particular internalName.
4559    *
4560    * @param  aInternalName
4561    *         The internalName of the add-on to retrieve
4562    * @return a DBAddonInternal
4563    */
4564   getVisibleAddonForInternalName: function XPIDB_getVisibleAddonForInternalName(aInternalName) {
4565     let stmt = this.getStatement("getVisibleAddoForInternalName");
4567     let addon = null;
4568     stmt.params.internalName = aInternalName;
4570     if (stepStatement(stmt))
4571       addon = this.makeAddonFromRow(stmt.row);
4573     stmt.reset();
4574     return addon;
4575   },
4577   /**
4578    * Asynchronously gets all add-ons with pending operations.
4579    *
4580    * @param  aTypes
4581    *         The types of add-ons to retrieve or null to get all types
4582    * @param  aCallback
4583    *         A callback to pass the array of DBAddonInternal to
4584    */
4585   getVisibleAddonsWithPendingOperations:
4586     function XPIDB_getVisibleAddonsWithPendingOperations(aTypes, aCallback) {
4587     let stmt = null;
4588     if (!aTypes || aTypes.length == 0) {
4589       stmt = this.getStatement("getVisibleAddonsWithPendingOperations");
4590     }
4591     else {
4592       let sql = "SELECT * FROM addon WHERE visible=1 AND " +
4593                 "(pendingUninstall=1 OR MAX(userDisabled,appDisabled)=active) " +
4594                 "AND type IN (";
4595       for (let i = 1; i <= aTypes.length; i++) {
4596         sql += "?" + i;
4597         if (i < aTypes.length)
4598           sql += ",";
4599       }
4600       sql += ")";
4602       // Note that binding to index 0 sets the value for the ?1 parameter
4603       stmt = this.getStatement("getVisibleAddonsWithPendingOperations_" +
4604                                aTypes.length, sql);
4605       for (let i = 0; i < aTypes.length; i++)
4606         stmt.bindStringParameter(i, aTypes[i]);
4607     }
4609     stmt.executeAsync(new AsyncAddonListCallback(aCallback));
4610   },
4612   /**
4613    * Synchronously gets all add-ons in the database.
4614    *
4615    * @return  an array of DBAddonInternals
4616    */
4617   getAddons: function XPIDB_getAddons() {
4618     let stmt = this.getStatement("getAddons");
4620     return [this.makeAddonFromRow(row) for each (row in resultRows(stmt))];;
4621   },
4623   /**
4624    * Synchronously adds an AddonInternal's metadata to the database.
4625    *
4626    * @param  aAddon
4627    *         AddonInternal to add
4628    * @param  aDescriptor
4629    *         The file descriptor of the add-on
4630    */
4631   addAddonMetadata: function XPIDB_addAddonMetadata(aAddon, aDescriptor) {
4632     this.beginTransaction();
4634     // Any errors in here should rollback the transaction
4635     try {
4636       let localestmt = this.getStatement("addAddonMetadata_locale");
4637       let stringstmt = this.getStatement("addAddonMetadata_strings");
4639       function insertLocale(aLocale) {
4640         copyProperties(aLocale, PROP_LOCALE_SINGLE, localestmt.params);
4641         executeStatement(localestmt);
4642         let row = XPIDatabase.connection.lastInsertRowID;
4644         PROP_LOCALE_MULTI.forEach(function(aProp) {
4645           aLocale[aProp].forEach(function(aStr) {
4646             stringstmt.params.locale = row;
4647             stringstmt.params.type = aProp;
4648             stringstmt.params.value = aStr;
4649             executeStatement(stringstmt);
4650           });
4651         });
4652         return row;
4653       }
4655       if (aAddon.visible) {
4656         let stmt = this.getStatement("clearVisibleAddons");
4657         stmt.params.id = aAddon.id;
4658         executeStatement(stmt);
4659       }
4661       let stmt = this.getStatement("addAddonMetadata_addon");
4663       stmt.params.locale = insertLocale(aAddon.defaultLocale);
4664       stmt.params.location = aAddon._installLocation.name;
4665       stmt.params.descriptor = aDescriptor;
4666       copyProperties(aAddon, PROP_METADATA, stmt.params);
4667       copyProperties(aAddon, DB_METADATA, stmt.params);
4668       DB_BOOL_METADATA.forEach(function(aProp) {
4669         stmt.params[aProp] = aAddon[aProp] ? 1 : 0;
4670       });
4671       executeStatement(stmt);
4672       let internal_id = this.connection.lastInsertRowID;
4674       stmt = this.getStatement("addAddonMetadata_addon_locale");
4675       aAddon.locales.forEach(function(aLocale) {
4676         let id = insertLocale(aLocale);
4677         aLocale.locales.forEach(function(aName) {
4678           stmt.params.internal_id = internal_id;
4679           stmt.params.name = aName;
4680           stmt.params.locale = insertLocale(aLocale);
4681           executeStatement(stmt);
4682         });
4683       });
4685       stmt = this.getStatement("addAddonMetadata_targetApplication");
4687       aAddon.targetApplications.forEach(function(aApp) {
4688         stmt.params.internal_id = internal_id;
4689         stmt.params.id = aApp.id;
4690         stmt.params.minVersion = aApp.minVersion;
4691         stmt.params.maxVersion = aApp.maxVersion;
4692         executeStatement(stmt);
4693       });
4695       stmt = this.getStatement("addAddonMetadata_targetPlatform");
4697       aAddon.targetPlatforms.forEach(function(aPlatform) {
4698         stmt.params.internal_id = internal_id;
4699         stmt.params.os = aPlatform.os;
4700         stmt.params.abi = aPlatform.abi;
4701         executeStatement(stmt);
4702       });
4704       this.commitTransaction();
4705     }
4706     catch (e) {
4707       this.rollbackTransaction();
4708       throw e;
4709     }
4710   },
4712   /**
4713    * Synchronously updates an add-ons metadata in the database. Currently just
4714    * removes and recreates.
4715    *
4716    * @param  aOldAddon
4717    *         The DBAddonInternal to be replaced
4718    * @param  aNewAddon
4719    *         The new AddonInternal to add
4720    * @param  aDescriptor
4721    *         The file descriptor of the add-on
4722    */
4723   updateAddonMetadata: function XPIDB_updateAddonMetadata(aOldAddon, aNewAddon,
4724                                                           aDescriptor) {
4725     this.beginTransaction();
4727     // Any errors in here should rollback the transaction
4728     try {
4729       this.removeAddonMetadata(aOldAddon);
4730       aNewAddon.installDate = aOldAddon.installDate;
4731       aNewAddon.applyBackgroundUpdates = aOldAddon.applyBackgroundUpdates;
4732       aNewAddon.active = (aNewAddon.visible && !aNewAddon.userDisabled &&
4733                           !aNewAddon.appDisabled)
4734       this.addAddonMetadata(aNewAddon, aDescriptor);
4735       this.commitTransaction();
4736     }
4737     catch (e) {
4738       this.rollbackTransaction();
4739       throw e;
4740     }
4741   },
4743   /**
4744    * Synchronously updates the target application entries for an add-on.
4745    *
4746    * @param  aAddon
4747    *         The DBAddonInternal being updated
4748    * @param  aTargets
4749    *         The array of target applications to update
4750    */
4751   updateTargetApplications: function XPIDB_updateTargetApplications(aAddon,
4752                                                                     aTargets) {
4753     this.beginTransaction();
4755     // Any errors in here should rollback the transaction
4756     try {
4757       let stmt = this.getStatement("updateTargetApplications");
4758       aTargets.forEach(function(aTarget) {
4759         stmt.params.internal_id = aAddon._internal_id;
4760         stmt.params.id = aTarget.id;
4761         stmt.params.minVersion = aTarget.minVersion;
4762         stmt.params.maxVersion = aTarget.maxVersion;
4763         executeStatement(stmt);
4764       });
4765       this.commitTransaction();
4766     }
4767     catch (e) {
4768       this.rollbackTransaction();
4769       throw e;
4770     }
4771   },
4773   /**
4774    * Synchronously removes an add-on from the database.
4775    *
4776    * @param  aAddon
4777    *         The DBAddonInternal being removed
4778    */
4779   removeAddonMetadata: function XPIDB_removeAddonMetadata(aAddon) {
4780     let stmt = this.getStatement("removeAddonMetadata");
4781     stmt.params.internal_id = aAddon._internal_id;
4782     executeStatement(stmt);
4783   },
4785   /**
4786    * Synchronously marks a DBAddonInternal as visible marking all other
4787    * instances with the same ID as not visible.
4788    *
4789    * @param  aAddon
4790    *         The DBAddonInternal to make visible
4791    * @param  callback
4792    *         A callback to pass the DBAddonInternal to
4793    */
4794   makeAddonVisible: function XPIDB_makeAddonVisible(aAddon) {
4795     let stmt = this.getStatement("clearVisibleAddons");
4796     stmt.params.id = aAddon.id;
4797     executeStatement(stmt);
4799     stmt = this.getStatement("makeAddonVisible");
4800     stmt.params.internal_id = aAddon._internal_id;
4801     executeStatement(stmt);
4803     aAddon.visible = true;
4804   },
4806   /**
4807    * Synchronously sets properties for an add-on.
4808    *
4809    * @param  aAddon
4810    *         The DBAddonInternal being updated
4811    * @param  aProperties
4812    *         A dictionary of properties to set
4813    */
4814   setAddonProperties: function XPIDB_setAddonProperties(aAddon, aProperties) {
4815     function convertBoolean(value) {
4816       return value ? 1 : 0;
4817     }
4819     let stmt = this.getStatement("setAddonProperties");
4820     stmt.params.internal_id = aAddon._internal_id;
4822     ["userDisabled", "appDisabled",
4823      "pendingUninstall"].forEach(function(aProp) {
4824       if (aProp in aProperties) {
4825         stmt.params[aProp] = convertBoolean(aProperties[aProp]);
4826         aAddon[aProp] = aProperties[aProp];
4827       }
4828       else {
4829         stmt.params[aProp] = convertBoolean(aAddon[aProp]);
4830       }
4831     });
4833     if ("applyBackgroundUpdates" in aProperties) {
4834       stmt.params.applyBackgroundUpdates = aProperties.applyBackgroundUpdates;
4835       aAddon.applyBackgroundUpdates = aProperties.applyBackgroundUpdates;
4836     }
4837     else {
4838       stmt.params.applyBackgroundUpdates = aAddon.applyBackgroundUpdates;
4839     }
4841     executeStatement(stmt);
4842   },
4844   /**
4845    * Synchronously pdates an add-on's active flag in the database.
4846    *
4847    * @param  aAddon
4848    *         The DBAddonInternal to update
4849    */
4850   updateAddonActive: function XPIDB_updateAddonActive(aAddon) {
4851     LOG("Updating add-on state");
4853     let stmt = this.getStatement("updateAddonActive");
4854     stmt.params.internal_id = aAddon._internal_id;
4855     stmt.params.active = aAddon.active ? 1 : 0;
4856     executeStatement(stmt);
4857   },
4859   /**
4860    * Synchronously calculates and updates all the active flags in the database.
4861    */
4862   updateActiveAddons: function XPIDB_updateActiveAddons() {
4863     LOG("Updating add-on states");
4864     let stmt = this.getStatement("setActiveAddons");
4865     executeStatement(stmt);
4867     // Note that this does not update the active property on cached
4868     // DBAddonInternal instances so we throw away the cache. This should only
4869     // happen during shutdown when everything is going away anyway or during
4870     // startup when the only references are internal.
4871     this.addonCache = [];
4872   },
4874   /**
4875    * Writes out the XPI add-ons list for the platform to read.
4876    */
4877   writeAddonsList: function XPIDB_writeAddonsList() {
4878     LOG("Writing add-ons list");
4879     Services.appinfo.invalidateCachesOnRestart();
4880     let addonsList = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST],
4881                                        true);
4883     let enabledAddons = [];
4884     let text = "[ExtensionDirs]\r\n";
4885     let count = 0;
4887     let stmt = this.getStatement("getActiveAddons");
4889     for (let row in resultRows(stmt)) {
4890       text += "Extension" + (count++) + "=" + row.descriptor + "\r\n";
4891       enabledAddons.push(row.id + ":" + row.version);
4892     }
4894     // The selected skin may come from an inactive theme (the default theme
4895     // when a lightweight theme is applied for example)
4896     text += "\r\n[ThemeDirs]\r\n";
4897     if (Prefs.getBoolPref(PREF_EM_DSS_ENABLED)) {
4898       stmt = this.getStatement("getThemes");
4899     }
4900     else {
4901       stmt = this.getStatement("getActiveTheme");
4902       stmt.params.internalName = XPIProvider.selectedSkin;
4903     }
4904     count = 0;
4905     for (let row in resultRows(stmt)) {
4906       text += "Extension" + (count++) + "=" + row.descriptor + "\r\n";
4907       enabledAddons.push(row.id + ":" + row.version);
4908     }
4910     var fos = FileUtils.openSafeFileOutputStream(addonsList);
4911     fos.write(text, text.length);
4912     FileUtils.closeSafeFileOutputStream(fos);
4914     Services.prefs.setCharPref(PREF_EM_ENABLED_ADDONS, enabledAddons.join(","));
4915   }
4918 function getHashStringForCrypto(aCrypto) {
4919   // return the two-digit hexadecimal code for a byte
4920   function toHexString(charCode)
4921     ("0" + charCode.toString(16)).slice(-2);
4923   // convert the binary hash data to a hex string.
4924   let binary = aCrypto.finish(false);
4925   return [toHexString(binary.charCodeAt(i)) for (i in binary)].join("").toLowerCase()
4929  * Instantiates an AddonInstall and passes the new object to a callback when
4930  * it is complete.
4932  * @param  aCallback
4933  *         The callback to pass the AddonInstall to
4934  * @param  aInstallLocation
4935  *         The install location the add-on will be installed into
4936  * @param  aUrl
4937  *         The nsIURL to get the add-on from. If this is an nsIFileURL then
4938  *         the add-on will not need to be downloaded
4939  * @param  aHash
4940  *         An optional hash for the add-on
4941  * @param  aName
4942  *         An optional name for the add-on
4943  * @param  aType
4944  *         An optional type for the add-on
4945  * @param  aIconURL
4946  *         An optional icon for the add-on
4947  * @param  aVersion
4948  *         An optional version for the add-on
4949  * @param  aReleaseNotesURI
4950  *         An optional nsIURI of release notes for the add-on
4951  * @param  aExistingAddon
4952  *         The add-on this install will update if known
4953  * @param  aLoadGroup
4954  *         The nsILoadGroup to associate any requests with
4955  * @throws if the url is the url of a local file and the hash does not match
4956  *         or the add-on does not contain an valid install manifest
4957  */
4958 function AddonInstall(aCallback, aInstallLocation, aUrl, aHash, aName, aType,
4959                       aIconURL, aVersion, aReleaseNotesURI, aExistingAddon,
4960                       aLoadGroup) {
4961   this.wrapper = new AddonInstallWrapper(this);
4962   this.installLocation = aInstallLocation;
4963   this.sourceURI = aUrl;
4964   this.releaseNotesURI = aReleaseNotesURI;
4965   if (aHash) {
4966     let hashSplit = aHash.toLowerCase().split(":");
4967     this.originalHash = {
4968       algorithm: hashSplit[0],
4969       data: hashSplit[1]
4970     };
4971   }
4972   this.hash = this.originalHash;
4973   this.loadGroup = aLoadGroup;
4974   this.listeners = [];
4975   this.existingAddon = aExistingAddon;
4976   this.error = 0;
4977   if (aLoadGroup)
4978     this.window = aLoadGroup.notificationCallbacks
4979                             .getInterface(Ci.nsIDOMWindow);
4980   else
4981     this.window = null;
4983   if (aUrl instanceof Ci.nsIFileURL) {
4984     this.file = aUrl.file.QueryInterface(Ci.nsILocalFile);
4986     if (!this.file.exists()) {
4987       WARN("XPI file " + this.file.path + " does not exist");
4988       this.state = AddonManager.STATE_DOWNLOAD_FAILED;
4989       this.error = AddonManager.ERROR_NETWORK_FAILURE;
4990       aCallback(this);
4991       return;
4992     }
4994     this.state = AddonManager.STATE_DOWNLOADED;
4995     this.progress = this.file.fileSize;
4996     this.maxProgress = this.file.fileSize;
4998     if (this.hash) {
4999       let crypto = Cc["@mozilla.org/security/hash;1"].
5000                    createInstance(Ci.nsICryptoHash);
5001       try {
5002         crypto.initWithString(this.hash.algorithm);
5003       }
5004       catch (e) {
5005         WARN("Unknown hash algorithm " + this.hash.algorithm);
5006         this.state = AddonManager.STATE_DOWNLOAD_FAILED;
5007         this.error = AddonManager.ERROR_INCORRECT_HASH;
5008         aCallback(this);
5009         return;
5010       }
5012       let fis = Cc["@mozilla.org/network/file-input-stream;1"].
5013                 createInstance(Ci.nsIFileInputStream);
5014       fis.init(this.file, -1, -1, false);
5015       crypto.updateFromStream(fis, this.file.fileSize);
5016       let calculatedHash = getHashStringForCrypto(crypto);
5017       if (calculatedHash != this.hash.data) {
5018         WARN("File hash (" + calculatedHash + ") did not match provided hash (" +
5019              this.hash.data + ")");
5020         this.state = AddonManager.STATE_DOWNLOAD_FAILED;
5021         this.error = AddonManager.ERROR_INCORRECT_HASH;
5022         aCallback(this);
5023         return;
5024       }
5025     }
5027     try {
5028       let self = this;
5029       this.loadManifest(function() {
5030         XPIDatabase.getVisibleAddonForID(self.addon.id, function(aAddon) {
5031           self.existingAddon = aAddon;
5032           if (aAddon)
5033             self.addon.userDisabled = aAddon.userDisabled;
5034           self.addon.updateDate = Date.now();
5035           self.addon.installDate = aAddon ? aAddon.installDate : self.addon.updateDate;
5037           if (!self.addon.isCompatible) {
5038             // TODO Should we send some event here?
5039             self.state = AddonManager.STATE_CHECKING;
5040             new UpdateChecker(self.addon, {
5041               onUpdateFinished: function(aAddon) {
5042                 self.state = AddonManager.STATE_DOWNLOADED;
5043                 XPIProvider.installs.push(self);
5044                 AddonManagerPrivate.callInstallListeners("onNewInstall",
5045                                                          self.listeners,
5046                                                          self.wrapper);
5048                 aCallback(self);
5049               }
5050             }, AddonManager.UPDATE_WHEN_ADDON_INSTALLED);
5051           }
5052           else {
5053             XPIProvider.installs.push(self);
5054             AddonManagerPrivate.callInstallListeners("onNewInstall",
5055                                                      self.listeners,
5056                                                      self.wrapper);
5058             aCallback(self);
5059           }
5060         });
5061       });
5062     }
5063     catch (e) {
5064       WARN("Invalid XPI", e);
5065       this.state = AddonManager.STATE_DOWNLOAD_FAILED;
5066       this.error = AddonManager.ERROR_CORRUPT_FILE;
5067       aCallback(this);
5068       return;
5069     }
5070   }
5071   else {
5072     this.state = AddonManager.STATE_AVAILABLE;
5073     this.name = aName;
5074     this.type = aType;
5075     this.version = aVersion;
5076     this.iconURL = aIconURL;
5077     this.progress = 0;
5078     this.maxProgress = -1;
5080     XPIProvider.installs.push(this);
5081     AddonManagerPrivate.callInstallListeners("onNewInstall", this.listeners,
5082                                              this.wrapper);
5084     aCallback(this);
5085   }
5088 AddonInstall.prototype = {
5089   installLocation: null,
5090   wrapper: null,
5091   stream: null,
5092   crypto: null,
5093   originalHash: null,
5094   hash: null,
5095   loadGroup: null,
5096   badCertHandler: null,
5097   listeners: null,
5099   name: null,
5100   type: null,
5101   version: null,
5102   iconURL: null,
5103   releaseNotesURI: null,
5104   sourceURI: null,
5105   file: null,
5106   ownsTempFile: false,
5107   certificate: null,
5108   certName: null,
5110   linkedInstalls: null,
5111   existingAddon: null,
5112   addon: null,
5114   state: null,
5115   error: null,
5116   progress: null,
5117   maxProgress: null,
5119   /**
5120    * Starts installation of this add-on from whatever state it is currently at
5121    * if possible.
5122    *
5123    * @throws if installation cannot proceed from the current state
5124    */
5125   install: function AI_install() {
5126     switch (this.state) {
5127     case AddonManager.STATE_AVAILABLE:
5128       this.startDownload();
5129       break;
5130     case AddonManager.STATE_DOWNLOADED:
5131       this.startInstall();
5132       break;
5133     case AddonManager.STATE_DOWNLOAD_FAILED:
5134     case AddonManager.STATE_INSTALL_FAILED:
5135     case AddonManager.STATE_CANCELLED:
5136       this.removeTemporaryFile();
5137       this.state = AddonManager.STATE_AVAILABLE;
5138       this.error = 0;
5139       this.progress = 0;
5140       this.maxProgress = -1;
5141       this.hash = this.originalHash;
5142       XPIProvider.installs.push(this);
5143       this.startDownload();
5144       break;
5145     case AddonManager.STATE_DOWNLOADING:
5146     case AddonManager.STATE_CHECKING:
5147     case AddonManager.STATE_INSTALLING:
5148       // Installation is already running
5149       return;
5150     default:
5151       throw new Error("Cannot start installing from this state");
5152     }
5153   },
5155   /**
5156    * Cancels installation of this add-on.
5157    *
5158    * @throws if installation cannot be cancelled from the current state
5159    */
5160   cancel: function AI_cancel() {
5161     switch (this.state) {
5162     case AddonManager.STATE_DOWNLOADING:
5163       if (this.channel)
5164         this.channel.cancel(Cr.NS_BINDING_ABORTED);
5165     case AddonManager.STATE_AVAILABLE:
5166     case AddonManager.STATE_DOWNLOADED:
5167       LOG("Cancelling download of " + this.sourceURI.spec);
5168       this.state = AddonManager.STATE_CANCELLED;
5169       XPIProvider.removeActiveInstall(this);
5170       AddonManagerPrivate.callInstallListeners("onDownloadCancelled",
5171                                                this.listeners, this.wrapper);
5172       this.removeTemporaryFile();
5173       break;
5174     case AddonManager.STATE_INSTALLED:
5175       LOG("Cancelling install of " + this.addon.id);
5176       let xpi = this.installLocation.getStagingDir();
5177       xpi.append(this.addon.id + ".xpi");
5178       Services.obs.notifyObservers(xpi, "flush-cache-entry", null);
5179       cleanStagingDir(this.installLocation.getStagingDir(),
5180                       [this.addon.id, this.addon.id + ".xpi",
5181                        this.addon.id + ".json"]);
5182       this.state = AddonManager.STATE_CANCELLED;
5183       XPIProvider.removeActiveInstall(this);
5185       if (this.existingAddon) {
5186         delete this.existingAddon.pendingUpgrade;
5187         this.existingAddon.pendingUpgrade = null;
5188       }
5190       AddonManagerPrivate.callAddonListeners("onOperationCancelled", createWrapper(this.addon));
5192       AddonManagerPrivate.callInstallListeners("onInstallCancelled",
5193                                                this.listeners, this.wrapper);
5194       break;
5195     default:
5196       throw new Error("Cannot cancel install of " + this.sourceURI.spec +
5197                       " from this state (" + this.state + ")");
5198     }
5199   },
5201   /**
5202    * Adds an InstallListener for this instance if the listener is not already
5203    * registered.
5204    *
5205    * @param  aListener
5206    *         The InstallListener to add
5207    */
5208   addListener: function AI_addListener(aListener) {
5209     if (!this.listeners.some(function(i) { return i == aListener; }))
5210       this.listeners.push(aListener);
5211   },
5213   /**
5214    * Removes an InstallListener for this instance if it is registered.
5215    *
5216    * @param  aListener
5217    *         The InstallListener to remove
5218    */
5219   removeListener: function AI_removeListener(aListener) {
5220     this.listeners = this.listeners.filter(function(i) {
5221       return i != aListener;
5222     });
5223   },
5225   /**
5226    * Removes the temporary file owned by this AddonInstall if there is one.
5227    */
5228   removeTemporaryFile: function AI_removeTemporaryFile() {
5229     // Only proceed if this AddonInstall owns its XPI file
5230     if (!this.ownsTempFile)
5231       return;
5233     try {
5234       this.file.remove(true);
5235       this.ownsTempFile = false;
5236     }
5237     catch (e) {
5238       WARN("Failed to remove temporary file " + this.file.path, e);
5239     }
5240   },
5242   /**
5243    * Updates the sourceURI and releaseNotesURI values on the Addon being
5244    * installed by this AddonInstall instance.
5245    */
5246   updateAddonURIs: function AI_updateAddonURIs() {
5247     this.addon.sourceURI = this.sourceURI.spec;
5248     if (this.releaseNotesURI)
5249       this.addon.releaseNotesURI = this.releaseNotesURI.spec;
5250   },
5252   /**
5253    * Loads add-on manifests from a multi-package XPI file. Each of the
5254    * XPI and JAR files contained in the XPI will be extracted. Any that
5255    * do not contain valid add-ons will be ignored. The first valid add-on will
5256    * be installed by this AddonInstall instance, the rest will have new
5257    * AddonInstall instances created for them.
5258    *
5259    * @param  aZipReader
5260    *         An open nsIZipReader for the multi-package XPI's files. This will
5261    *         be closed before this method returns.
5262    * @param  aCallback
5263    *         A function to call when all of the add-on manifests have been
5264    *         loaded.
5265    */
5266   loadMultipackageManifests: function AI_loadMultipackageManifests(aZipReader,
5267                                                                    aCallback) {
5268     let files = [];
5269     let entries = aZipReader.findEntries("(*.[Xx][Pp][Ii]|*.[Jj][Aa][Rr])");
5270     while (entries.hasMore()) {
5271       let entryName = entries.getNext();
5272       var target = getTemporaryFile();
5273       try {
5274         aZipReader.extract(entryName, target);
5275         files.push(target);
5276       }
5277       catch (e) {
5278         WARN("Failed to extract " + entryName + " from multi-package " +
5279              "XPI", e);
5280         target.remove(false);
5281       }
5282     }
5284     aZipReader.close();
5286     if (files.length == 0) {
5287       throw new Error("Multi-package XPI does not contain any packages " +
5288                       "to install");
5289     }
5291     let addon = null;
5293     // Find the first file that has a valid install manifest and use it for
5294     // the add-on that this AddonInstall instance will install.
5295     while (files.length > 0) {
5296       this.removeTemporaryFile();
5297       this.file = files.shift();
5298       this.ownsTempFile = true;
5299       try {
5300         addon = loadManifestFromZipFile(this.file);
5301         break;
5302       }
5303       catch (e) {
5304         WARN(this.file.leafName + " cannot be installed from multi-package " +
5305              "XPI", e);
5306       }
5307     }
5309     if (!addon) {
5310       // No valid add-on was found
5311       aCallback();
5312       return;
5313     }
5315     this.addon = addon;
5317     this.updateAddonURIs();
5319     this.addon._install = this;
5320     this.name = this.addon.selectedLocale.name;
5321     this.type = this.addon.type;
5322     this.version = this.addon.version;
5324     // Setting the iconURL to something inside the XPI locks the XPI and
5325     // makes it impossible to delete on Windows.
5326     //let newIcon = createWrapper(this.addon).iconURL;
5327     //if (newIcon)
5328     //  this.iconURL = newIcon;
5330     // Create new AddonInstall instances for every remaining file
5331     if (files.length > 0) {
5332       this.linkedInstalls = [];
5333       let count = 0;
5334       let self = this;
5335       files.forEach(function(file) {
5336         AddonInstall.createInstall(function(aInstall) {
5337           // Ignore bad add-ons (createInstall will have logged the error)
5338           if (aInstall.state == AddonManager.STATE_DOWNLOAD_FAILED) {
5339             // Manually remove the temporary file
5340             file.remove(true);
5341           }
5342           else {
5343             // Make the new install own its temporary file
5344             aInstall.ownsTempFile = true;
5346             self.linkedInstalls.push(aInstall)
5348             aInstall.sourceURI = self.sourceURI;
5349             aInstall.releaseNotesURI = self.releaseNotesURI;
5350             aInstall.updateAddonURIs();
5351           }
5353           count++;
5354           if (count == files.length)
5355             aCallback();
5356         }, file);
5357       }, this);
5358     }
5359     else {
5360       aCallback();
5361     }
5362   },
5364   /**
5365    * Called after the add-on is a local file and the signature and install
5366    * manifest can be read.
5367    *
5368    * @param  aCallback
5369    *         A function to call when the manifest has been loaded
5370    * @throws if the add-on does not contain a valid install manifest or the
5371    *         XPI is incorrectly signed
5372    */
5373   loadManifest: function AI_loadManifest(aCallback) {
5374     function addRepositoryData(aAddon) {
5375       // Try to load from the existing cache first
5376       AddonRepository.getCachedAddonByID(aAddon.id, function(aRepoAddon) {
5377         if (aRepoAddon) {
5378           aAddon._repositoryAddon = aRepoAddon;
5379           aCallback();
5380           return;
5381         }
5383         // It wasn't there so try to re-download it
5384         AddonRepository.cacheAddons([aAddon.id], function() {
5385           AddonRepository.getCachedAddonByID(aAddon.id, function(aRepoAddon) {
5386             aAddon._repositoryAddon = aRepoAddon;
5387             aCallback();
5388           });
5389         });
5390       });
5391     }
5393     let zipreader = Cc["@mozilla.org/libjar/zip-reader;1"].
5394                     createInstance(Ci.nsIZipReader);
5395     try {
5396       zipreader.open(this.file);
5397     }
5398     catch (e) {
5399       zipreader.close();
5400       throw e;
5401     }
5403     let principal = zipreader.getCertificatePrincipal(null);
5404     if (principal && principal.hasCertificate) {
5405       LOG("Verifying XPI signature");
5406       if (verifyZipSigning(zipreader, principal)) {
5407         let x509 = principal.certificate;
5408         if (x509 instanceof Ci.nsIX509Cert)
5409           this.certificate = x509;
5410         if (this.certificate && this.certificate.commonName.length > 0)
5411           this.certName = this.certificate.commonName;
5412         else
5413           this.certName = principal.prettyName;
5414       }
5415       else {
5416         zipreader.close();
5417         throw new Error("XPI is incorrectly signed");
5418       }
5419     }
5421     try {
5422       this.addon = loadManifestFromZipReader(zipreader);
5423     }
5424     catch (e) {
5425       zipreader.close();
5426       throw e;
5427     }
5429     if (this.addon.type == "multipackage") {
5430       let self = this;
5431       this.loadMultipackageManifests(zipreader, function() {
5432         addRepositoryData(self.addon);
5433       });
5434       return;
5435     }
5437     zipreader.close();
5439     this.updateAddonURIs();
5441     this.addon._install = this;
5442     this.name = this.addon.selectedLocale.name;
5443     this.type = this.addon.type;
5444     this.version = this.addon.version;
5446     // Setting the iconURL to something inside the XPI locks the XPI and
5447     // makes it impossible to delete on Windows.
5448     //let newIcon = createWrapper(this.addon).iconURL;
5449     //if (newIcon)
5450     //  this.iconURL = newIcon;
5452     addRepositoryData(this.addon);
5453   },
5455   observe: function AI_observe(aSubject, aTopic, aData) {
5456     // Network is going offline
5457     this.cancel();
5458   },
5460   /**
5461    * Starts downloading the add-on's XPI file.
5462    */
5463   startDownload: function AI_startDownload() {
5464     this.state = AddonManager.STATE_DOWNLOADING;
5465     if (!AddonManagerPrivate.callInstallListeners("onDownloadStarted",
5466                                                   this.listeners, this.wrapper)) {
5467       this.state = AddonManager.STATE_CANCELLED;
5468       XPIProvider.removeActiveInstall(this);
5469       AddonManagerPrivate.callInstallListeners("onDownloadCancelled",
5470                                                this.listeners, this.wrapper)
5471       return;
5472     }
5474     // If a listener changed our state then do not proceed with the download
5475     if (this.state != AddonManager.STATE_DOWNLOADING)
5476       return;
5478     try {
5479       this.file = getTemporaryFile();
5480       this.ownsTempFile = true;
5481       this.stream = Cc["@mozilla.org/network/file-output-stream;1"].
5482                     createInstance(Ci.nsIFileOutputStream);
5483       this.stream.init(this.file, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE |
5484                        FileUtils.MODE_TRUNCATE, FileUtils.PERMS_FILE, 0);
5485     }
5486     catch (e) {
5487       WARN("Failed to start download", e);
5488       this.state = AddonManager.STATE_DOWNLOAD_FAILED;
5489       this.error = AddonManager.ERROR_FILE_ACCESS;
5490       XPIProvider.removeActiveInstall(this);
5491       AddonManagerPrivate.callInstallListeners("onDownloadFailed",
5492                                                this.listeners, this.wrapper);
5493       return;
5494     }
5496     let listener = Cc["@mozilla.org/network/stream-listener-tee;1"].
5497                    createInstance(Ci.nsIStreamListenerTee);
5498     listener.init(this, this.stream);
5499     try {
5500       Components.utils.import("resource://gre/modules/CertUtils.jsm");
5501       let requireBuiltIn = Prefs.getBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, true);
5502       this.badCertHandler = new BadCertHandler(!requireBuiltIn);
5504       this.channel = NetUtil.newChannel(this.sourceURI);
5505       this.channel.notificationCallbacks = this;
5506       if (this.channel instanceof Ci.nsIHttpChannelInternal)
5507         this.channel.forceAllowThirdPartyCookie = true;
5508       this.channel.asyncOpen(listener, null);
5510       Services.obs.addObserver(this, "network:offline-about-to-go-offline", false);
5511     }
5512     catch (e) {
5513       WARN("Failed to start download", e);
5514       this.state = AddonManager.STATE_DOWNLOAD_FAILED;
5515       this.error = AddonManager.ERROR_NETWORK_FAILURE;
5516       XPIProvider.removeActiveInstall(this);
5517       AddonManagerPrivate.callInstallListeners("onDownloadFailed",
5518                                                this.listeners, this.wrapper);
5519     }
5520   },
5522   /**
5523    * Update the crypto hasher with the new data and call the progress listeners.
5524    *
5525    * @see nsIStreamListener
5526    */
5527   onDataAvailable: function AI_onDataAvailable(aRequest, aContext, aInputstream,
5528                                                aOffset, aCount) {
5529     this.crypto.updateFromStream(aInputstream, aCount);
5530     this.progress += aCount;
5531     if (!AddonManagerPrivate.callInstallListeners("onDownloadProgress",
5532                                                   this.listeners, this.wrapper)) {
5533       // TODO cancel the download and make it available again (bug 553024)
5534     }
5535   },
5537   /**
5538    * Check the redirect response for a hash of the target XPI and verify that
5539    * we don't end up on an insecure channel.
5540    *
5541    * @see nsIChannelEventSink
5542    */
5543   asyncOnChannelRedirect: function(aOldChannel, aNewChannel, aFlags, aCallback) {
5544     if (!this.hash && aOldChannel.originalURI.schemeIs("https") &&
5545         aOldChannel instanceof Ci.nsIHttpChannel) {
5546       try {
5547         let hashStr = aOldChannel.getResponseHeader("X-Target-Digest");
5548         let hashSplit = hashStr.toLowerCase().split(":");
5549         this.hash = {
5550           algorithm: hashSplit[0],
5551           data: hashSplit[1]
5552         };
5553       }
5554       catch (e) {
5555       }
5556     }
5558     // Verify that we don't end up on an insecure channel if we haven't got a
5559     // hash to verify with (see bug 537761 for discussion)
5560     if (!this.hash)
5561       this.badCertHandler.asyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, aCallback);
5562     else
5563       aCallback.onRedirectVerifyCallback(Cr.NS_OK);
5565     this.channel = aNewChannel;
5566   },
5568   /**
5569    * This is the first chance to get at real headers on the channel.
5570    *
5571    * @see nsIStreamListener
5572    */
5573   onStartRequest: function AI_onStartRequest(aRequest, aContext) {
5574     this.crypto = Cc["@mozilla.org/security/hash;1"].
5575                   createInstance(Ci.nsICryptoHash);
5576     if (this.hash) {
5577       try {
5578         this.crypto.initWithString(this.hash.algorithm);
5579       }
5580       catch (e) {
5581         WARN("Unknown hash algorithm " + this.hash.algorithm);
5582         this.state = AddonManager.STATE_DOWNLOAD_FAILED;
5583         this.error = AddonManager.ERROR_INCORRECT_HASH;
5584         XPIProvider.removeActiveInstall(this);
5585         AddonManagerPrivate.callInstallListeners("onDownloadFailed",
5586                                                  this.listeners, this.wrapper);
5587         aRequest.cancel(Cr.NS_BINDING_ABORTED);
5588         return;
5589       }
5590     }
5591     else {
5592       // We always need something to consume data from the inputstream passed
5593       // to onDataAvailable so just create a dummy cryptohasher to do that.
5594       this.crypto.initWithString("sha1");
5595     }
5597     this.progress = 0;
5598     if (aRequest instanceof Ci.nsIChannel) {
5599       try {
5600         this.maxProgress = aRequest.contentLength;
5601       }
5602       catch (e) {
5603       }
5604       LOG("Download started for " + this.sourceURI.spec + " to file " +
5605           this.file.path);
5606     }
5607   },
5609   /**
5610    * The download is complete.
5611    *
5612    * @see nsIStreamListener
5613    */
5614   onStopRequest: function AI_onStopRequest(aRequest, aContext, aStatus) {
5615     this.stream.close();
5616     this.channel = null;
5617     this.badCerthandler = null;
5618     Services.obs.removeObserver(this, "network:offline-about-to-go-offline");
5620     // If the download was cancelled then all events will have already been sent
5621     if (aStatus == Cr.NS_BINDING_ABORTED) {
5622       this.removeTemporaryFile();
5623       return;
5624     }
5626     LOG("Download of " + this.sourceURI.spec + " completed.");
5628     if (Components.isSuccessCode(aStatus)) {
5629       if (!(aRequest instanceof Ci.nsIHttpChannel) || aRequest.requestSucceeded) {
5630         if (!this.hash && (aRequest instanceof Ci.nsIChannel)) {
5631           try {
5632             checkCert(aRequest,
5633                       !Prefs.getBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, true));
5634           }
5635           catch (e) {
5636             this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, e);
5637             return;
5638           }
5639         }
5641         // convert the binary hash data to a hex string.
5642         let calculatedHash = getHashStringForCrypto(this.crypto);
5643         this.crypto = null;
5644         if (this.hash && calculatedHash != this.hash.data) {
5645           this.downloadFailed(AddonManager.ERROR_INCORRECT_HASH,
5646                               "Downloaded file hash (" + calculatedHash +
5647                               ") did not match provided hash (" + this.hash.data + ")");
5648           return;
5649         }
5650         try {
5651           let self = this;
5652           this.loadManifest(function() {
5653             if (self.addon.isCompatible) {
5654               self.downloadCompleted();
5655             }
5656             else {
5657               // TODO Should we send some event here (bug 557716)?
5658               self.state = AddonManager.STATE_CHECKING;
5659               new UpdateChecker(self.addon, {
5660                 onUpdateFinished: function(aAddon) {
5661                   self.downloadCompleted();
5662                 }
5663               }, AddonManager.UPDATE_WHEN_ADDON_INSTALLED);
5664             }
5665           });
5666         }
5667         catch (e) {
5668           this.downloadFailed(AddonManager.ERROR_CORRUPT_FILE, e);
5669         }
5670       }
5671       else {
5672         if (aRequest instanceof Ci.nsIHttpChannel)
5673           this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE,
5674                               aRequest.responseStatus + " " +
5675                               aRequest.responseStatusText);
5676         else
5677           this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, aStatus);
5678       }
5679     }
5680     else {
5681       this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, aStatus);
5682     }
5683   },
5685   /**
5686    * Notify listeners that the download failed.
5687    *
5688    * @param  aReason
5689    *         Something to log about the failure
5690    * @param  error
5691    *         The error code to pass to the listeners
5692    */
5693   downloadFailed: function(aReason, aError) {
5694     WARN("Download failed", aError);
5695     this.state = AddonManager.STATE_DOWNLOAD_FAILED;
5696     this.error = aReason;
5697     XPIProvider.removeActiveInstall(this);
5698     AddonManagerPrivate.callInstallListeners("onDownloadFailed", this.listeners,
5699                                              this.wrapper);
5701     // If the listener hasn't restarted the download then remove any temporary
5702     // file
5703     if (this.state == AddonManager.STATE_DOWNLOAD_FAILED)
5704       this.removeTemporaryFile();
5705   },
5707   /**
5708    * Notify listeners that the download completed.
5709    */
5710   downloadCompleted: function() {
5711     let self = this;
5712     XPIDatabase.getVisibleAddonForID(this.addon.id, function(aAddon) {
5713       if (aAddon)
5714         self.existingAddon = aAddon;
5716       self.state = AddonManager.STATE_DOWNLOADED;
5717       self.addon.updateDate = Date.now();
5719       if (self.existingAddon) {
5720         self.addon.existingAddonID = self.existingAddon.id;
5721         self.addon.userDisabled = self.existingAddon.userDisabled;
5722         self.addon.installDate = self.existingAddon.installDate;
5723       }
5724       else {
5725         self.addon.installDate = self.addon.updateDate;
5726       }
5728       if (AddonManagerPrivate.callInstallListeners("onDownloadEnded",
5729                                                    self.listeners,
5730                                                    self.wrapper)) {
5731         // If a listener changed our state then do not proceed with the install
5732         if (self.state != AddonManager.STATE_DOWNLOADED)
5733           return;
5735         self.install();
5737         if (self.linkedInstalls) {
5738           self.linkedInstalls.forEach(function(aInstall) {
5739             aInstall.install();
5740           });
5741         }
5742       }
5743     });
5744   },
5746   // TODO This relies on the assumption that we are always installing into the
5747   // highest priority install location so the resulting add-on will be visible
5748   // overriding any existing copy in another install location (bug 557710).
5749   /**
5750    * Installs the add-on into the install location.
5751    */
5752   startInstall: function AI_startInstall() {
5753     this.state = AddonManager.STATE_INSTALLING;
5754     if (!AddonManagerPrivate.callInstallListeners("onInstallStarted",
5755                                                   this.listeners, this.wrapper)) {
5756       this.state = AddonManager.STATE_DOWNLOADED;
5757       XPIProvider.removeActiveInstall(this);
5758       AddonManagerPrivate.callInstallListeners("onInstallCancelled",
5759                                                this.listeners, this.wrapper)
5760       return;
5761     }
5763     // Find and cancel any pending installs for the same add-on in the same
5764     // install location
5765     XPIProvider.installs.forEach(function(aInstall) {
5766       if (aInstall.state == AddonManager.STATE_INSTALLED &&
5767           aInstall.installLocation == this.installLocation &&
5768           aInstall.addon.id == this.addon.id)
5769         aInstall.cancel();
5770     }, this);
5772     let isUpgrade = this.existingAddon &&
5773                     this.existingAddon._installLocation == this.installLocation;
5774     let requiresRestart = XPIProvider.installRequiresRestart(this.addon);
5776     LOG("Starting install of " + this.sourceURI.spec);
5777     AddonManagerPrivate.callAddonListeners("onInstalling",
5778                                            createWrapper(this.addon),
5779                                            requiresRestart);
5780     let stagedAddon = this.installLocation.getStagingDir();
5782     try {
5783       // First stage the file regardless of whether restarting is necessary
5784       if (this.addon.unpack || Prefs.getBoolPref(PREF_XPI_UNPACK, false)) {
5785         LOG("Addon " + this.addon.id + " will be installed as " +
5786             "an unpacked directory");
5787         stagedAddon.append(this.addon.id);
5788         if (stagedAddon.exists())
5789           recursiveRemove(stagedAddon);
5790         stagedAddon.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
5791         extractFiles(this.file, stagedAddon);
5792       }
5793       else {
5794         LOG("Addon " + this.addon.id + " will be installed as " +
5795             "a packed xpi");
5796         stagedAddon.append(this.addon.id + ".xpi");
5797         if (stagedAddon.exists())
5798           stagedAddon.remove(true);
5799         this.file.copyTo(this.installLocation.getStagingDir(),
5800                          this.addon.id + ".xpi");
5801       }
5803       if (requiresRestart) {
5804         // Point the add-on to its extracted files as the xpi may get deleted
5805         this.addon._sourceBundle = stagedAddon;
5807         // Cache the AddonInternal as it may have updated compatibiltiy info
5808         let stagedJSON = stagedAddon.clone();
5809         stagedJSON.leafName = this.addon.id + ".json";
5810         if (stagedJSON.exists())
5811           stagedJSON.remove(true);
5812         let stream = Cc["@mozilla.org/network/file-output-stream;1"].
5813                      createInstance(Ci.nsIFileOutputStream);
5814         let converter = Cc["@mozilla.org/intl/converter-output-stream;1"].
5815                         createInstance(Ci.nsIConverterOutputStream);
5817         try {
5818           stream.init(stagedJSON, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE |
5819                                   FileUtils.MODE_TRUNCATE, FileUtils.PERMS_FILE,
5820                                  0);
5821           converter.init(stream, "UTF-8", 0, 0x0000);
5822           converter.writeString(JSON.stringify(this.addon));
5823         }
5824         finally {
5825           converter.close();
5826           stream.close();
5827         }
5829         LOG("Install of " + this.sourceURI.spec + " completed.");
5830         this.state = AddonManager.STATE_INSTALLED;
5831         if (isUpgrade) {
5832           delete this.existingAddon.pendingUpgrade;
5833           this.existingAddon.pendingUpgrade = this.addon;
5834         }
5835         AddonManagerPrivate.callInstallListeners("onInstallEnded",
5836                                                  this.listeners, this.wrapper,
5837                                                  createWrapper(this.addon));
5838       }
5839       else {
5840         // The install is completed so it should be removed from the active list
5841         XPIProvider.removeActiveInstall(this);
5843         // TODO We can probably reduce the number of DB operations going on here
5844         // We probably also want to support rolling back failed upgrades etc.
5845         // See bug 553015.
5847         // Deactivate and remove the old add-on as necessary
5848         let reason = BOOTSTRAP_REASONS.ADDON_INSTALL;
5849         if (this.existingAddon) {
5850           if (Services.vc.compare(this.existingAddon.version, this.addon.version) < 0)
5851             reason = BOOTSTRAP_REASONS.ADDON_UPGRADE;
5852           else
5853             reason = BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
5855           if (this.existingAddon.bootstrap) {
5856             let file = this.existingAddon._installLocation
5857                            .getLocationForID(this.existingAddon.id);
5858             if (this.existingAddon.active) {
5859               XPIProvider.callBootstrapMethod(this.existingAddon.id,
5860                                               this.existingAddon.version,
5861                                               file, "shutdown", reason);
5862             }
5863             XPIProvider.callBootstrapMethod(this.existingAddon.id,
5864                                             this.existingAddon.version,
5865                                             file, "uninstall", reason);
5866             XPIProvider.unloadBootstrapScope(this.existingAddon.id);
5867           }
5869           if (!isUpgrade && this.existingAddon.active) {
5870             this.existingAddon.active = false;
5871             XPIDatabase.updateAddonActive(this.existingAddon);
5872           }
5873         }
5875         // Install the new add-on into its final location
5876         let existingAddonID = this.existingAddon ? this.existingAddon.id : null;
5877         let file = this.installLocation.installAddon(this.addon.id, stagedAddon,
5878                                                      existingAddonID);
5879         cleanStagingDir(stagedAddon.parent, []);
5881         // Update the metadata in the database
5882         this.addon._sourceBundle = file;
5883         this.addon._installLocation = this.installLocation;
5884         this.addon.updateDate = recursiveLastModifiedTime(file);
5885         this.addon.visible = true;
5886         if (isUpgrade) {
5887           XPIDatabase.updateAddonMetadata(this.existingAddon, this.addon,
5888                                           file.persistentDescriptor);
5889         }
5890         else {
5891           this.addon.installDate = this.addon.updateDate;
5892           this.addon.active = (this.addon.visible && !this.addon.userDisabled &&
5893                                !this.addon.appDisabled)
5894           XPIDatabase.addAddonMetadata(this.addon, file.persistentDescriptor);
5895         }
5897         // Retrieve the new DBAddonInternal for the add-on we just added
5898         let self = this;
5899         XPIDatabase.getAddonInLocation(this.addon.id, this.installLocation.name,
5900                                        function(a) {
5901           self.addon = a;
5902           if (self.addon.bootstrap) {
5903             XPIProvider.callBootstrapMethod(self.addon.id, self.addon.version,
5904                                             file, "install", reason);
5905             if (self.addon.active) {
5906               XPIProvider.callBootstrapMethod(self.addon.id, self.addon.version,
5907                                               file, "startup", reason);
5908             }
5909             else {
5910               XPIProvider.unloadBootstrapScope(self.addon.id);
5911             }
5912           }
5913           AddonManagerPrivate.callAddonListeners("onInstalled",
5914                                                  createWrapper(self.addon));
5916           LOG("Install of " + self.sourceURI.spec + " completed.");
5917           self.state = AddonManager.STATE_INSTALLED;
5918           AddonManagerPrivate.callInstallListeners("onInstallEnded",
5919                                                    self.listeners, self.wrapper,
5920                                                    createWrapper(self.addon));
5921         });
5922       }
5923     }
5924     catch (e) {
5925       WARN("Failed to install", e);
5926       if (stagedAddon.exists())
5927         recursiveRemove(stagedAddon);
5928       this.state = AddonManager.STATE_INSTALL_FAILED;
5929       this.error = AddonManager.ERROR_FILE_ACCESS;
5930       XPIProvider.removeActiveInstall(this);
5931       AddonManagerPrivate.callInstallListeners("onInstallFailed",
5932                                                this.listeners,
5933                                                this.wrapper);
5934     }
5935     finally {
5936       this.removeTemporaryFile();
5937     }
5938   },
5940   getInterface: function(iid) {
5941     if (iid.equals(Ci.nsIAuthPrompt2)) {
5942       var factory = Cc["@mozilla.org/prompter;1"].
5943                     getService(Ci.nsIPromptFactory);
5944       return factory.getPrompt(this.window, Ci.nsIAuthPrompt);
5945     }
5946     else if (iid.equals(Ci.nsIChannelEventSink)) {
5947       return this;
5948     }
5950     return this.badCertHandler.getInterface(iid);
5951   }
5955  * Creates a new AddonInstall to install an add-on from a local file. Installs
5956  * always go into the profile install location.
5958  * @param  aCallback
5959  *         The callback to pass the new AddonInstall to
5960  * @param  aFile
5961  *         The file to install
5962  */
5963 AddonInstall.createInstall = function(aCallback, aFile) {
5964   let location = XPIProvider.installLocationsByName[KEY_APP_PROFILE];
5965   let url = Services.io.newFileURI(aFile);
5967   try {
5968     new AddonInstall(aCallback, location, url);
5969   }
5970   catch(e) {
5971     ERROR("Error creating install", e);
5972     aCallback(null);
5973   }
5977  * Creates a new AddonInstall to download and install a URL.
5979  * @param  aCallback
5980  *         The callback to pass the new AddonInstall to
5981  * @param  aUri
5982  *         The URI to download
5983  * @param  aHash
5984  *         A hash for the add-on
5985  * @param  aName
5986  *         A name for the add-on
5987  * @param  aIconURL
5988  *         An icon URL for the add-on
5989  * @param  aVersion
5990  *         A version for the add-on
5991  * @param  aLoadGroup
5992  *         An nsILoadGroup to associate the download with
5993  */
5994 AddonInstall.createDownload = function(aCallback, aUri, aHash, aName, aIconURL,
5995                                        aVersion, aLoadGroup) {
5996   let location = XPIProvider.installLocationsByName[KEY_APP_PROFILE];
5997   let url = NetUtil.newURI(aUri);
5998   new AddonInstall(aCallback, location, url, aHash, aName, null,
5999                    aIconURL, aVersion, null, null, aLoadGroup);
6003  * Creates a new AddonInstall for an update.
6005  * @param  aCallback
6006  *         The callback to pass the new AddonInstall to
6007  * @param  aAddon
6008  *         The add-on being updated
6009  * @param  aUpdate
6010  *         The metadata about the new version from the update manifest
6011  */
6012 AddonInstall.createUpdate = function(aCallback, aAddon, aUpdate) {
6013   let url = NetUtil.newURI(aUpdate.updateURL);
6014   let releaseNotesURI = null;
6015   try {
6016     if (aUpdate.updateInfoURL)
6017       releaseNotesURI = NetUtil.newURI(escapeAddonURI(aAddon, aUpdate.updateInfoURL));
6018   }
6019   catch (e) {
6020     // If the releaseNotesURI cannot be parsed then just ignore it.
6021   }
6022   new AddonInstall(aCallback, aAddon._installLocation, url, aUpdate.updateHash,
6023                    aAddon.selectedLocale.name, aAddon.type,
6024                    aAddon.iconURL, aUpdate.version, releaseNotesURI, aAddon);
6028  * Creates a wrapper for an AddonInstall that only exposes the public API
6030  * @param  install
6031  *         The AddonInstall to create a wrapper for
6032  */
6033 function AddonInstallWrapper(aInstall) {
6034   ["name", "type", "version", "iconURL", "releaseNotesURI", "file", "state", "error",
6035    "progress", "maxProgress", "certificate", "certName"].forEach(function(aProp) {
6036     this.__defineGetter__(aProp, function() aInstall[aProp]);
6037   }, this);
6039   this.__defineGetter__("existingAddon", function() {
6040     return createWrapper(aInstall.existingAddon);
6041   });
6042   this.__defineGetter__("addon", function() createWrapper(aInstall.addon));
6043   this.__defineGetter__("sourceURI", function() aInstall.sourceURI);
6045   this.__defineGetter__("linkedInstalls", function() {
6046     if (!aInstall.linkedInstalls)
6047       return null;
6048     return [i.wrapper for each (i in aInstall.linkedInstalls)];
6049   });
6051   this.install = function() {
6052     aInstall.install();
6053   }
6055   this.cancel = function() {
6056     aInstall.cancel();
6057   }
6059   this.addListener = function(listener) {
6060     aInstall.addListener(listener);
6061   }
6063   this.removeListener = function(listener) {
6064     aInstall.removeListener(listener);
6065   }
6068 AddonInstallWrapper.prototype = {};
6071  * Creates a new update checker.
6073  * @param  aAddon
6074  *         The add-on to check for updates
6075  * @param  aListener
6076  *         An UpdateListener to notify of updates
6077  * @param  aReason
6078  *         The reason for the update check
6079  * @param  aAppVersion
6080  *         An optional application version to check for updates for
6081  * @param  aPlatformVersion
6082  *         An optional platform version to check for updates for
6083  * @throws if the aListener or aReason arguments are not valid
6084  */
6085 function UpdateChecker(aAddon, aListener, aReason, aAppVersion, aPlatformVersion) {
6086   if (!aListener || !aReason)
6087     throw Cr.NS_ERROR_INVALID_ARG;
6089   Components.utils.import("resource://gre/modules/AddonUpdateChecker.jsm");
6091   this.addon = aAddon;
6092   this.listener = aListener;
6093   this.appVersion = aAppVersion;
6094   this.platformVersion = aPlatformVersion;
6095   this.syncCompatibility = (aReason == AddonManager.UPDATE_WHEN_NEW_APP_INSTALLED);
6097   let updateURL = aAddon.updateURL ? aAddon.updateURL :
6098                                      Services.prefs.getCharPref(PREF_EM_UPDATE_URL);
6100   const UPDATE_TYPE_COMPATIBILITY = 32;
6101   const UPDATE_TYPE_NEWVERSION = 64;
6103   aReason |= UPDATE_TYPE_COMPATIBILITY;
6104   if ("onUpdateAvailable" in this.listener)
6105     aReason |= UPDATE_TYPE_NEWVERSION;
6107   let url = escapeAddonURI(aAddon, updateURL, aReason, aAppVersion);
6108   AddonUpdateChecker.checkForUpdates(aAddon.id, aAddon.type, aAddon.updateKey,
6109                                      url, this);
6112 UpdateChecker.prototype = {
6113   addon: null,
6114   listener: null,
6115   appVersion: null,
6116   platformVersion: null,
6117   syncCompatibility: null,
6119   /**
6120    * Calls a method on the listener passing any number of arguments and
6121    * consuming any exceptions.
6122    *
6123    * @param  aMethod
6124    *         The method to call on the listener
6125    */
6126   callListener: function(aMethod) {
6127     if (!(aMethod in this.listener))
6128       return;
6130     let args = Array.slice(arguments, 1);
6131     try {
6132       this.listener[aMethod].apply(this.listener, args);
6133     }
6134     catch (e) {
6135       LOG("Exception calling UpdateListener method " + aMethod + ": " + e);
6136     }
6137   },
6139   /**
6140    * Called when AddonUpdateChecker completes the update check
6141    *
6142    * @param  updates
6143    *         The list of update details for the add-on
6144    */
6145   onUpdateCheckComplete: function UC_onUpdateCheckComplete(aUpdates) {
6146     let AUC = AddonUpdateChecker;
6148     // Always apply any compatibility update for the current version
6149     let compatUpdate = AUC.getCompatibilityUpdate(aUpdates, this.addon.version,
6150                                                   this.syncCompatibility);
6152     // Apply the compatibility update to the database
6153     if (compatUpdate)
6154       this.addon.applyCompatibilityUpdate(compatUpdate, this.syncCompatibility);
6156     // If the request is for an application or platform version that is
6157     // different to the current application or platform version then look for a
6158     // compatibility update for those versions.
6159     if ((this.appVersion &&
6160          Services.vc.compare(this.appVersion, Services.appinfo.version) != 0) ||
6161         (this.platformVersion &&
6162          Services.vc.compare(this.platformVersion, Services.appinfo.platformVersion) != 0)) {
6163       compatUpdate = AUC.getCompatibilityUpdate(aUpdates, this.addon.version,
6164                                                 false, this.appVersion,
6165                                                 this.platformVersion);
6166     }
6168     if (compatUpdate)
6169       this.callListener("onCompatibilityUpdateAvailable", createWrapper(this.addon));
6170     else
6171       this.callListener("onNoCompatibilityUpdateAvailable", createWrapper(this.addon));
6173     function sendUpdateAvailableMessages(aSelf, aInstall) {
6174       if (aInstall) {
6175         aSelf.callListener("onUpdateAvailable", createWrapper(aSelf.addon),
6176                            aInstall.wrapper);
6177       }
6178       else {
6179         aSelf.callListener("onNoUpdateAvailable", createWrapper(aSelf.addon));
6180       }
6181       aSelf.callListener("onUpdateFinished", createWrapper(aSelf.addon),
6182                          AddonManager.UPDATE_STATUS_NO_ERROR);
6183     }
6185     let update = AUC.getNewestCompatibleUpdate(aUpdates,
6186                                                this.appVersion,
6187                                                this.platformVersion);
6189     if (update && Services.vc.compare(this.addon.version, update.version) < 0) {
6190       for (let i = 0; i < XPIProvider.installs.length; i++) {
6191         // Skip installs that don't match the available update
6192         if (XPIProvider.installs[i].existingAddon != this.addon ||
6193             XPIProvider.installs[i].version != update.version)
6194           continue;
6196         // If the existing install has not yet started downloading then send an
6197         // available update notification. If it is already downloading then
6198         // don't send any available update notification
6199         if (XPIProvider.installs[i].state == AddonManager.STATE_AVAILABLE)
6200           sendUpdateAvailableMessages(this, XPIProvider.installs[i]);
6201         else
6202           sendUpdateAvailableMessages(this, null);
6203         return;
6204       }
6206       let self = this;
6207       AddonInstall.createUpdate(function(aInstall) {
6208         sendUpdateAvailableMessages(self, aInstall);
6209       }, this.addon, update);
6210     }
6211     else {
6212       sendUpdateAvailableMessages(this, null);
6213     }
6214   },
6216   /**
6217    * Called when AddonUpdateChecker fails the update check
6218    *
6219    * @param  aError
6220    *         An error status
6221    */
6222   onUpdateCheckError: function UC_onUpdateCheckError(aError) {
6223     this.callListener("onNoCompatibilityUpdateAvailable", createWrapper(this.addon));
6224     this.callListener("onNoUpdateAvailable", createWrapper(this.addon));
6225     this.callListener("onUpdateFinished", createWrapper(this.addon), aError);
6226   }
6230  * The AddonInternal is an internal only representation of add-ons. It may
6231  * have come from the database (see DBAddonInternal below) or an install
6232  * manifest.
6233  */
6234 function AddonInternal() {
6237 AddonInternal.prototype = {
6238   _selectedLocale: null,
6239   active: false,
6240   visible: false,
6241   userDisabled: false,
6242   appDisabled: false,
6243   sourceURI: null,
6244   releaseNotesURI: null,
6246   get selectedLocale() {
6247     if (this._selectedLocale)
6248       return this._selectedLocale;
6249     let locale = findClosestLocale(this.locales);
6250     this._selectedLocale = locale ? locale : this.defaultLocale;
6251     return this._selectedLocale;
6252   },
6254   get providesUpdatesSecurely() {
6255     return !!(this.updateKey || !this.updateURL ||
6256               this.updateURL.substring(0, 6) == "https:");
6257   },
6259   get isCompatible() {
6260     return this.isCompatibleWith();
6261   },
6263   get isPlatformCompatible() {
6264     if (this.targetPlatforms.length == 0)
6265       return true;
6267     let matchedOS = false;
6269     // If any targetPlatform matches the OS and contains an ABI then we will
6270     // only match a targetPlatform that contains both the current OS and ABI
6271     let needsABI = false;
6273     // Some platforms do not specify an ABI, test against null in that case.
6274     let abi = null;
6275     try {
6276       abi = Services.appinfo.XPCOMABI;
6277     }
6278     catch (e) { }
6280     for (let i = 0; i < this.targetPlatforms.length; i++) {
6281       let platform = this.targetPlatforms[i];
6282       if (platform.os == Services.appinfo.OS) {
6283         if (platform.abi) {
6284           needsABI = true;
6285           if (platform.abi === abi)
6286             return true;
6287         }
6288         else {
6289           matchedOS = true;
6290         }
6291       }
6292     }
6294     return matchedOS && !needsABI;
6295   },
6297   isCompatibleWith: function(aAppVersion, aPlatformVersion) {
6298     let app = this.matchingTargetApplication;
6299     if (!app)
6300       return false;
6302     if (!aAppVersion)
6303       aAppVersion = Services.appinfo.version;
6304     if (!aPlatformVersion)
6305       aPlatformVersion = Services.appinfo.platformVersion;
6307     let version;
6308     if (app.id == Services.appinfo.ID)
6309       version = aAppVersion;
6310     else if (app.id == TOOLKIT_ID)
6311       version = aPlatformVersion
6313     return (Services.vc.compare(version, app.minVersion) >= 0) &&
6314            (Services.vc.compare(version, app.maxVersion) <= 0)
6315   },
6317   get matchingTargetApplication() {
6318     let app = null;
6319     for (let i = 0; i < this.targetApplications.length; i++) {
6320       if (this.targetApplications[i].id == Services.appinfo.ID)
6321         return this.targetApplications[i];
6322       if (this.targetApplications[i].id == TOOLKIT_ID)
6323         app = this.targetApplications[i];
6324     }
6325     return app;
6326   },
6328   get blocklistState() {
6329     let bs = Cc["@mozilla.org/extensions/blocklist;1"].
6330              getService(Ci.nsIBlocklistService);
6331     return bs.getAddonBlocklistState(this.id, this.version);
6332   },
6334   applyCompatibilityUpdate: function(aUpdate, aSyncCompatibility) {
6335     this.targetApplications.forEach(function(aTargetApp) {
6336       aUpdate.targetApplications.forEach(function(aUpdateTarget) {
6337         if (aTargetApp.id == aUpdateTarget.id && (aSyncCompatibility ||
6338             Services.vc.compare(aTargetApp.maxVersion, aUpdateTarget.maxVersion) < 0)) {
6339           aTargetApp.minVersion = aUpdateTarget.minVersion;
6340           aTargetApp.maxVersion = aUpdateTarget.maxVersion;
6341         }
6342       });
6343     });
6344     this.appDisabled = !isUsableAddon(this);
6345   },
6347   /**
6348    * toJSON is called by JSON.stringify in order to create a filtered version
6349    * of this object to be serialized to a JSON file. A new object is returned
6350    * with copies of all non-private properties. Functions, getters and setters
6351    * are not copied.
6352    *
6353    * @param  aKey
6354    *         The key that this object is being serialized as in the JSON.
6355    *         Unused here since this is always the main object serialized
6356    *
6357    * @return an object containing copies of the properties of this object
6358    *         ignoring private properties, functions, getters and setters
6359    */
6360   toJSON: function(aKey) {
6361     let obj = {};
6362     for (let prop in this) {
6363       // Ignore private properties
6364       if (prop.substring(0, 1) == "_")
6365         continue;
6367       // Ignore getters
6368       if (this.__lookupGetter__(prop))
6369         continue;
6371       // Ignore setters
6372       if (this.__lookupSetter__(prop))
6373         continue;
6375       // Ignore functions
6376       if (typeof this[prop] == "function")
6377         continue;
6379       obj[prop] = this[prop];
6380     }
6382     return obj;
6383   }
6387  * The DBAddonInternal is a special AddonInternal that has been retrieved from
6388  * the database. Add-ons retrieved synchronously only have the basic metadata
6389  * the rest is filled out synchronously when needed. Asynchronously read add-ons
6390  * have all data available.
6391  */
6392 function DBAddonInternal() {
6393   this.__defineGetter__("targetApplications", function() {
6394     delete this.targetApplications;
6395     return this.targetApplications = XPIDatabase._getTargetApplications(this);
6396   });
6398   this.__defineGetter__("targetPlatforms", function() {
6399     delete this.targetPlatforms;
6400     return this.targetPlatforms = XPIDatabase._getTargetPlatforms(this);
6401   });
6403   this.__defineGetter__("locales", function() {
6404     delete this.locales;
6405     return this.locales = XPIDatabase._getLocales(this);
6406   });
6408   this.__defineGetter__("defaultLocale", function() {
6409     delete this.defaultLocale;
6410     return this.defaultLocale = XPIDatabase._getDefaultLocale(this);
6411   });
6413   this.__defineGetter__("pendingUpgrade", function() {
6414     delete this.pendingUpgrade;
6415     for (let i = 0; i < XPIProvider.installs.length; i++) {
6416       let install = XPIProvider.installs[i];
6417       if (install.state == AddonManager.STATE_INSTALLED &&
6418           !(install.addon instanceof DBAddonInternal) &&
6419           install.addon.id == this.id &&
6420           install.installLocation == this._installLocation) {
6421         return this.pendingUpgrade = install.addon;
6422       }
6423     };
6424   });
6427 DBAddonInternal.prototype = {
6428   applyCompatibilityUpdate: function(aUpdate, aSyncCompatibility) {
6429     let changes = [];
6430     this.targetApplications.forEach(function(aTargetApp) {
6431       aUpdate.targetApplications.forEach(function(aUpdateTarget) {
6432         if (aTargetApp.id == aUpdateTarget.id && (aSyncCompatibility ||
6433             Services.vc.compare(aTargetApp.maxVersion, aUpdateTarget.maxVersion) < 0)) {
6434           aTargetApp.minVersion = aUpdateTarget.minVersion;
6435           aTargetApp.maxVersion = aUpdateTarget.maxVersion;
6436           changes.push(aUpdateTarget);
6437         }
6438       });
6439     });
6440     try {
6441       XPIDatabase.updateTargetApplications(this, changes);
6442     }
6443     catch (e) {
6444       // A failure just means that we discard the compatibility update
6445       ERROR("Failed to update target application info in the database for " +
6446             "add-on " + this.id, e);
6447       return;
6448     }
6449     XPIProvider.updateAddonDisabledState(this);
6450   }
6453 DBAddonInternal.prototype.__proto__ = AddonInternal.prototype;
6456  * Creates an AddonWrapper for an AddonInternal.
6458  * @param   addon
6459  *          The AddonInternal to wrap
6460  * @return  an AddonWrapper or null if addon was null
6461  */
6462 function createWrapper(aAddon) {
6463   if (!aAddon)
6464     return null;
6465   if (!aAddon._wrapper)
6466     aAddon._wrapper = new AddonWrapper(aAddon);
6467   return aAddon._wrapper;
6471  * The AddonWrapper wraps an Addon to provide the data visible to consumers of
6472  * the public API.
6473  */
6474 function AddonWrapper(aAddon) {
6475   function chooseValue(aObj, aProp) {
6476     let repositoryAddon = aAddon._repositoryAddon;
6477     let objValue = aObj[aProp];
6479     if (repositoryAddon && (aProp in repositoryAddon) &&
6480         (objValue === undefined || objValue === null)) {
6481       return [repositoryAddon[aProp], true];
6482     }
6484     return [objValue, false];
6485   }
6487   ["id", "version", "type", "isCompatible", "isPlatformCompatible",
6488    "providesUpdatesSecurely", "blocklistState", "appDisabled",
6489    "userDisabled", "skinnable", "size"].forEach(function(aProp) {
6490      this.__defineGetter__(aProp, function() aAddon[aProp]);
6491   }, this);
6493   ["fullDescription", "developerComments", "eula", "supportURL",
6494    "contributionURL", "contributionAmount", "averageRating", "reviewCount",
6495    "reviewURL", "totalDownloads", "weeklyDownloads", "dailyUsers",
6496    "repositoryStatus"].forEach(function(aProp) {
6497     this.__defineGetter__(aProp, function() {
6498       if (aAddon._repositoryAddon)
6499         return aAddon._repositoryAddon[aProp];
6501       return null;
6502     });
6503   }, this);
6505   ["optionsURL", "aboutURL"].forEach(function(aProp) {
6506     this.__defineGetter__(aProp, function() {
6507       return this.isActive ? aAddon[aProp] : null;
6508     });
6509   }, this);
6511   ["installDate", "updateDate"].forEach(function(aProp) {
6512     this.__defineGetter__(aProp, function() new Date(aAddon[aProp]));
6513   }, this);
6515   ["sourceURI", "releaseNotesURI"].forEach(function(aProp) {
6516     this.__defineGetter__(aProp, function() {
6517       let target = chooseValue(aAddon, aProp)[0];
6518       if (!target)
6519         return null;
6520       return NetUtil.newURI(target);
6521     });
6522   }, this);
6524   // Maps iconURL and icon64URL to the properties of the same name or icon.png
6525   // and icon64.png in the add-on's files.
6526   ["icon", "icon64"].forEach(function(aProp) {
6527     this.__defineGetter__(aProp + "URL", function() {
6528       if (this.isActive && aAddon[aProp + "URL"])
6529         return aAddon[aProp + "URL"];
6531       if (this.hasResource(aProp + ".png"))
6532         return this.getResourceURI(aProp + ".png").spec;
6534       if (aAddon._repositoryAddon)
6535         return aAddon._repositoryAddon[aProp + "URL"];
6537       return null;
6538     }, this);
6539   }, this);
6541   PROP_LOCALE_SINGLE.forEach(function(aProp) {
6542     this.__defineGetter__(aProp, function() {
6543       // Override XPI creator if repository creator is defined
6544       if (aProp == "creator" &&
6545           aAddon._repositoryAddon && aAddon._repositoryAddon.creator) {
6546         return aAddon._repositoryAddon.creator;
6547       }
6549       let result = null;
6551       if (aAddon.active) {
6552         try {
6553           let pref = PREF_EM_EXTENSION_FORMAT + aAddon.id + "." + aProp;
6554           let value = Services.prefs.getComplexValue(pref,
6555                                                      Ci.nsIPrefLocalizedString);
6556           if (value.data)
6557             result = value.data;
6558         }
6559         catch (e) {
6560         }
6561       }
6563       if (result == null)
6564         [result, ] = chooseValue(aAddon.selectedLocale, aProp);
6566       if (aProp == "creator")
6567         return result ? new AddonManagerPrivate.AddonAuthor(result) : null;
6569       return result;
6570     });
6571   }, this);
6573   PROP_LOCALE_MULTI.forEach(function(aProp) {
6574     this.__defineGetter__(aProp, function() {
6575       let results = null;
6576       let usedRepository = false;
6578       if (aAddon.active) {
6579         let pref = PREF_EM_EXTENSION_FORMAT + aAddon.id + "." +
6580                    aProp.substring(0, aProp.length - 1);
6581         let list = Services.prefs.getChildList(pref, {});
6582         if (list.length > 0) {
6583           list.sort();
6584           results = [];
6585           list.forEach(function(aPref) {
6586             let value = Services.prefs.getComplexValue(aPref,
6587                                                        Ci.nsIPrefLocalizedString);
6588             if (value.data)
6589               results.push(value.data);
6590           });
6591         }
6592       }
6594       if (results == null)
6595         [results, usedRepository] = chooseValue(aAddon.selectedLocale, aProp);
6597       if (results && !usedRepository) {
6598         results = results.map(function(aResult) {
6599           return new AddonManagerPrivate.AddonAuthor(aResult);
6600         });
6601       }
6603       return results;
6604     });
6605   }, this);
6607   this.__defineGetter__("screenshots", function() {
6608     let repositoryAddon = aAddon._repositoryAddon;
6609     if (repositoryAddon && ("screenshots" in repositoryAddon)) {
6610       let repositoryScreenshots = repositoryAddon.screenshots;
6611       if (repositoryScreenshots && repositoryScreenshots.length > 0)
6612         return repositoryScreenshots;
6613     }
6615     if (aAddon.type == "theme" && this.hasResource("preview.png")) {
6616       let url = this.getResourceURI("preview.png").spec;
6617       return [new AddonManagerPrivate.AddonScreenshot(url)];
6618     }
6620     return null;
6621   });
6623   this.__defineGetter__("applyBackgroundUpdates", function() {
6624     return aAddon.applyBackgroundUpdates;
6625   });
6626   this.__defineSetter__("applyBackgroundUpdates", function(val) {
6627     if (val != AddonManager.AUTOUPDATE_DEFAULT &&
6628         val != AddonManager.AUTOUPDATE_DISABLE &&
6629         val != AddonManager.AUTOUPDATE_ENABLE) {
6630       val = val ? AddonManager.AUTOUPDATE_DEFAULT :
6631                   AddonManager.AUTOUPDATE_DISABLE;
6632     }
6634     if (val == aAddon.applyBackgroundUpdates)
6635       return val;
6637     XPIDatabase.setAddonProperties(aAddon, {
6638       applyBackgroundUpdates: val
6639     });
6640     AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, ["applyBackgroundUpdates"]);
6642     return val;
6643   });
6645   this.__defineGetter__("install", function() {
6646     if (!("_install" in aAddon) || !aAddon._install)
6647       return null;
6648     return aAddon._install.wrapper;
6649   });
6651   this.__defineGetter__("pendingUpgrade", function() {
6652     return createWrapper(aAddon.pendingUpgrade);
6653   });
6655   this.__defineGetter__("scope", function() {
6656     if (aAddon._installLocation)
6657       return aAddon._installLocation.scope;
6659     return AddonManager.SCOPE_PROFILE;
6660   });
6662   this.__defineGetter__("pendingOperations", function() {
6663     let pending = 0;
6664     if (!(aAddon instanceof DBAddonInternal)) {
6665       // Add-on is pending install if there is no associated install (shouldn't
6666       // happen here) or if the install is in the process of or has successfully
6667       // completed the install.
6668       if (!aAddon._install || aAddon._install.state == AddonManager.STATE_INSTALLING ||
6669           aAddon._install.state == AddonManager.STATE_INSTALLED)
6670         pending |= AddonManager.PENDING_INSTALL;
6671     }
6672     else if (aAddon.pendingUninstall) {
6673       pending |= AddonManager.PENDING_UNINSTALL;
6674     }
6676     if (aAddon.active && (aAddon.userDisabled || aAddon.appDisabled))
6677       pending |= AddonManager.PENDING_DISABLE;
6678     else if (!aAddon.active && (!aAddon.userDisabled && !aAddon.appDisabled))
6679       pending |= AddonManager.PENDING_ENABLE;
6681     if (aAddon.pendingUpgrade)
6682       pending |= AddonManager.PENDING_UPGRADE;
6684     return pending;
6685   });
6687   this.__defineGetter__("operationsRequiringRestart", function() {
6688     let ops = 0;
6689     if (XPIProvider.installRequiresRestart(aAddon))
6690       ops |= AddonManager.OP_NEEDS_RESTART_INSTALL;
6691     if (XPIProvider.uninstallRequiresRestart(aAddon))
6692       ops |= AddonManager.OP_NEEDS_RESTART_UNINSTALL;
6693     if (XPIProvider.enableRequiresRestart(aAddon))
6694       ops |= AddonManager.OP_NEEDS_RESTART_ENABLE;
6695     if (XPIProvider.disableRequiresRestart(aAddon))
6696       ops |= AddonManager.OP_NEEDS_RESTART_DISABLE;
6698     return ops;
6699   });
6701   this.__defineGetter__("permissions", function() {
6702     let permissions = 0;
6704     // Add-ons that aren't installed cannot be modified in any way
6705     if (!(aAddon instanceof DBAddonInternal))
6706       return permissions;
6708     if (!aAddon.appDisabled) {
6709       if (aAddon.userDisabled)
6710         permissions |= AddonManager.PERM_CAN_ENABLE;
6711       else if (aAddon.type != "theme")
6712         permissions |= AddonManager.PERM_CAN_DISABLE;
6713     }
6715     // Add-ons that are in locked install locations, or are pending uninstall
6716     // cannot be upgraded or uninstalled
6717     if (!aAddon._installLocation.locked && !aAddon.pendingUninstall) {
6718       // Add-ons that are installed by a file link cannot be upgraded
6719       if (!aAddon._installLocation.isLinkedAddon(aAddon.id))
6720         permissions |= AddonManager.PERM_CAN_UPGRADE;
6722       permissions |= AddonManager.PERM_CAN_UNINSTALL;
6723     }
6724     return permissions;
6725   });
6727   this.__defineGetter__("isActive", function() {
6728     if (Services.appinfo.inSafeMode)
6729       return false;
6730     return aAddon.active;
6731   });
6733   this.__defineSetter__("userDisabled", function(val) {
6734     if (val == aAddon.userDisabled)
6735       return val;
6737     if (aAddon instanceof DBAddonInternal) {
6738       if (aAddon.type == "theme" && val) {
6739         if (aAddon.internalName == XPIProvider.defaultSkin)
6740           throw new Error("Cannot disable the default theme");
6741         XPIProvider.enableDefaultTheme();
6742       }
6743       else {
6744         XPIProvider.updateAddonDisabledState(aAddon, val);
6745       }
6746     }
6747     else {
6748       aAddon.userDisabled = val;
6749     }
6751     return val;
6752   });
6754   this.isCompatibleWith = function(aAppVersion, aPlatformVersion) {
6755     return aAddon.isCompatibleWith(aAppVersion, aPlatformVersion);
6756   };
6758   this.uninstall = function() {
6759     if (!(aAddon instanceof DBAddonInternal))
6760       throw new Error("Cannot uninstall an add-on that isn't installed");
6761     if (aAddon.pendingUninstall)
6762       throw new Error("Add-on is already marked to be uninstalled");
6763     XPIProvider.uninstallAddon(aAddon);
6764   };
6766   this.cancelUninstall = function() {
6767     if (!(aAddon instanceof DBAddonInternal))
6768       throw new Error("Cannot cancel uninstall for an add-on that isn't installed");
6769     if (!aAddon.pendingUninstall)
6770       throw new Error("Add-on is not marked to be uninstalled");
6771     XPIProvider.cancelUninstallAddon(aAddon);
6772   };
6774   this.findUpdates = function(aListener, aReason, aAppVersion, aPlatformVersion) {
6775     new UpdateChecker(aAddon, aListener, aReason, aAppVersion, aPlatformVersion);
6776   };
6778   this.hasResource = function(aPath) {
6779     let bundle = aAddon._sourceBundle.clone();
6781     if (bundle.isDirectory()) {
6782       if (aPath) {
6783         aPath.split("/").forEach(function(aPart) {
6784           bundle.append(aPart);
6785         });
6786       }
6787       return bundle.exists();
6788     }
6790     let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].
6791                     createInstance(Ci.nsIZipReader);
6792     zipReader.open(bundle);
6793     let result = zipReader.hasEntry(aPath);
6794     zipReader.close();
6795     return result;
6796   },
6798   this.getResourceURI = function(aPath) {
6799     let bundle = aAddon._sourceBundle.clone();
6801     if (bundle.isDirectory()) {
6802       if (aPath) {
6803         aPath.split("/").forEach(function(aPart) {
6804           bundle.append(aPart);
6805         });
6806       }
6807       return Services.io.newFileURI(bundle);
6808     }
6810     if (!aPath)
6811       return Services.io.newFileURI(bundle);
6812     return buildJarURI(bundle, aPath);
6813   }
6817  * An object which identifies a directory install location for add-ons. The
6818  * location consists of a directory which contains the add-ons installed in the
6819  * location.
6821  * Each add-on installed in the location is either a directory containing the
6822  * add-on's files or a text file containing an absolute path to the directory
6823  * containing the add-ons files. The directory or text file must have the same
6824  * name as the add-on's ID.
6826  * There may also a special directory named "staged" which can contain
6827  * directories with the same name as an add-on ID. If the directory is empty
6828  * then it means the add-on will be uninstalled from this location during the
6829  * next startup. If the directory contains the add-on's files then they will be
6830  * installed during the next startup.
6832  * @param  aName
6833  *         The string identifier for the install location
6834  * @param  aDirectory
6835  *         The nsIFile directory for the install location
6836  * @param  aScope
6837  *         The scope of add-ons installed in this location
6838  * @param  aLocked
6839  *         true if add-ons cannot be installed, uninstalled or upgraded in the
6840  *         install location
6841  */
6842 function DirectoryInstallLocation(aName, aDirectory, aScope, aLocked) {
6843   this._name = aName;
6844   this.locked = aLocked;
6845   this._directory = aDirectory;
6846   this._scope = aScope
6847   this._IDToFileMap = {};
6848   this._FileToIDMap = {};
6849   this._linkedAddons = [];
6851   if (!aDirectory.exists())
6852     return;
6853   if (!aDirectory.isDirectory())
6854     throw new Error("Location must be a directory.");
6856   this._readAddons();
6859 DirectoryInstallLocation.prototype = {
6860   _name       : "",
6861   _directory   : null,
6862   _IDToFileMap : null,  // mapping from add-on ID to nsIFile
6863   _FileToIDMap : null,  // mapping from add-on path to add-on ID
6865   /**
6866    * Reads a directory linked to in a file.
6867    *
6868    * @param   file
6869    *          The file containing the directory path
6870    * @return  a nsILocalFile object representing the linked directory.
6871    */
6872   _readDirectoryFromFile: function DirInstallLocation__readDirectoryFromFile(aFile) {
6873     let fis = Cc["@mozilla.org/network/file-input-stream;1"].
6874               createInstance(Ci.nsIFileInputStream);
6875     fis.init(aFile, -1, -1, false);
6876     let line = { value: "" };
6877     if (fis instanceof Ci.nsILineInputStream)
6878       fis.readLine(line);
6879     fis.close();
6880     if (line.value) {
6881       let linkedDirectory = Cc["@mozilla.org/file/local;1"].
6882                             createInstance(Ci.nsILocalFile);
6884       try {
6885         linkedDirectory.initWithPath(line.value);
6886       }
6887       catch (e) {
6888         linkedDirectory.setRelativeDescriptor(file.parent, line.value);
6889       }
6891       if (!linkedDirectory.exists()) {
6892         WARN("File pointer " + aFile.path + " points to " + linkedDirectory.path +
6893              " which does not exist");
6894         return null;
6895       }
6897       if (!linkedDirectory.isDirectory()) {
6898         WARN("File pointer " + aFile.path + " points to " + linkedDirectory.path +
6899              " which is not a directory");
6900         return null;
6901       }
6903       return linkedDirectory;
6904     }
6906     WARN("File pointer " + aFile.path + " does not contain a path");
6907     return null;
6908   },
6910   /**
6911    * Finds all the add-ons installed in this location.
6912    */
6913   _readAddons: function DirInstallLocation__readAddons() {
6914     let entries = this._directory.directoryEntries
6915                                  .QueryInterface(Ci.nsIDirectoryEnumerator);
6916     let entry;
6917     while (entry = entries.nextFile) {
6918       // Should never happen really
6919       if (!(entry instanceof Ci.nsILocalFile))
6920         continue;
6922       let id = entry.leafName;
6924       if (id == DIR_STAGE || id == DIR_XPI_STAGE || id == DIR_TRASH)
6925         continue;
6927       let directLoad = false;
6928       if (entry.isFile() &&
6929           id.substring(id.length - 4).toLowerCase() == ".xpi") {
6930         directLoad = true;
6931         id = id.substring(0, id.length - 4);
6932       }
6934       if (!gIDTest.test(id)) {
6935         LOG("Ignoring file entry whose name is not a valid add-on ID: " +
6936              entry.path);
6937         continue;
6938       }
6940       if (entry.isFile() && !directLoad) {
6941         newEntry = this._readDirectoryFromFile(entry);
6942         if (!newEntry)
6943           continue;
6945         entry = newEntry;
6946         this._linkedAddons.push(id);
6947       }
6949       this._IDToFileMap[id] = entry;
6950       this._FileToIDMap[entry.path] = id;
6951     }
6952     entries.close();
6953   },
6955   /**
6956    * Gets the name of this install location.
6957    */
6958   get name() {
6959     return this._name;
6960   },
6962   /**
6963    * Gets the scope of this install location.
6964    */
6965   get scope() {
6966     return this._scope;
6967   },
6969   /**
6970    * Gets an array of nsIFiles for add-ons installed in this location.
6971    */
6972   get addonLocations() {
6973     let locations = [];
6974     for (let id in this._IDToFileMap) {
6975       locations.push(this._IDToFileMap[id].clone()
6976                          .QueryInterface(Ci.nsILocalFile));
6977     }
6978     return locations;
6979   },
6981   /**
6982    * Gets the staging directory to put add-ons that are pending install and
6983    * uninstall into.
6984    *
6985    * @return an nsIFile
6986    */
6987   getStagingDir: function DirInstallLocation_getStagingDir() {
6988     let dir = this._directory.clone();
6989     dir.append(DIR_STAGE);
6990     return dir;
6991   },
6993   /**
6994    * Gets the directory used by old versions for staging XPI and JAR files ready
6995    * to be installed.
6996    *
6997    * @return an nsIFile
6998    */
6999   getXPIStagingDir: function DirInstallLocation_getXPIStagingDir() {
7000     let dir = this._directory.clone();
7001     dir.append(DIR_XPI_STAGE);
7002     return dir;
7003   },
7005   /**
7006    * Returns a directory that is normally on the same filesystem as the rest of
7007    * the install location and can be used for temporarily storing files during
7008    * safe move operations. Calling this method will delete the existing trash
7009    * directory and its contents.
7010    *
7011    * @return an nsIFile
7012    */
7013   getTrashDir: function DirInstallLocation_getTrashDir() {
7014     let trashDir = this._directory.clone();
7015     trashDir.append(DIR_TRASH);
7016     if (trashDir.exists())
7017       recursiveRemove(trashDir);
7018     trashDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
7019     return trashDir;
7020   },
7022   /**
7023    * Installs an add-on into the install location.
7024    *
7025    * @param  aId
7026    *         The ID of the add-on to install
7027    * @param  aSource
7028    *         The source nsIFile to install from
7029    * @param  aExistingAddonID
7030    *         The ID of an existing add-on to uninstall at the same time
7031    * @param  aCopy
7032    *         If false the source files will be moved to the new location,
7033    *         otherwise they will only be copied
7034    * @return an nsIFile indicating where the add-on was installed to
7035    */
7036   installAddon: function DirInstallLocation_installAddon(aId, aSource,
7037                                                          aExistingAddonID,
7038                                                          aCopy) {
7039     let trashDir = this.getTrashDir();
7041     let transaction = new SafeInstallOperation();
7043     let self = this;
7044     function moveOldAddon(aId) {
7045       let file = self._directory.clone().QueryInterface(Ci.nsILocalFile);
7046       file.append(aId);
7048       if (file.exists())
7049         transaction.move(file, trashDir);
7051       file = self._directory.clone().QueryInterface(Ci.nsILocalFile);
7052       file.append(aId + ".xpi");
7053       if (file.exists()) {
7054         Services.obs.notifyObservers(file, "flush-cache-entry", null);
7055         transaction.move(file, trashDir);
7056       }
7057     }
7059     // If any of these operations fails the finally block will clean up the
7060     // temporary directory
7061     try {
7062       moveOldAddon(aId);
7063       if (aExistingAddonID && aExistingAddonID != aId)
7064         moveOldAddon(aExistingAddonID);
7066       if (aCopy) {
7067         transaction.copy(aSource, this._directory);
7068       }
7069       else {
7070         if (aSource.isFile())
7071           Services.obs.notifyObservers(aSource, "flush-cache-entry", null);
7073         transaction.move(aSource, this._directory);
7074       }
7075     }
7076     finally {
7077       // It isn't ideal if this cleanup fails but it isn't worth rolling back
7078       // the install because of it.
7079       try {
7080         recursiveRemove(trashDir);
7081       }
7082       catch (e) {
7083         WARN("Failed to remove trash directory when installing " + aId, e);
7084       }
7085     }
7087     let newFile = this._directory.clone().QueryInterface(Ci.nsILocalFile);
7088     newFile.append(aSource.leafName);
7089     newFile.lastModifiedTime = Date.now();
7090     this._FileToIDMap[newFile.path] = aId;
7091     this._IDToFileMap[aId] = newFile;
7093     if (aExistingAddonID && aExistingAddonID != aId &&
7094         aExistingAddonID in this._IDToFileMap) {
7095       delete this._FileToIDMap[this._IDToFileMap[aExistingAddonID]];
7096       delete this._IDToFileMap[aExistingAddonID];
7097     }
7099     return newFile;
7100   },
7102   /**
7103    * Uninstalls an add-on from this location.
7104    *
7105    * @param  aId
7106    *         The ID of the add-on to uninstall
7107    * @throws if the ID does not match any of the add-ons installed
7108    */
7109   uninstallAddon: function DirInstallLocation_uninstallAddon(aId) {
7110     let file = this._IDToFileMap[aId];
7111     if (!file) {
7112       WARN("Attempted to remove " + aId + " from " +
7113            this._name + " but it was already gone");
7114       return;
7115     }
7117     file = this._directory.clone();
7118     file.append(aId);
7119     if (!file.exists())
7120       file.leafName += ".xpi";
7122     if (!file.exists()) {
7123       WARN("Attempted to remove " + aId + " from " +
7124            this._name + " but it was already gone");
7126       delete this._FileToIDMap[file.path];
7127       delete this._IDToFileMap[aId];
7128       return;
7129     }
7131     let trashDir = this.getTrashDir();
7133     if (file.leafName != aId)
7134       Services.obs.notifyObservers(file, "flush-cache-entry", null);
7136     let transaction = new SafeInstallOperation();
7138     try {
7139       transaction.move(file, trashDir);
7140     }
7141     finally {
7142       // It isn't ideal if this cleanup fails, but it is probably better than
7143       // rolling back the uninstall at this point
7144       try {
7145         recursiveRemove(trashDir);
7146       }
7147       catch (e) {
7148         WARN("Failed to remove trash directory when uninstalling " + aId, e);
7149       }
7150     }
7152     delete this._FileToIDMap[file.path];
7153     delete this._IDToFileMap[aId];
7154   },
7156   /**
7157    * Gets the ID of the add-on installed in the given nsIFile.
7158    *
7159    * @param  aFile
7160    *         The nsIFile to look in
7161    * @return the ID
7162    * @throws if the file does not represent an installed add-on
7163    */
7164   getIDForLocation: function DirInstallLocation_getIDForLocation(aFile) {
7165     if (aFile.path in this._FileToIDMap)
7166       return this._FileToIDMap[aFile.path];
7167     throw new Error("Unknown add-on location " + aFile.path);
7168   },
7170   /**
7171    * Gets the directory that the add-on with the given ID is installed in.
7172    *
7173    * @param  aId
7174    *         The ID of the add-on
7175    * @return the nsILocalFile
7176    * @throws if the ID does not match any of the add-ons installed
7177    */
7178   getLocationForID: function DirInstallLocation_getLocationForID(aId) {
7179     if (aId in this._IDToFileMap)
7180       return this._IDToFileMap[aId].clone().QueryInterface(Ci.nsILocalFile);
7181     throw new Error("Unknown add-on ID " + aId);
7182   },
7184   /**
7185    * Returns true if the given addon was installed in this location by a text
7186    * file pointing to its real path.
7187    *
7188    * @param aId
7189    *        The ID of the addon
7190    */
7191   isLinkedAddon: function(aId) {
7192     return this._linkedAddons.indexOf(aId) != -1;
7193   }
7196 #ifdef XP_WIN
7198  * An object that identifies a registry install location for add-ons. The location
7199  * consists of a registry key which contains string values mapping ID to the
7200  * path where an add-on is installed
7202  * @param  aName
7203  *         The string identifier of this Install Location.
7204  * @param  aRootKey
7205  *         The root key (one of the ROOT_KEY_ values from nsIWindowsRegKey).
7206  * @param  scope
7207  *         The scope of add-ons installed in this location
7208  */
7209 function WinRegInstallLocation(aName, aRootKey, aScope) {
7210   this.locked = true;
7211   this._name = aName;
7212   this._rootKey = aRootKey;
7213   this._scope = aScope;
7214   this._IDToFileMap = {};
7215   this._FileToIDMap = {};
7217   let path = this._appKeyPath + "\\Extensions";
7218   let key = Cc["@mozilla.org/windows-registry-key;1"].
7219             createInstance(Ci.nsIWindowsRegKey);
7221   // Reading the registry may throw an exception, and that's ok.  In error
7222   // cases, we just leave ourselves in the empty state.
7223   try {
7224     key.open(this._rootKey, path, Ci.nsIWindowsRegKey.ACCESS_READ);
7225   }
7226   catch (e) {
7227     return;
7228   }
7230   this._readAddons(key);
7231   key.close();
7234 WinRegInstallLocation.prototype = {
7235   _name       : "",
7236   _rootKey    : null,
7237   _scope      : null,
7238   _IDToFileMap : null,  // mapping from ID to nsIFile
7239   _FileToIDMap : null,  // mapping from path to ID
7241   /**
7242    * Retrieves the path of this Application's data key in the registry.
7243    */
7244   get _appKeyPath() {
7245     let appVendor = Services.appinfo.vendor;
7246     let appName = Services.appinfo.name;
7248 #ifdef MOZ_THUNDERBIRD
7249     // XXX Thunderbird doesn't specify a vendor string
7250     if (appVendor == "")
7251       appVendor = "Mozilla";
7252 #endif
7254     // XULRunner-based apps may intentionally not specify a vendor
7255     if (appVendor != "")
7256       appVendor += "\\";
7258     return "SOFTWARE\\" + appVendor + appName;
7259   },
7261   /**
7262    * Read the registry and build a mapping between ID and path for each
7263    * installed add-on.
7264    *
7265    * @param  key
7266    *         The key that contains the ID to path mapping
7267    */
7268   _readAddons: function RegInstallLocation__readAddons(aKey) {
7269     let count = aKey.valueCount;
7270     for (let i = 0; i < count; ++i) {
7271       let id = aKey.getValueName(i);
7273       let file = Cc["@mozilla.org/file/local;1"].
7274                 createInstance(Ci.nsILocalFile);
7275       file.initWithPath(aKey.readStringValue(id));
7277       if (!file.exists()) {
7278         WARN("Ignoring missing add-on in " + file.path);
7279         continue;
7280       }
7282       this._IDToFileMap[id] = file;
7283       this._FileToIDMap[file.path] = id;
7284     }
7285   },
7287   /**
7288    * Gets the name of this install location.
7289    */
7290   get name() {
7291     return this._name;
7292   },
7294   /**
7295    * Gets the scope of this install location.
7296    */
7297   get scope() {
7298     return this._scope;
7299   },
7301   /**
7302    * Gets an array of nsIFiles for add-ons installed in this location.
7303    */
7304   get addonLocations() {
7305     let locations = [];
7306     for (let id in this._IDToFileMap) {
7307       locations.push(this._IDToFileMap[id].clone()
7308                          .QueryInterface(Ci.nsILocalFile));
7309     }
7310     return locations;
7311   },
7313   /**
7314    * Gets the ID of the add-on installed in the given nsIFile.
7315    *
7316    * @param  aFile
7317    *         The nsIFile to look in
7318    * @return the ID
7319    * @throws if the file does not represent an installed add-on
7320    */
7321   getIDForLocation: function RegInstallLocation_getIDForLocation(aFile) {
7322     if (aFile.path in this._FileToIDMap)
7323       return this._FileToIDMap[aFile.path];
7324     throw new Error("Unknown add-on location");
7325   },
7327   /**
7328    * Gets the nsIFile that the add-on with the given ID is installed in.
7329    *
7330    * @param  aId
7331    *         The ID of the add-on
7332    * @return the nsIFile
7333    */
7334   getLocationForID: function RegInstallLocation_getLocationForID(aId) {
7335     if (aId in this._IDToFileMap)
7336       return this._IDToFileMap[aId].clone().QueryInterface(Ci.nsILocalFile);
7337     throw new Error("Unknown add-on ID");
7338   },
7340   /**
7341    * @see DirectoryInstallLocation
7342    */
7343   isLinkedAddon: function(aId) {
7344     return true;
7345   }
7347 #endif
7349 AddonManagerPrivate.registerProvider(XPIProvider);