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
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.
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;
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",
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 = {
146 // Map new string type identifiers to old style nsIUpdateItem types
155 * Valid IDs fit this pattern.
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);
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.
178 function SafeInstallOperation() {
179 this._installedFiles = [];
180 this._createdDirs = [];
183 SafeInstallOperation.prototype = {
184 _installedFiles: null,
187 _installFile: function(aFile, aTargetDirectory, aCopy) {
188 let oldFile = aCopy ? null : aFile.clone();
189 let newFile = aFile.clone();
192 newFile.copyTo(aTargetDirectory, null);
194 newFile.moveTo(aTargetDirectory, null);
197 ERROR("Failed to " + (aCopy ? "copy" : "move") + " file " + aFile.path +
198 " to " + aTargetDirectory.path, e);
201 this._installedFiles.push({ oldFile: null, newFile: newFile });
204 _installDirectory: function(aDirectory, aTargetDirectory, aCopy) {
205 let newDir = aTargetDirectory.clone();
206 newDir.append(aDirectory.leafName);
208 newDir.create(Ci.nsILocalFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
211 ERROR("Failed to create directory " + newDir.path, e);
214 this._createdDirs.push(newDir);
216 let entries = aDirectory.directoryEntries
217 .QueryInterface(Ci.nsIDirectoryEnumerator);
218 let cacheEntries = [];
221 while (entry = entries.nextFile)
222 cacheEntries.push(entry);
228 cacheEntries.forEach(function(aEntry) {
230 this._installDirEntry(aEntry, newDir, aCopy);
233 ERROR("Failed to " + (aCopy ? "copy" : "move") + " entry " +
239 // If this is only a copy operation then there is nothing else to do
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
246 aDirectory.permissions = FileUtils.PERMS_DIRECTORY;
247 aDirectory.remove(false);
250 ERROR("Failed to remove directory " + aDirectory.path, e);
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 });
259 _installDirEntry: function(aDirEntry, aTargetDirectory, aCopy) {
261 if (aDirEntry.isDirectory())
262 this._installDirectory(aDirEntry, aTargetDirectory, aCopy);
264 this._installFile(aDirEntry, aTargetDirectory, aCopy);
267 ERROR("Failure " + (aCopy ? "copying" : "moving") + " " + aDirEntry.path +
268 " to " + aTargetDirectory.path);
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.
278 * The file or directory to be moved.
279 * @param aTargetDirectory
280 * The directory to move into, this is expected to be an empty
283 move: function(aFile, aTargetDirectory) {
285 this._installDirEntry(aFile, aTargetDirectory, false);
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.
298 * The file or directory to be copied.
299 * @param aTargetDirectory
300 * The directory to copy into, this is expected to be an empty
303 copy: function(aFile, aTargetDirectory) {
305 this._installDirEntry(aFile, aTargetDirectory, true);
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
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);
326 else if (!move.oldFile) {
327 // No old file means this was a copied file
328 move.newFile.remove(true);
331 move.newFile.moveTo(move.oldFile.parent, null);
335 while (this._createdDirs.length > 0)
336 recursiveRemove(this._createdDirs.pop());
341 * Gets the currently selected locale for display.
342 * @return the selected locale or "en-US" if none is selected
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.
354 * An array of locales
355 * @return the best match for the currently selected locale
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
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)
388 // Count the number of parts that match
389 var maxmatchcount = Math.min(fparts.length, lparts.length);
391 while (matchcount < maxmatchcount &&
392 fparts[matchcount] == lparts[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;
405 // If we found a valid match for this locale return it
413 * Calculates whether an add-on should be appDisabled or not.
416 * The add-on to check
417 * @return true if the add-on should not be appDisabled
419 function isUsableAddon(aAddon) {
420 // Hack to ensure the default theme is always usable
421 if (aAddon.type == "theme" && aAddon.internalName == XPIProvider.defaultSkin)
424 if (aAddon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED)
427 if (XPIProvider.checkUpdateSecurity && !aAddon.providesUpdatesSecurely)
430 if (!aAddon.isPlatformCompatible)
433 if (XPIProvider.checkCompatibility) {
434 if (!aAddon.isCompatible)
438 if (!aAddon.matchingTargetApplication)
445 this.__defineGetter__("gRDF", function() {
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.
459 * The RDF object to convert
460 * @return a string if the object could be converted or null
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;
473 * Gets an RDF property as a string
476 * The RDF datasource to read the property from
478 * The RDF resource to read the property from
480 * The property to read
481 * @return a string if the property existed or null
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.
491 * The URI that the manifest is being read from
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
498 function loadManifestFromRDF(aUri, aStream) {
499 function getPropertyArray(aDs, aSource, aProperty) {
501 let targets = aDs.GetTargets(aSource, EM_R(aProperty), true);
502 while (targets.hasMoreElements())
503 values.push(getRDFValue(targets.getNext()));
509 * Reads locale properties from either the main install manifest root or
510 * an em:localized section in the install manifest.
513 * The nsIRDFDatasource to read from
515 * The nsIRDFResource to read the properties from
517 * True if the locale is to be read from the main install manifest
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
523 * @return an object containing the locale properties
525 function readLocale(aDs, aSource, isDefault, aSeenLocales) {
529 let targets = ds.GetTargets(aSource, EM_R("locale"), true);
530 while (targets.hasMoreElements()) {
531 let localeName = getRDFValue(targets.getNext());
533 WARN("Ignoring empty locale in localized properties");
536 if (aSeenLocales.indexOf(localeName) != -1) {
537 WARN("Ignoring duplicate locale in localized properties");
540 aSeenLocales.push(localeName);
541 locale.locales.push(localeName);
544 if (locale.locales.length == 0) {
545 WARN("Ignoring localized properties with no listed locales");
550 PROP_LOCALE_SINGLE.forEach(function(aProp) {
551 locale[aProp] = getRDFProperty(aDs, aSource, aProp);
554 PROP_LOCALE_MULTI.forEach(function(aProp) {
555 locale[aProp] = getPropertyArray(aDs, aSource,
556 aProp.substring(0, aProp.length - 1));
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);
578 let count = aStream.available();
580 listener.onDataAvailable(channel, null, aStream, pos, count);
582 count = aStream.available();
584 listener.onStopRequest(channel, null, Components.results.NS_OK);
587 listener.onStopRequest(channel, null, e.result);
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);
596 addon.unpack = getRDFProperty(ds, root, "unpack") == "true";
599 addon.type = addon.internalName ? "theme" : "extension";
602 for (let name in TYPES) {
603 if (TYPES[name] == addon.type) {
610 if (!(addon.type in TYPES))
611 throw new Error("Install manifest specifies unknown type: " + addon.type);
613 if (addon.type != "multipackage") {
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);
619 throw new Error("No version in install manifest");
622 // Only read the bootstrapped property for extensions
623 if (addon.type == "extension") {
624 addon.bootstrap = getRDFProperty(ds, root, "bootstrap") == "true";
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";
639 addon.defaultLocale = readLocale(ds, root, true);
641 let seenLocales = [];
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);
648 addon.locales.push(locale);
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);
660 if (!targetAppInfo.id || !targetAppInfo.minVersion ||
661 !targetAppInfo.maxVersion) {
662 WARN("Ignoring invalid targetApplication entry in install manifest");
665 if (seenApplications.indexOf(targetAppInfo.id) != -1) {
666 WARN("Ignoring duplicate targetApplication entry for " + targetAppInfo.id +
667 " in install manifest");
670 seenApplications.push(targetAppInfo.id);
671 addon.targetApplications.push(targetAppInfo);
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) {
684 let pos = aPlatform.indexOf("_");
686 platform.os = aPlatform.substring(0, pos);
687 platform.abi = aPlatform.substring(pos + 1);
690 platform.os = aPlatform;
693 addon.targetPlatforms.push(platform);
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;
704 addon.userDisabled = addon.blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED;
707 addon.appDisabled = !isUsableAddon(addon);
709 addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT;
715 * Loads an AddonInternal object from an add-on extracted in a directory.
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
722 function loadManifestFromDir(aDir) {
723 function getFileSize(aFile) {
724 if (aFile.isSymlink())
727 if (!aFile.isDirectory())
728 return aFile.fileSize;
731 let entries = aFile.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
733 while (entry = entries.nextFile)
734 size += getFileSize(entry);
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 " +
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);
753 let addon = loadManifestFromRDF(Services.io.newFileURI(file), bis);
754 addon._sourceBundle = aDir.clone().QueryInterface(Ci.nsILocalFile);
755 addon.size = getFileSize(aDir);
765 * Loads an AddonInternal object from an nsIZipReader for an add-on.
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
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);
779 let uri = buildJarURI(aZipReader.file, FILE_INSTALL_MANIFEST);
780 let addon = loadManifestFromRDF(uri, bis);
781 addon._sourceBundle = aZipReader.file;
784 let entries = aZipReader.findEntries(null);
785 while (entries.hasMore())
786 addon.size += aZipReader.getEntry(entries.getNext()).realSize;
797 * Loads an AddonInternal object from an add-on in an XPI file.
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
804 function loadManifestFromZipFile(aXPIFile) {
805 let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].
806 createInstance(Ci.nsIZipReader);
808 zipReader.open(aXPIFile);
810 return loadManifestFromZipReader(zipReader);
817 function loadManifestFromFile(aFile) {
819 return loadManifestFromZipFile(aFile);
821 return loadManifestFromDir(aFile);
825 * Creates a jar: URI for a file inside a ZIP file.
828 * The ZIP file as an nsIFile
830 * The path inside the ZIP file
831 * @return an nsIURI for the file
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
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);
856 * Extracts files from a ZIP file into a directory.
859 * The source ZIP file that contains the add-on.
861 * The nsIFile to extract to.
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);
872 let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].
873 createInstance(Ci.nsIZipReader);
874 zipReader.open(aZipFile);
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()) {
884 target.create(Ci.nsILocalFile.DIRECTORY_TYPE,
885 FileUtils.PERMS_DIRECTORY);
888 ERROR("extractFiles: failed to create target directory for " +
889 "extraction file = " + target.path, e);
894 entries = zipReader.findEntries(null);
895 while (entries.hasMore()) {
896 let entryName = entries.getNext();
897 let target = getTargetFile(aDir, entryName);
901 zipReader.extract(entryName, target);
902 target.permissions |= FileUtils.PERMS_FILE;
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.
915 * A nsIZipReader to check
917 * The nsIPrincipal to compare against
918 * @return true if all the contents that should be signed were signed by the
921 function verifyZipSigning(aZip, aPrincipal) {
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/")
929 // Directory entries aren't in the manifest.
930 if (entry.substr(-1) == "/")
933 var entryPrincipal = aZip.getCertificatePrincipal(entry);
934 if (!entryPrincipal || !aPrincipal.equals(entryPrincipal))
937 return aZip.manifestEntriesCount == count;
941 * Replaces %...% strings in an addon url (update and updateInfo) with
942 * appropriate values.
945 * The AddonInternal representing the add-on
949 * An optional number representing the type of update, only applicable
950 * when creating a url for retrieving an update manifest
952 * The optional application version to use for %APP_VERSION%
953 * @return the appropriately escaped uri.
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";
967 var xpcomABI = Services.appinfo.XPCOMABI;
969 xpcomABI = UNKNOWN_XPCOM_ABI;
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
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
991 let app = aAddon.matchingTargetApplication;
993 var maxVersion = app.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)
1001 uri = uri.replace(/%(\w{3,})%/g, function(aMatch, aParam) {
1003 catMan = Cc["@mozilla.org/categorymanager;1"].
1004 getService(Ci.nsICategoryManager);
1008 var contractID = catMan.getCategoryEntry(CATEGORY_UPDATE_PARAMS, aParam);
1009 var paramHandler = Cc[contractID].getService(Ci.nsIPropertyBag2);
1010 return paramHandler.getPropertyAsAString(aParam);
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.
1026 * An object to copy from
1027 * @param aProperties
1028 * An array of properties to be copied
1030 * An optional target object to copy the properties to
1031 * @return the object that the properties were copied onto
1033 function copyProperties(aObject, aProperties, aTarget) {
1036 aProperties.forEach(function(aProp) {
1037 aTarget[aProp] = aObject[aProp];
1043 * Copies properties from a mozIStorageRow to an object. If no target object is
1044 * passed a new object will be created and returned.
1047 * A mozIStorageRow to copy from
1048 * @param aProperties
1049 * An array of properties to be copied
1051 * An optional target object to copy the properties to
1052 * @return the object that the properties were copied onto
1054 function copyRowProperties(aRow, aProperties, aTarget) {
1057 aProperties.forEach(function(aProp) {
1058 aTarget[aProp] = aRow.getResultByName(aProp);
1064 * A generator to synchronously return result rows from an mozIStorageStatement.
1067 * The statement to execute
1069 function resultRows(aStatement) {
1071 while (stepStatement(aStatement))
1072 yield aStatement.row;
1080 * Removes the specified files or directories in a staging directory and then if
1081 * the staging directory is empty attempts to remove it.
1084 * nsIFile for the staging directory to clean up
1086 * An array of file or directory to remove from the directory, the
1087 * array may be empty
1089 function cleanStagingDir(aDir, aLeafNames) {
1090 aLeafNames.forEach(function(aName) {
1091 let file = aDir.clone();
1094 recursiveRemove(file);
1097 let dirEntries = aDir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
1099 if (dirEntries.nextFile)
1107 aDir.permissions = FileUtils.PERMS_DIRECTORY;
1111 // Failing to remove the staging directory is ignorable
1116 * Recursively removes a directory or file fixing permissions when necessary.
1119 * The nsIFile to remove
1121 function recursiveRemove(aFile) {
1122 aFile.permissions = aFile.isDirectory() ? FileUtils.PERMS_DIRECTORY
1123 : FileUtils.PERMS_FILE;
1130 if (!aFile.isDirectory()) {
1131 ERROR("Failed to remove file " + aFile.path, e);
1137 let dirEntries = aFile.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
1139 while (entry = dirEntries.nextFile)
1140 recursiveRemove(entry);
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.
1153 * A non-null nsIFile object
1154 * @return Epoch time, as described above. 0 for an empty directory.
1156 function recursiveLastModifiedTime(aFile) {
1158 return aFile.lastModifiedTime;
1160 if (aFile.isDirectory()) {
1161 let entries = aFile.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
1163 let maxTime = aFile.lastModifiedTime;
1164 while (entry = entries.nextFile) {
1165 time = recursiveLastModifiedTime(entry);
1166 maxTime = Math.max(time, maxTime);
1172 // If the file is something else, just ignore it.
1177 * A helpful wrapper around the prefs service that allows for default values
1178 * when requested values aren't set.
1182 * Gets a preference from the default branch ignoring user-set values.
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
1191 getDefaultCharPref: function(aName, aDefaultValue) {
1193 return Services.prefs.getDefaultBranch("").getCharPref(aName);
1197 return aDefaultValue;
1201 * Gets a string preference.
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
1209 getCharPref: function(aName, aDefaultValue) {
1211 return Services.prefs.getCharPref(aName);
1215 return aDefaultValue;
1219 * Gets a boolean preference.
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
1227 getBoolPref: function(aName, aDefaultValue) {
1229 return Services.prefs.getBoolPref(aName);
1233 return aDefaultValue;
1237 * Gets an integer preference.
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
1245 getIntPref: function(aName, defaultValue) {
1247 return Services.prefs.getIntPref(aName);
1251 return defaultValue;
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
1262 // The default skin for the application
1263 defaultSkin: "classic/1.0",
1264 // The current skin used by the application
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
1270 // The name of the checkCompatibility preference for the current application
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
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
1296 // Add-ons that became disabled for compatibility reasons
1301 * Starts the XPI provider initializes the install locations and prefs.
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
1310 startup: function XPI_startup(aAppChanged) {
1313 this.installLocations = [];
1314 this.installLocationsByName = {};
1316 function addDirectoryInstallLocation(aName, aKey, aPaths, aScope, aLocked) {
1318 var dir = FileUtils.getDir(aKey, aPaths);
1321 // Some directories aren't defined on some platforms, ignore them
1322 LOG("Skipping unavailable install location " + aName);
1327 var location = new DirectoryInstallLocation(aName, dir, aScope, aLocked);
1330 WARN("Failed to add directory install location " + aName, e);
1334 XPIProvider.installLocations.push(location);
1335 XPIProvider.installLocationsByName[location.name] = location;
1338 function addRegistryInstallLocation(aName, aRootkey, aScope) {
1340 var location = new WinRegInstallLocation(aName, aRootkey, aScope);
1343 WARN("Failed to add registry install location " + aName, e);
1347 XPIProvider.installLocations.push(location);
1348 XPIProvider.installLocationsByName[location.name] = location;
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) {
1359 addRegistryInstallLocation("winreg-app-global",
1360 Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
1361 AddonManager.SCOPE_SYSTEM);
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);
1371 if (enabledScopes & AddonManager.SCOPE_APPLICATION) {
1372 addDirectoryInstallLocation(KEY_APP_GLOBAL, KEY_APPDIR,
1374 AddonManager.SCOPE_APPLICATION, true);
1377 if (enabledScopes & AddonManager.SCOPE_USER) {
1379 addRegistryInstallLocation("winreg-app-user",
1380 Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
1381 AddonManager.SCOPE_USER);
1383 addDirectoryInstallLocation(KEY_APP_SYSTEM_USER, "XREUSysExt",
1384 [Services.appinfo.ID],
1385 AddonManager.SCOPE_USER, true);
1388 // The profile location is always enabled
1389 addDirectoryInstallLocation(KEY_APP_PROFILE, KEY_PROFILEDIR,
1391 AddonManager.SCOPE_PROFILE, false);
1393 this.defaultSkin = Prefs.getDefaultCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN,
1395 this.currentSkin = Prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN,
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,
1404 this.checkUpdateSecurity = Prefs.getBoolPref(PREF_EM_CHECK_UPDATE_SECURITY,
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
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();
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(","));
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);
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.
1447 Services.appinfo.annotateCrashReport("Theme", this.currentSkin);
1450 Services.appinfo.annotateCrashReport("EMCheckCompatibility",
1451 this.checkCompatibility);
1453 this.addAddonsToCrashReporter();
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);
1463 // Let these shutdown a little earlier when they still have access to most
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,
1474 BOOTSTRAP_REASONS.APP_SHUTDOWN);
1476 Services.obs.removeObserver(this, "quit-application-granted");
1478 }, "quit-application-granted", false);
1480 this.extensionsActive = true;
1484 * Shuts down the database and releases all references.
1486 shutdown: function XPI_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
1504 if (Prefs.getBoolPref(PREF_PENDING_OPERATIONS, false)) {
1505 XPIDatabase.updateActiveAddons();
1506 XPIDatabase.writeAddonsList();
1507 Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, false);
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);
1523 * Applies any pending theme change to the preferences.
1525 applyThemeChange: function XPI_applyThemeChange() {
1526 if (!Prefs.getBoolPref(PREF_DSS_SWITCHPENDING, false))
1529 // Tell the Chrome Registry which Skin to select
1531 this.selectedSkin = Prefs.getCharPref(PREF_DSS_SKIN_TO_SELECT);
1532 Services.prefs.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN,
1534 Services.prefs.clearUserPref(PREF_DSS_SKIN_TO_SELECT);
1535 LOG("Changed skin to " + this.selectedSkin);
1536 this.currentSkin = this.selectedSkin;
1539 ERROR("Error applying theme change", e);
1541 Services.prefs.clearUserPref(PREF_DSS_SWITCHPENDING);
1545 * Shows the "Compatibility Updates" UI
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);
1564 * Adds a list of currently active add-ons to the next crash report.
1566 addAddonsToCrashReporter: function XPI_addAddonsToCrashReporter() {
1567 if (!("nsICrashReporter" in Ci) ||
1568 !(Services.appinfo instanceof Ci.nsICrashReporter))
1571 // In safe mode no add-ons are loaded so we should not include them in the
1573 if (Services.appinfo.inSafeMode)
1576 let data = this.enabledAddons;
1577 for (let id in this.bootstrappedAddons)
1578 data += (data ? "," : "") + id + ":" + this.bootstrappedAddons[id].version;
1581 Services.appinfo.annotateCrashReport("Add-ons", data);
1587 * Gets the add-on states for an install location.
1588 * This function may be expensive because of the recursiveLastModifiedTime call.
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.
1597 getAddonStates: function XPI_getAddonStates(aLocation) {
1598 let addonStates = {};
1599 aLocation.addonLocations.forEach(function(file) {
1600 let id = aLocation.getIDForLocation(file);
1602 descriptor: file.persistentDescriptor,
1603 mtime: recursiveLastModifiedTime(file)
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.
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
1619 getInstallLocationStates: function XPI_getInstallLocationStates() {
1621 this.installLocations.forEach(function(aLocation) {
1622 let addons = aLocation.addonLocations;
1623 if (addons.length == 0)
1626 let locationState = {
1627 name: aLocation.name,
1628 addons: this.getAddonStates(aLocation)
1631 states.push(locationState);
1637 * Check the staging directories of install locations for any add-ons to be
1638 * installed or add-ons to be uninstalled.
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
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)
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);
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))
1675 if (file.isDirectory())
1678 let extension = file.leafName;
1679 extension = extension.substring(extension.length - 4);
1681 if (extension != ".xpi" && extension != ".jar")
1693 addon = loadManifestFromZipFile(stagedXPI);
1696 ERROR("Unable to read add-on manifest for " + stagedXPI.leafName +
1697 " in XPI stage of " + aLocation.name, e);
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);
1707 targetDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
1710 ERROR("Failed to create staging directory for add-on " + id, e);
1715 extractFiles(stagedXPI, targetDir);
1718 ERROR("Failed to extract staged XPI for add-on " + id + " in " +
1724 stagedXPI.moveTo(stagingDir, addon.id + ".xpi");
1727 ERROR("Failed to move staged XPI for add-on " + id + " in " +
1735 if (stagedXPIDir.exists()) {
1737 recursiveRemove(stagedXPIDir);
1740 // Non-critical, just saves some perf on startup if we clean this up.
1741 LOG("Error removing XPI staging dir " + stagedXPIDir.path, e);
1745 if (!stagingDir || !stagingDir.exists() || !stagingDir.isDirectory())
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);
1759 if (id.substring(id.length - 5).toLowerCase() != ".json")
1760 WARN("Ignoring file: " + stageDirEntry.path);
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);
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);
1784 aLocation.uninstallAddon(id);
1787 ERROR("Failed to uninstall add-on " + id + " in " + aLocation.name, e);
1789 // The file check later will spot the removal and cleanup the database
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);
1809 fis.init(jsonfile, -1, 0, 0);
1810 aManifests[aLocation.name][id] = json.decodeFromStream(fis,
1812 existingAddonID = aManifests[aLocation.name][id].existingAddonID;
1815 ERROR("Unable to read add-on manifest for " + id + " in " +
1823 LOG("Processing install of " + id + " in " + aLocation.name);
1825 var addonInstallLocation = aLocation.installAddon(id, stageDirEntry,
1827 if (aManifests[aLocation.name][id])
1828 aManifests[aLocation.name][id]._sourceBundle = addonInstallLocation;
1831 ERROR("Failed to install staged add-on " + id + " in " + aLocation.name,
1833 delete aManifests[aLocation.name][id];
1840 recursiveRemove(stagingDir);
1843 // Non-critical, just saves some perf on startup if we clean this up.
1844 LOG("Error removing staging dir " + stagingDir.path, e);
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.
1857 * A dictionary to add new install manifests to to save having to
1859 * @return true if any new add-ons were installed
1861 installDistributionAddons: function XPI_installDistributionAddons(aManifests) {
1864 distroDir = FileUtils.getDir(KEY_APP_DISTRIBUTION, [DIR_EXTENSIONS]);
1870 if (!distroDir.exists())
1873 if (!distroDir.isDirectory())
1876 let changed = false;
1877 let profileLocation = this.installLocationsByName[KEY_APP_PROFILE];
1879 let entries = distroDir.directoryEntries
1880 .QueryInterface(Ci.nsIDirectoryEnumerator);
1882 while (entry = entries.nextFile) {
1883 // Should never happen really
1884 if (!(entry instanceof Ci.nsILocalFile))
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);
1894 LOG("Ignoring distribution add-on that isn't an XPI: " + entry.path);
1898 else if (!entry.isDirectory()) {
1899 LOG("Ignoring distribution add-on that isn't a file or directory: " +
1904 if (!gIDTest.test(id)) {
1905 LOG("Ignoring distribution add-on whose name is not a valid add-on ID: " +
1912 addon = loadManifestFromFile(entry);
1915 WARN("File entry " + entry.path + " contains an invalid add-on", e);
1919 if (addon.id != id) {
1920 WARN("File entry " + entry.path + " contains an add-on with an " +
1925 let existingEntry = null;
1927 existingEntry = profileLocation.getLocationForID(id);
1932 if (existingEntry) {
1935 existingAddon = loadManifestFromFile(existingEntry);
1937 if (Services.vc.compare(addon.version, existingAddon.version) <= 0)
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);
1946 else if (Prefs.getBoolPref(PREF_BRANCH_INSTALLED_ADDON + id, false)) {
1950 // Install the add-on
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;
1966 ERROR("Failed to install distribution add-on " + entry.path, e);
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.
1982 * The array of current install location states
1984 * A dictionary of cached AddonInstalls for add-ons that have been
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
1999 processFileChanges: function XPI_processFileChanges(aState, aManifests,
2000 aUpdateCompatibility,
2003 let visibleAddons = {};
2004 let oldBootstrappedAddons = this.bootstrappedAddons;
2005 this.bootstrappedAddons = {};
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.
2012 * @param aInstallLocation
2013 * The install location containing the add-on
2015 * The AddonInternal as it appeared the last time the application
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
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];
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;
2037 // The ID in the manifest that was loaded must match the ID of the old
2039 if (newAddon.id != aOldAddon.id)
2040 throw new Error("Incorrect id in install manifest");
2043 WARN("Add-on is invalid", e);
2044 XPIDatabase.removeAddonMetadata(aOldAddon);
2045 if (!aInstallLocation.locked)
2046 aInstallLocation.uninstallAddon(aOldAddon.id);
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)
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);
2079 // Otherwise the caches will need to be invalidated
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
2092 * @param aInstallLocation
2093 * The install location containing the add-on
2095 * The AddonInternal as it appeared the last time the application
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
2102 function updateVisibilityAndCompatibility(aInstallLocation, aOldAddon,
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,
2119 BOOTSTRAP_REASONS.ADDON_INSTALL);
2121 // If it should be active then mark it as active otherwise unload
2123 if (!aOldAddon.appDisabled && !aOldAddon.userDisabled) {
2124 aOldAddon.active = true;
2125 XPIDatabase.updateAddonActive(aOldAddon);
2128 XPIProvider.unloadBootstrapScope(newAddon.id);
2132 // Otherwise a restart is necessary
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
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);
2177 if (aOldAddon.visible && aOldAddon.active && aOldAddon.bootstrap) {
2178 XPIProvider.bootstrappedAddons[aOldAddon.id] = {
2179 version: aOldAddon.version,
2180 descriptor: aAddonState.descriptor
2188 * Called when an add-on has been removed.
2190 * @param aInstallLocation
2191 * The install location containing the add-on
2193 * The AddonInternal as it appeared the last time the application
2195 * @return a boolean indicating if restarting the application is required
2196 * to complete changing this add-on
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
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)
2218 * Called when a new add-on has been detected.
2220 * @param aInstallLocation
2221 * The install location containing the add-on
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
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];
2242 // Otherwise load the manifest from the add-on
2244 let file = aInstallLocation.getLocationForID(aId);
2245 newAddon = loadManifestFromFile(file);
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");
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);
2259 WARN("Could not uninstall invalid item from locked install location");
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.
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);
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;
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;
2301 newAddon.active = (newAddon.visible && !newAddon.userDisabled &&
2302 !newAddon.appDisabled)
2306 // Update the database.
2307 XPIDatabase.addAddonMetadata(newAddon, aAddonState.descriptor);
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
2313 ERROR("Failed to add add-on " + aId + " in " + aInstallLocation.name +
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",
2342 XPIProvider.unloadBootstrapScope(newAddon.id);
2345 if (!newAddon.bootstrap)
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);
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);
2376 knownLocations.splice(pos, 1);
2377 let addons = XPIDatabase.getAddonsInLocation(installLocation.name);
2378 // Iterate through the add-ons installed the last time the application
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
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) ||
2403 changed = updateVisibilityAndCompatibility(installLocation,
2404 aOldAddon, addonState) ||
2407 if (aOldAddon.visible && aOldAddon._installLocation.name != KEY_APP_GLOBAL)
2408 XPIProvider.allAppGlobal = false;
2411 changed = removeMetadata(installLocation, aOldAddon) || changed;
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;
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
2432 knownLocations.forEach(function(aLocation) {
2433 let addons = XPIDatabase.getAddonsInLocation(aLocation);
2434 addons.forEach(function(aOldAddon) {
2435 changed = removeMetadata(aLocation, aOldAddon) || changed;
2439 // Cache the new install location states
2440 cache = JSON.stringify(this.getInstallLocationStates());
2441 Services.prefs.setCharPref(PREF_INSTALL_CACHE, cache);
2447 * Imports the xpinstall permissions from preferences into the permissions
2448 * manager for the user to change later.
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, "");
2458 hosts.split(",").forEach(function(aHost) {
2459 Services.perms.add(NetUtil.newURI("http://" + aHost), XPI_PERMISSION,
2463 Services.prefs.setCharPref(aPref, "");
2467 importList(PREF_XPI_WHITELIST_PERMISSIONS,
2468 Ci.nsIPermissionManager.ALLOW_ACTION);
2469 importList(PREF_XPI_BLACKLIST_PERMISSIONS,
2470 Ci.nsIPermissionManager.DENY_ACTION);
2474 * Checks for any changes that have occurred since the last time the
2475 * application was launched.
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
2483 * @return true if a change requiring a restart was detected
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
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
2517 this.bootstrappedAddons = JSON.parse(Prefs.getCharPref(PREF_BOOTSTRAP_ADDONS,
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);
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);
2540 bootstrapDescriptors.splice(pos, 1);
2544 if (bootstrapDescriptors.length > 0) {
2545 WARN("Bootstrap state is invalid (missing add-ons: " + bootstrapDescriptors.toSource() + ")");
2546 updateDatabase = true;
2550 // Catch any errors during the main startup and rollback the database changes
2551 XPIDatabase.beginTransaction();
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);
2560 extensionListChanged = this.processFileChanges(state, manifests,
2565 ERROR("Error processing file changes", e);
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();
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);
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));
2598 LOG("No changes found");
2599 XPIDatabase.commitTransaction();
2602 ERROR("Error during startup file checks, rolling back any database " +
2604 XPIDatabase.rollbackTransaction();
2607 // Check that the add-ons list still exists
2608 let addonsList = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST],
2610 if (!addonsList.exists()) {
2611 LOG("Add-ons list is missing, recreating");
2612 XPIDatabase.writeAddonsList();
2619 * Called to test whether this provider supports installing a particular
2623 * The mimetype to check for
2624 * @return true if the mimetype is application/x-xpinstall
2626 supportsMimetype: function XPI_supportsMimetype(aMimetype) {
2627 return aMimetype == "application/x-xpinstall";
2631 * Called to test whether installing XPI add-ons is enabled.
2633 * @return true if installing is enabled
2635 isInstallEnabled: function XPI_isInstallEnabled() {
2636 // Default to enabled if the preference does not exist
2637 return Prefs.getBoolPref(PREF_XPI_ENABLED, true);
2641 * Called to test whether installing XPI add-ons from a URI is allowed.
2644 * The URI being installed from
2645 * @return true if installing is allowed
2647 isInstallAllowed: function XPI_isInstallAllowed(aUri) {
2648 if (!this.isInstallEnabled())
2654 // file: and chrome: don't need whitelisted hosts
2655 if (aUri.schemeIs("chrome") || aUri.schemeIs("file"))
2659 let permission = Services.perms.testPermission(aUri, XPI_PERMISSION);
2660 if (permission == Ci.nsIPermissionManager.DENY_ACTION)
2663 let requireWhitelist = Prefs.getBoolPref(PREF_XPI_WHITELIST_REQUIRED, true);
2664 if (requireWhitelist && (permission != Ci.nsIPermissionManager.ALLOW_ACTION))
2671 * Called to get an AddonInstall to download and install an add-on from a URL.
2674 * The URL to be installed
2676 * A hash for the install
2678 * A name for the install
2680 * An icon URL for the install
2682 * A version for the install
2684 * An nsILoadGroup to associate requests with
2686 * A callback to pass the AddonInstall to
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);
2696 * Called to get an AddonInstall to install an add-on from a local file.
2699 * The file to be installed
2701 * A callback to pass the AddonInstall to
2703 getInstallForFile: function XPI_getInstallForFile(aFile, aCallback) {
2704 AddonInstall.createInstall(function(aInstall) {
2706 aCallback(aInstall.wrapper);
2713 * Removes an AddonInstall from the list of active installs.
2716 * The AddonInstall to remove
2718 removeActiveInstall: function XPI_removeActiveInstall(aInstall) {
2719 this.installs = this.installs.filter(function(i) i != aInstall);
2723 * Called to get an Addon with a particular ID.
2726 * The ID of the add-on to retrieve
2728 * A callback to pass the Addon to
2730 getAddonByID: function XPI_getAddonByID(aId, aCallback) {
2731 XPIDatabase.getVisibleAddonForID(aId, function(aAddon) {
2733 aCallback(createWrapper(aAddon));
2740 * Called to get Addons of a particular type.
2743 * An array of types to fetch. Can be null to get all types.
2745 * A callback to pass an array of Addons to
2747 getAddonsByTypes: function XPI_getAddonsByTypes(aTypes, aCallback) {
2748 XPIDatabase.getVisibleAddons(aTypes, function(aAddons) {
2749 aCallback([createWrapper(a) for each (a in aAddons)]);
2754 * Called to get Addons that have pending operations.
2757 * An array of types to fetch. Can be null to get all types
2759 * A callback to pass an array of Addons to
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));
2775 * Called to get the current AddonInstalls, optionally limiting to a list of
2779 * An array of types or null to get all types
2781 * A callback to pass the array of AddonInstalls to
2783 getInstallsByTypes: function XPI_getInstallsByTypes(aTypes, aCallback) {
2785 this.installs.forEach(function(aInstall) {
2786 if (!aTypes || aTypes.indexOf(aInstall.type) >= 0)
2787 results.push(aInstall.wrapper);
2793 * Called when a new add-on has been enabled when only one add-on of that type
2797 * The ID of the newly enabled add-on
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
2804 addonChanged: function XPI_addonChanged(aId, aType, aPendingRestart) {
2805 // We only care about themes in this provider
2806 if (aType != "theme")
2810 // Fallback to the default theme when no theme was enabled
2811 this.enableDefaultTheme();
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)
2823 if (aTheme.id == aId)
2824 newSkin = aTheme.internalName;
2825 else if (aTheme.userDisabled == false && !aTheme.pendingUninstall)
2826 previousTheme = aTheme;
2829 if (aPendingRestart) {
2830 Services.prefs.setBoolPref(PREF_DSS_SWITCHPENDING, true);
2831 Services.prefs.setCharPref(PREF_DSS_SKIN_TO_SELECT, newSkin);
2833 else if (newSkin == this.currentSkin) {
2835 Services.prefs.clearUserPref(PREF_DSS_SWITCHPENDING);
2839 Services.prefs.clearUserPref(PREF_DSS_SKIN_TO_SELECT);
2844 Services.prefs.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN, newSkin);
2845 this.currentSkin = newSkin;
2847 this.selectedSkin = newSkin;
2849 // Flush the preferences to disk so they don't get out of sync with the
2851 Services.prefs.savePrefFile(null);
2853 // Mark the previous theme as disabled. This won't cause recursion since
2854 // only enabled calls notifyAddonChanged.
2856 this.updateAddonDisabledState(previousTheme, true);
2860 * Update the appDisabled property for all add-ons.
2862 updateAddonAppDisabledStates: function XPI_updateAddonAppDisabledStates() {
2863 let addons = XPIDatabase.getAddons();
2864 addons.forEach(function(aAddon) {
2865 this.updateAddonDisabledState(aAddon);
2870 * When the previously selected theme is removed this method will be called
2871 * to enable the default theme.
2873 enableDefaultTheme: function XPI_enableDefaultTheme() {
2874 LOG("Activating default theme");
2875 let addon = XPIDatabase.getVisibleAddonForInternalName(this.defaultSkin);
2877 this.updateAddonDisabledState(addon, false);
2879 WARN("Unable to activate the default theme");
2883 * Notified when a preference we're interested in has changed.
2887 observe: function XPI_observe(aSubject, aTopic, aData) {
2889 case this.checkCompatibilityPref:
2890 case PREF_EM_CHECK_UPDATE_SECURITY:
2891 this.checkCompatibility = Prefs.getBoolPref(this.checkCompatibilityPref,
2893 this.checkUpdateSecurity = Prefs.getBoolPref(PREF_EM_CHECK_UPDATE_SECURITY,
2895 this.updateAllAddonDisabledStates();
2901 * Tests whether enabling an add-on will require a restart.
2904 * The add-on to test
2905 * @return true if the operation requires a restart
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)
2913 // If the application is in safe mode then any change can be made without
2915 if (Services.appinfo.inSafeMode)
2918 // Anything that is active is already enabled
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))
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;
2934 return !aAddon.bootstrap;
2938 * Tests whether disabling an add-on will require a restart.
2941 * The add-on to test
2942 * @return true if the operation requires a restart
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)
2950 // If the application is in safe mode then any change can be made without
2952 if (Services.appinfo.inSafeMode)
2955 // Anything that isn't active is already disabled
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))
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)
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;
2980 return !aAddon.bootstrap;
2984 * Tests whether installing an add-on will require a restart.
2987 * The add-on to test
2988 * @return true if the operation requires a restart
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)
2996 // If the application is in safe mode then any change can be made without
2998 if (Services.appinfo.inSafeMode)
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)
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
3014 let existingAddon = aAddon._install.existingAddon;
3015 if (existingAddon && this.uninstallRequiresRestart(existingAddon))
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)
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
3027 return aAddon.type == "theme" || !aAddon.bootstrap;
3031 * Tests whether uninstalling an add-on will require a restart.
3034 * The add-on to test
3035 * @return true if the operation requires a restart
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)
3043 // If the application is in safe mode then any change can be made without
3045 if (Services.appinfo.inSafeMode)
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);
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.
3062 * The nsILocalFile for the add-on
3064 * The add-on's version
3065 * @return a JavaScript scope
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] = {
3072 descriptor: aFile.persistentDescriptor
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;
3084 if (!bootstrap.exists()) {
3085 ERROR("Attempted to load bootstrap scope from missing directory " + bootstrap.path);
3089 if (bootstrap.isDirectory()) {
3090 bootstrap.append("bootstrap.js");
3091 let uri = Services.io.newFileURI(bootstrap);
3094 spec = buildJarURI(bootstrap, "bootstrap.js").spec;
3096 if (bootstrap.exists()) {
3097 let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
3098 createInstance(Ci.mozIJSSubScriptLoader);
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");
3111 WARN("Error loading bootstrap.js for " + aId, e);
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];
3119 WARN("Bootstrap missing for " + aId);
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.
3130 unloadBootstrapScope: function XPI_unloadBootstrapScope(aId) {
3131 delete this.bootstrapScopes[aId];
3132 delete this.bootstrappedAddons[aId];
3133 this.addAddonsToCrashReporter();
3137 * Calls a bootstrap method for an add-on.
3140 * The ID of the add-on
3142 * The version of the add-on
3144 * The nsILocalFile for the add-on
3146 * The name of the bootstrap method to call
3148 * The reason flag to pass to the bootstrap's startup method
3150 callBootstrapMethod: function XPI_callBootstrapMethod(aId, aVersion, aFile,
3152 // Never call any bootstrap methods in safe mode
3153 if (Services.appinfo.inSafeMode)
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);
3168 installPath: aFile.clone()
3171 LOG("Calling bootstrap method " + aMethod + " on " + aId + " version " +
3174 this.bootstrapScopes[aId][aMethod](params, aReason);
3177 WARN("Exception running bootstrap method " + aMethod + " on " +
3183 * Updates the appDisabled property for all add-ons.
3185 updateAllAddonDisabledStates: function XPI_updateAllAddonDisabledStates() {
3186 let addons = XPIDatabase.getAddons();
3187 addons.forEach(function(aAddon) {
3188 this.updateAddonDisabledState(aAddon);
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.
3198 * The DBAddonInternal to update
3199 * @param aUserDisabled
3200 * Value for the userDisabled property. If undefined the value will
3202 * @throws if addon is not a DBAddonInternal
3204 updateAddonDisabledState: function XPI_updateAddonDisabledState(aAddon,
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)
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
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))
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);
3242 var needsRestart = this.disableRequiresRestart(aAddon);
3243 AddonManagerPrivate.callAddonListeners("onDisabling", wrapper,
3247 needsRestart = this.enableRequiresRestart(aAddon);
3248 AddonManagerPrivate.callAddonListeners("onEnabling", wrapper,
3252 if (!needsRestart) {
3253 aAddon.active = !isDisabled;
3254 XPIDatabase.updateAddonActive(aAddon);
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);
3262 AddonManagerPrivate.callAddonListeners("onDisabled", wrapper);
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);
3270 AddonManagerPrivate.callAddonListeners("onEnabled", wrapper);
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);
3281 * Uninstalls an add-on, immediately if possible or marks it as pending
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
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
3310 Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
3313 // If the add-on is not visible then there is no need to notify listeners.
3314 if (!aAddon.visible)
3317 let wrapper = createWrapper(aAddon);
3318 AddonManagerPrivate.callAddonListeners("onUninstalling", wrapper,
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);
3328 this.callBootstrapMethod(aAddon.id, aAddon.version, file, "uninstall",
3329 BOOTSTRAP_REASONS.ADDON_UNINSTALL);
3330 this.unloadBootstrapScope(aAddon.id);
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);
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);
3359 XPIProvider.unloadBootstrapScope(aAddon.id);
3363 // We always send onInstalled even if a restart is required to enable
3364 // the revealed add-on
3365 AddonManagerPrivate.callAddonListeners("onInstalled", wrappedAddon);
3368 function checkInstallLocation(aPos) {
3372 let location = XPIProvider.installLocations[aPos];
3373 XPIDatabase.getAddonInLocation(aAddon.id, location.name, function(aNewAddon) {
3375 revealAddon(aNewAddon);
3377 checkInstallLocation(aPos - 1);
3381 checkInstallLocation(this.installLocations.length - 1);
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);
3390 * Cancels the pending uninstall of an add-on.
3393 * The DBAddonInternal to cancel uninstall for
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
3405 if (!aAddon.visible)
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);
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.
3431 * The storage error code associated with the error
3432 * @param aErrorString
3435 function logSQLError(aError, aErrorString) {
3436 ERROR("SQL error " + aError + ": " + aErrorString);
3440 * A helper function to log any errors that occur during async statements.
3443 * A mozIStorageError to log
3445 function asyncErrorLogger(aError) {
3446 logSQLError(aError.result, aError.message);
3450 * A helper function to execute a statement synchronously and log any error
3454 * A mozIStorageStatement to execute
3456 function executeStatement(aStatement) {
3458 aStatement.execute();
3461 logSQLError(XPIDatabase.connection.lastError,
3462 XPIDatabase.connection.lastErrorString);
3468 * A helper function to step a statement synchronously and log any error that
3472 * A mozIStorageStatement to execute
3474 function stepStatement(aStatement) {
3476 return aStatement.executeStep();
3479 logSQLError(XPIDatabase.connection.lastError,
3480 XPIDatabase.connection.lastErrorString);
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.
3492 * A callback function to pass the array of DBAddonInternals to
3494 function AsyncAddonListCallback(aCallback) {
3495 this.callback = aCallback;
3499 AsyncAddonListCallback.prototype = {
3505 handleResult: function(aResults) {
3507 while (row = aResults.getNextRow()) {
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);
3518 if ("getCachedAddonByID" in AddonRepository)
3519 AddonRepository.getCachedAddonByID(aAddon.id, completeAddon);
3521 completeAddon(null);
3526 handleError: asyncErrorLogger,
3528 handleCompletion: function(aReason) {
3529 this.complete = true;
3530 if (this.addons.length == this.count)
3531 this.callback(this.addons);
3536 // true if the database connection has been opened
3538 // A cache of statements that are used and need to be finalized on shutdown
3540 // A cache of weak referenced DBAddonInternals so we can reuse objects where
3543 // The nested transaction count
3544 transactionCount: 0,
3546 // The statements used by the database
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, " +
3578 addAddonMetadata_targetApplication: "INSERT INTO targetApplication VALUES " +
3579 "(:internal_id, :id, :minVersion, " +
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, " +
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'"
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.
3639 beginTransaction: function XPIDB_beginTransaction() {
3640 if (this.initialized)
3641 this.getStatement("createSavepoint").execute();
3642 this.transactionCount++;
3646 * Commits the most recent transaction. The data may still be rolled back if
3647 * an outer transaction is rolled back.
3649 commitTransaction: function XPIDB_commitTransaction() {
3650 if (this.transactionCount == 0) {
3651 ERROR("Attempt to commit one transaction too many.");
3655 if (this.initialized)
3656 this.getStatement("releaseSavepoint").execute();
3657 this.transactionCount--;
3661 * Rolls back the most recent transaction. The database will return to its
3662 * state when the transaction was started.
3664 rollbackTransaction: function XPIDB_rollbackTransaction() {
3665 if (this.transactionCount == 0) {
3666 ERROR("Attempt to rollback one transaction too many.");
3670 if (this.initialized) {
3671 this.getStatement("rollbackSavepoint").execute();
3672 this.getStatement("releaseSavepoint").execute();
3674 this.transactionCount--;
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.
3683 * The nsIFile to open
3684 * @return the mozIStorageConnection for the database
3686 openDatabaseFile: function XPIDB_openDatabaseFile(aDBFile) {
3687 LOG("Opening database");
3688 let connection = null;
3690 // Attempt to open the database
3692 connection = Services.storage.openUnsharedDatabase(aDBFile);
3695 ERROR("Failed to open database (1st attempt)", e);
3697 aDBFile.remove(true);
3700 ERROR("Failed to remove database that could not be opened", e);
3703 connection = Services.storage.openUnsharedDatabase(aDBFile);
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");
3715 connection.executeSimpleSQL("PRAGMA synchronous = FULL");
3716 connection.executeSimpleSQL("PRAGMA locking_mode = EXCLUSIVE");
3722 * Opens a new connection to the database file.
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
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();
3752 if (dbfile.exists())
3753 dbfile.remove(true);
3755 // Reopen an empty database
3756 this.connection = this.openDatabaseFile(dbfile);
3759 ERROR("Failed to remove old database", e);
3760 // If the file couldn't be deleted then fall back to an in-memory
3762 this.connection = Services.storage.openSpecialDatabase("memory");
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();
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();
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();
3786 ERROR("Error processing file changes", e);
3787 this.rollbackTransaction();
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);
3802 Services.prefs.clearUserPref(PREF_DB_SCHEMA);
3805 // The preference may not be defined
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'");
3817 * A lazy getter for the database connection.
3820 this.openConnection(true);
3821 return this.connection;
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
3830 * @return an array of persisitent descriptors for the directories
3832 getActiveBundles: function XPIDB_getActiveBundles() {
3835 let addonsList = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST],
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);
3855 * Retrieves migration data from the old extensions.rdf database.
3857 * @return an object holding information about what add-ons were previously
3858 * userDisabled and any updated compatibility information
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");
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: []
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"),
3891 while (targetApps.hasMoreElements()) {
3892 let targetApp = targetApps.getNext()
3893 .QueryInterface(Ci.nsIRDFResource);
3895 id: getRDFProperty(ds, targetApp, "id")
3898 let minVersion = getRDFProperty(ds, targetApp, "updatedMinVersion");
3900 appInfo.minVersion = minVersion;
3901 appInfo.maxVersion = getRDFProperty(ds, targetApp, "updatedMaxVersion");
3904 appInfo.minVersion = getRDFProperty(ds, targetApp, "minVersion");
3905 appInfo.maxVersion = getRDFProperty(ds, targetApp, "maxVersion");
3907 migrateData[location][id].targetApplications.push(appInfo);
3917 * Retrieves migration data from a database that has an older or newer schema.
3919 * @return an object holding information about what add-ons were previously
3920 * userDisabled and any updated compatibility information
3922 getMigrateDataFromDatabase: function XPIDB_getMigrateDataFromDatabase() {
3923 let migrateData = {};
3925 // Attempt to migrate data from a different (even future!) version of the
3928 var stmt = this.connection.createStatement("SELECT internal_id, id, " +
3929 "location, userDisabled, " +
3930 "installDate, version " +
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: []
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({
3956 minVersion: row.minVersion,
3957 maxVersion: row.maxVersion
3964 // An error here means the schema is too different to read
3965 ERROR("Error migrating data", e);
3978 * Shuts down the database connection and releases all cached objects.
3980 shutdown: function XPIDB_shutdown(aCallback) {
3981 if (this.initialized) {
3982 for each (let stmt in this.statementCache)
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();
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;
4004 connection.asyncClose(aCallback);
4013 * Gets a cached statement or creates a new statement if it doesn't already
4017 * A unique key to reference the statement
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
4023 getStatement: function XPIDB_getStatement(aKey, aSql) {
4024 if (aKey in this.statementCache)
4025 return this.statementCache[aKey];
4027 aSql = this.statements[aKey];
4030 return this.statementCache[aKey] = this.connection.createStatement(aSql);
4033 ERROR("Error creating statement " + aKey + " (" + aSql + ")");
4039 * Creates the schema in the database.
4041 createSchema: function XPIDB_createSchema() {
4042 LOG("Creating database schema");
4043 this.beginTransaction();
4045 // Any errors in here should rollback the transaction
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, " +
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 " +
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; " +
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; " +
4092 this.connection.executeSimpleSQL("CREATE TRIGGER delete_locale AFTER " +
4093 "DELETE ON locale BEGIN " +
4094 "DELETE FROM locale_strings WHERE locale_id=old.id; " +
4096 this.connection.schemaVersion = DB_SCHEMA;
4097 this.commitTransaction();
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;
4110 * Synchronously reads the multi-value locale strings for a locale
4113 * The locale object to read into
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);
4127 * Synchronously reads the locales for an add-on
4130 * The DBAddonInternal to read the locales for
4131 * @return the array of locales
4133 _getLocales: function XPIDB__getLocales(aAddon) {
4134 let stmt = this.getStatement("_getLocales");
4137 stmt.params.internal_id = aAddon._internal_id;
4138 for (let row in resultRows(stmt)) {
4141 locales: [row.locale]
4143 copyProperties(row, PROP_LOCALE_SINGLE, locale);
4144 locales.push(locale);
4146 locales.forEach(function(aLocale) {
4147 this._readLocaleStrings(aLocale);
4153 * Synchronously reads the default locale for an add-on
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
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;
4169 this._readLocaleStrings(locale);
4174 * Synchronously reads the target application entries for an add-on
4177 * The DBAddonInternal to read the target applications for
4178 * @return an array of target applications
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))];
4188 * Synchronously reads the target platform entries for an add-on
4191 * The DBAddonInternal to read the target platforms for
4192 * @return an array of target platforms
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))];
4202 * Synchronously makes a DBAddonInternal from a storage row or returns one
4206 * The storage row to make the DBAddonInternal from
4207 * @return a DBAddonInternal
4209 makeAddonFromRow: function XPIDB_makeAddonFromRow(aRow) {
4210 if (this.addonCache[aRow.internal_id]) {
4211 let addon = this.addonCache[aRow.internal_id].get();
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;
4227 addon._sourceBundle = addon._installLocation.getLocationForID(addon.id);
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.
4235 this.addonCache[aRow.internal_id] = Components.utils.getWeakReference(addon);
4240 * Asynchronously fetches additional metadata for a DBAddonInternal.
4243 * The DBAddonInternal
4245 * The callback to call when the metadata is completely retrieved
4247 fetchAddonMetadata: function XPIDB_fetchAddonMetadata(aAddon) {
4248 function readLocaleStrings(aLocale, aCallback) {
4249 let stmt = XPIDatabase.getStatement("_readLocaleStrings");
4251 stmt.params.id = aLocale.id;
4253 handleResult: function(aResults) {
4255 while (row = aResults.getNextRow()) {
4256 let type = row.getResultByName("type");
4257 if (!(type in aLocale))
4259 aLocale[type].push(row.getResultByName("value"));
4263 handleError: asyncErrorLogger,
4265 handleCompletion: function(aReason) {
4271 function readDefaultLocale() {
4272 delete aAddon.defaultLocale;
4273 let stmt = XPIDatabase.getStatement("_getDefaultLocale");
4275 stmt.params.id = aAddon._defaultLocale;
4277 handleResult: function(aResults) {
4278 aAddon.defaultLocale = copyRowProperties(aResults.getNextRow(),
4279 PROP_LOCALE_SINGLE);
4280 aAddon.defaultLocale.id = aAddon._defaultLocale;
4283 handleError: asyncErrorLogger,
4285 handleCompletion: function(aReason) {
4286 if (aAddon.defaultLocale) {
4287 readLocaleStrings(aAddon.defaultLocale, readLocales);
4290 ERROR("Missing default locale for " + aAddon.id);
4297 function readLocales() {
4298 delete aAddon.locales;
4299 aAddon.locales = [];
4300 let stmt = XPIDatabase.getStatement("_getLocales");
4302 stmt.params.internal_id = aAddon._internal_id;
4304 handleResult: function(aResults) {
4306 while (row = aResults.getNextRow()) {
4308 id: row.getResultByName("id"),
4309 locales: [row.getResultByName("locale")]
4311 copyRowProperties(row, PROP_LOCALE_SINGLE, locale);
4312 aAddon.locales.push(locale);
4316 handleError: asyncErrorLogger,
4318 handleCompletion: function(aReason) {
4320 function readNextLocale() {
4321 if (pos < aAddon.locales.length)
4322 readLocaleStrings(aAddon.locales[pos++], readNextLocale);
4324 readTargetApplications();
4332 function readTargetApplications() {
4333 delete aAddon.targetApplications;
4334 aAddon.targetApplications = [];
4335 let stmt = XPIDatabase.getStatement("_getTargetApplications");
4337 stmt.params.internal_id = aAddon._internal_id;
4339 handleResult: function(aResults) {
4341 while (row = aResults.getNextRow())
4342 aAddon.targetApplications.push(copyRowProperties(row, PROP_TARGETAPP));
4345 handleError: asyncErrorLogger,
4347 handleCompletion: function(aReason) {
4348 readTargetPlatforms();
4353 function readTargetPlatforms() {
4354 delete aAddon.targetPlatforms;
4355 aAddon.targetPlatforms = [];
4356 let stmt = XPIDatabase.getStatement("_getTargetPlatforms");
4358 stmt.params.internal_id = aAddon._internal_id;
4360 handleResult: function(aResults) {
4362 while (row = aResults.getNextRow())
4363 aAddon.targetPlatforms.push(copyRowProperties(row, ["os", "abi"]));
4366 handleError: asyncErrorLogger,
4368 handleCompletion: function(aReason) {
4369 let callbacks = aAddon._pendingCallbacks;
4370 delete aAddon._pendingCallbacks;
4371 callbacks.forEach(function(aCallback) {
4378 readDefaultLocale();
4382 * Synchronously makes a DBAddonInternal from a mozIStorageRow or returns one
4386 * The mozIStorageRow to make the DBAddonInternal from
4387 * @return a DBAddonInternal
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();
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
4397 if ("_pendingCallbacks" in addon)
4398 addon._pendingCallbacks.push(aCallback);
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;
4417 addon._sourceBundle = addon._installLocation.getLocationForID(addon.id);
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.
4425 this.addonCache[internal_id] = Components.utils.getWeakReference(addon);
4426 addon._pendingCallbacks = [aCallback];
4427 this.fetchAddonMetadata(addon);
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
4436 * @return an array of names of install locations
4438 getInstallLocations: function XPIDB_getInstallLocations() {
4439 let stmt = this.getStatement("getInstallLocations");
4441 return [row.location for each (row in resultRows(stmt))];
4445 * Synchronously reads all the add-ons in a particular install location.
4448 * The name of the install location
4449 * @return an array of DBAddonInternals
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))];
4459 * Asynchronously gets an add-on with a particular ID in a particular
4463 * The ID of the add-on to retrieve
4465 * The name of the install location
4467 * A callback to pass the DBAddonInternal to
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) {
4479 // This should never happen but indicates invalid data in the database if
4481 if (aAddons.length > 1)
4482 ERROR("Multiple addons with ID " + aId + " found in location " + aLocation);
4483 aCallback(aAddons[0]);
4488 * Asynchronously gets the add-on with an ID that is visible.
4491 * The ID of the add-on to retrieve
4493 * A callback to pass the DBAddonInternal to
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) {
4504 // This should never happen but indicates invalid data in the database if
4506 if (aAddons.length > 1)
4507 ERROR("Multiple visible addons with ID " + aId + " found");
4508 aCallback(aAddons[0]);
4513 * Asynchronously gets the visible add-ons, optionally restricting by type.
4516 * An array of types to include or null to include all types
4518 * A callback to pass the array of DBAddonInternals to
4520 getVisibleAddons: function XPIDB_getVisibleAddons(aTypes, aCallback) {
4522 if (!aTypes || aTypes.length == 0) {
4523 stmt = this.getStatement("getVisibleAddons");
4526 let sql = "SELECT * FROM addon WHERE visible=1 AND type IN (";
4527 for (let i = 1; i <= aTypes.length; i++) {
4529 if (i < aTypes.length)
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]);
4540 stmt.executeAsync(new AsyncAddonListCallback(aCallback));
4544 * Synchronously gets all add-ons of a particular type.
4547 * The type of add-on to retrieve
4548 * @return an array of DBAddonInternals
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))];;
4558 * Synchronously gets an add-on with a particular internalName.
4560 * @param aInternalName
4561 * The internalName of the add-on to retrieve
4562 * @return a DBAddonInternal
4564 getVisibleAddonForInternalName: function XPIDB_getVisibleAddonForInternalName(aInternalName) {
4565 let stmt = this.getStatement("getVisibleAddoForInternalName");
4568 stmt.params.internalName = aInternalName;
4570 if (stepStatement(stmt))
4571 addon = this.makeAddonFromRow(stmt.row);
4578 * Asynchronously gets all add-ons with pending operations.
4581 * The types of add-ons to retrieve or null to get all types
4583 * A callback to pass the array of DBAddonInternal to
4585 getVisibleAddonsWithPendingOperations:
4586 function XPIDB_getVisibleAddonsWithPendingOperations(aTypes, aCallback) {
4588 if (!aTypes || aTypes.length == 0) {
4589 stmt = this.getStatement("getVisibleAddonsWithPendingOperations");
4592 let sql = "SELECT * FROM addon WHERE visible=1 AND " +
4593 "(pendingUninstall=1 OR MAX(userDisabled,appDisabled)=active) " +
4595 for (let i = 1; i <= aTypes.length; i++) {
4597 if (i < aTypes.length)
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]);
4609 stmt.executeAsync(new AsyncAddonListCallback(aCallback));
4613 * Synchronously gets all add-ons in the database.
4615 * @return an array of DBAddonInternals
4617 getAddons: function XPIDB_getAddons() {
4618 let stmt = this.getStatement("getAddons");
4620 return [this.makeAddonFromRow(row) for each (row in resultRows(stmt))];;
4624 * Synchronously adds an AddonInternal's metadata to the database.
4627 * AddonInternal to add
4628 * @param aDescriptor
4629 * The file descriptor of the add-on
4631 addAddonMetadata: function XPIDB_addAddonMetadata(aAddon, aDescriptor) {
4632 this.beginTransaction();
4634 // Any errors in here should rollback the transaction
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);
4655 if (aAddon.visible) {
4656 let stmt = this.getStatement("clearVisibleAddons");
4657 stmt.params.id = aAddon.id;
4658 executeStatement(stmt);
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;
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);
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);
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);
4704 this.commitTransaction();
4707 this.rollbackTransaction();
4713 * Synchronously updates an add-ons metadata in the database. Currently just
4714 * removes and recreates.
4717 * The DBAddonInternal to be replaced
4719 * The new AddonInternal to add
4720 * @param aDescriptor
4721 * The file descriptor of the add-on
4723 updateAddonMetadata: function XPIDB_updateAddonMetadata(aOldAddon, aNewAddon,
4725 this.beginTransaction();
4727 // Any errors in here should rollback the transaction
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();
4738 this.rollbackTransaction();
4744 * Synchronously updates the target application entries for an add-on.
4747 * The DBAddonInternal being updated
4749 * The array of target applications to update
4751 updateTargetApplications: function XPIDB_updateTargetApplications(aAddon,
4753 this.beginTransaction();
4755 // Any errors in here should rollback the transaction
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);
4765 this.commitTransaction();
4768 this.rollbackTransaction();
4774 * Synchronously removes an add-on from the database.
4777 * The DBAddonInternal being removed
4779 removeAddonMetadata: function XPIDB_removeAddonMetadata(aAddon) {
4780 let stmt = this.getStatement("removeAddonMetadata");
4781 stmt.params.internal_id = aAddon._internal_id;
4782 executeStatement(stmt);
4786 * Synchronously marks a DBAddonInternal as visible marking all other
4787 * instances with the same ID as not visible.
4790 * The DBAddonInternal to make visible
4792 * A callback to pass the DBAddonInternal to
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;
4807 * Synchronously sets properties for an add-on.
4810 * The DBAddonInternal being updated
4811 * @param aProperties
4812 * A dictionary of properties to set
4814 setAddonProperties: function XPIDB_setAddonProperties(aAddon, aProperties) {
4815 function convertBoolean(value) {
4816 return value ? 1 : 0;
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];
4829 stmt.params[aProp] = convertBoolean(aAddon[aProp]);
4833 if ("applyBackgroundUpdates" in aProperties) {
4834 stmt.params.applyBackgroundUpdates = aProperties.applyBackgroundUpdates;
4835 aAddon.applyBackgroundUpdates = aProperties.applyBackgroundUpdates;
4838 stmt.params.applyBackgroundUpdates = aAddon.applyBackgroundUpdates;
4841 executeStatement(stmt);
4845 * Synchronously pdates an add-on's active flag in the database.
4848 * The DBAddonInternal to update
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);
4860 * Synchronously calculates and updates all the active flags in the database.
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 = [];
4875 * Writes out the XPI add-ons list for the platform to read.
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],
4883 let enabledAddons = [];
4884 let text = "[ExtensionDirs]\r\n";
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);
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");
4901 stmt = this.getStatement("getActiveTheme");
4902 stmt.params.internalName = XPIProvider.selectedSkin;
4905 for (let row in resultRows(stmt)) {
4906 text += "Extension" + (count++) + "=" + row.descriptor + "\r\n";
4907 enabledAddons.push(row.id + ":" + row.version);
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(","));
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
4933 * The callback to pass the AddonInstall to
4934 * @param aInstallLocation
4935 * The install location the add-on will be installed into
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
4940 * An optional hash for the add-on
4942 * An optional name for the add-on
4944 * An optional type for the add-on
4946 * An optional icon for the add-on
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
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
4958 function AddonInstall(aCallback, aInstallLocation, aUrl, aHash, aName, aType,
4959 aIconURL, aVersion, aReleaseNotesURI, aExistingAddon,
4961 this.wrapper = new AddonInstallWrapper(this);
4962 this.installLocation = aInstallLocation;
4963 this.sourceURI = aUrl;
4964 this.releaseNotesURI = aReleaseNotesURI;
4966 let hashSplit = aHash.toLowerCase().split(":");
4967 this.originalHash = {
4968 algorithm: hashSplit[0],
4972 this.hash = this.originalHash;
4973 this.loadGroup = aLoadGroup;
4974 this.listeners = [];
4975 this.existingAddon = aExistingAddon;
4978 this.window = aLoadGroup.notificationCallbacks
4979 .getInterface(Ci.nsIDOMWindow);
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;
4994 this.state = AddonManager.STATE_DOWNLOADED;
4995 this.progress = this.file.fileSize;
4996 this.maxProgress = this.file.fileSize;
4999 let crypto = Cc["@mozilla.org/security/hash;1"].
5000 createInstance(Ci.nsICryptoHash);
5002 crypto.initWithString(this.hash.algorithm);
5005 WARN("Unknown hash algorithm " + this.hash.algorithm);
5006 this.state = AddonManager.STATE_DOWNLOAD_FAILED;
5007 this.error = AddonManager.ERROR_INCORRECT_HASH;
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;
5029 this.loadManifest(function() {
5030 XPIDatabase.getVisibleAddonForID(self.addon.id, function(aAddon) {
5031 self.existingAddon = 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",
5050 }, AddonManager.UPDATE_WHEN_ADDON_INSTALLED);
5053 XPIProvider.installs.push(self);
5054 AddonManagerPrivate.callInstallListeners("onNewInstall",
5064 WARN("Invalid XPI", e);
5065 this.state = AddonManager.STATE_DOWNLOAD_FAILED;
5066 this.error = AddonManager.ERROR_CORRUPT_FILE;
5072 this.state = AddonManager.STATE_AVAILABLE;
5075 this.version = aVersion;
5076 this.iconURL = aIconURL;
5078 this.maxProgress = -1;
5080 XPIProvider.installs.push(this);
5081 AddonManagerPrivate.callInstallListeners("onNewInstall", this.listeners,
5088 AddonInstall.prototype = {
5089 installLocation: null,
5096 badCertHandler: null,
5103 releaseNotesURI: null,
5106 ownsTempFile: false,
5110 linkedInstalls: null,
5111 existingAddon: null,
5120 * Starts installation of this add-on from whatever state it is currently at
5123 * @throws if installation cannot proceed from the current state
5125 install: function AI_install() {
5126 switch (this.state) {
5127 case AddonManager.STATE_AVAILABLE:
5128 this.startDownload();
5130 case AddonManager.STATE_DOWNLOADED:
5131 this.startInstall();
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;
5140 this.maxProgress = -1;
5141 this.hash = this.originalHash;
5142 XPIProvider.installs.push(this);
5143 this.startDownload();
5145 case AddonManager.STATE_DOWNLOADING:
5146 case AddonManager.STATE_CHECKING:
5147 case AddonManager.STATE_INSTALLING:
5148 // Installation is already running
5151 throw new Error("Cannot start installing from this state");
5156 * Cancels installation of this add-on.
5158 * @throws if installation cannot be cancelled from the current state
5160 cancel: function AI_cancel() {
5161 switch (this.state) {
5162 case AddonManager.STATE_DOWNLOADING:
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();
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;
5190 AddonManagerPrivate.callAddonListeners("onOperationCancelled", createWrapper(this.addon));
5192 AddonManagerPrivate.callInstallListeners("onInstallCancelled",
5193 this.listeners, this.wrapper);
5196 throw new Error("Cannot cancel install of " + this.sourceURI.spec +
5197 " from this state (" + this.state + ")");
5202 * Adds an InstallListener for this instance if the listener is not already
5206 * The InstallListener to add
5208 addListener: function AI_addListener(aListener) {
5209 if (!this.listeners.some(function(i) { return i == aListener; }))
5210 this.listeners.push(aListener);
5214 * Removes an InstallListener for this instance if it is registered.
5217 * The InstallListener to remove
5219 removeListener: function AI_removeListener(aListener) {
5220 this.listeners = this.listeners.filter(function(i) {
5221 return i != aListener;
5226 * Removes the temporary file owned by this AddonInstall if there is one.
5228 removeTemporaryFile: function AI_removeTemporaryFile() {
5229 // Only proceed if this AddonInstall owns its XPI file
5230 if (!this.ownsTempFile)
5234 this.file.remove(true);
5235 this.ownsTempFile = false;
5238 WARN("Failed to remove temporary file " + this.file.path, e);
5243 * Updates the sourceURI and releaseNotesURI values on the Addon being
5244 * installed by this AddonInstall instance.
5246 updateAddonURIs: function AI_updateAddonURIs() {
5247 this.addon.sourceURI = this.sourceURI.spec;
5248 if (this.releaseNotesURI)
5249 this.addon.releaseNotesURI = this.releaseNotesURI.spec;
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.
5260 * An open nsIZipReader for the multi-package XPI's files. This will
5261 * be closed before this method returns.
5263 * A function to call when all of the add-on manifests have been
5266 loadMultipackageManifests: function AI_loadMultipackageManifests(aZipReader,
5269 let entries = aZipReader.findEntries("(*.[Xx][Pp][Ii]|*.[Jj][Aa][Rr])");
5270 while (entries.hasMore()) {
5271 let entryName = entries.getNext();
5272 var target = getTemporaryFile();
5274 aZipReader.extract(entryName, target);
5278 WARN("Failed to extract " + entryName + " from multi-package " +
5280 target.remove(false);
5286 if (files.length == 0) {
5287 throw new Error("Multi-package XPI does not contain any packages " +
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;
5300 addon = loadManifestFromZipFile(this.file);
5304 WARN(this.file.leafName + " cannot be installed from multi-package " +
5310 // No valid add-on was found
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;
5328 // this.iconURL = newIcon;
5330 // Create new AddonInstall instances for every remaining file
5331 if (files.length > 0) {
5332 this.linkedInstalls = [];
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
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();
5354 if (count == files.length)
5365 * Called after the add-on is a local file and the signature and install
5366 * manifest can be read.
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
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) {
5378 aAddon._repositoryAddon = aRepoAddon;
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;
5393 let zipreader = Cc["@mozilla.org/libjar/zip-reader;1"].
5394 createInstance(Ci.nsIZipReader);
5396 zipreader.open(this.file);
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;
5413 this.certName = principal.prettyName;
5417 throw new Error("XPI is incorrectly signed");
5422 this.addon = loadManifestFromZipReader(zipreader);
5429 if (this.addon.type == "multipackage") {
5431 this.loadMultipackageManifests(zipreader, function() {
5432 addRepositoryData(self.addon);
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;
5450 // this.iconURL = newIcon;
5452 addRepositoryData(this.addon);
5455 observe: function AI_observe(aSubject, aTopic, aData) {
5456 // Network is going offline
5461 * Starts downloading the add-on's XPI file.
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)
5474 // If a listener changed our state then do not proceed with the download
5475 if (this.state != AddonManager.STATE_DOWNLOADING)
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);
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);
5496 let listener = Cc["@mozilla.org/network/stream-listener-tee;1"].
5497 createInstance(Ci.nsIStreamListenerTee);
5498 listener.init(this, this.stream);
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);
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);
5523 * Update the crypto hasher with the new data and call the progress listeners.
5525 * @see nsIStreamListener
5527 onDataAvailable: function AI_onDataAvailable(aRequest, aContext, aInputstream,
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)
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.
5541 * @see nsIChannelEventSink
5543 asyncOnChannelRedirect: function(aOldChannel, aNewChannel, aFlags, aCallback) {
5544 if (!this.hash && aOldChannel.originalURI.schemeIs("https") &&
5545 aOldChannel instanceof Ci.nsIHttpChannel) {
5547 let hashStr = aOldChannel.getResponseHeader("X-Target-Digest");
5548 let hashSplit = hashStr.toLowerCase().split(":");
5550 algorithm: hashSplit[0],
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)
5561 this.badCertHandler.asyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, aCallback);
5563 aCallback.onRedirectVerifyCallback(Cr.NS_OK);
5565 this.channel = aNewChannel;
5569 * This is the first chance to get at real headers on the channel.
5571 * @see nsIStreamListener
5573 onStartRequest: function AI_onStartRequest(aRequest, aContext) {
5574 this.crypto = Cc["@mozilla.org/security/hash;1"].
5575 createInstance(Ci.nsICryptoHash);
5578 this.crypto.initWithString(this.hash.algorithm);
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);
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");
5598 if (aRequest instanceof Ci.nsIChannel) {
5600 this.maxProgress = aRequest.contentLength;
5604 LOG("Download started for " + this.sourceURI.spec + " to file " +
5610 * The download is complete.
5612 * @see nsIStreamListener
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();
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)) {
5633 !Prefs.getBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, true));
5636 this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, e);
5641 // convert the binary hash data to a hex string.
5642 let calculatedHash = getHashStringForCrypto(this.crypto);
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 + ")");
5652 this.loadManifest(function() {
5653 if (self.addon.isCompatible) {
5654 self.downloadCompleted();
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();
5663 }, AddonManager.UPDATE_WHEN_ADDON_INSTALLED);
5668 this.downloadFailed(AddonManager.ERROR_CORRUPT_FILE, e);
5672 if (aRequest instanceof Ci.nsIHttpChannel)
5673 this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE,
5674 aRequest.responseStatus + " " +
5675 aRequest.responseStatusText);
5677 this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, aStatus);
5681 this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, aStatus);
5686 * Notify listeners that the download failed.
5689 * Something to log about the failure
5691 * The error code to pass to the listeners
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,
5701 // If the listener hasn't restarted the download then remove any temporary
5703 if (this.state == AddonManager.STATE_DOWNLOAD_FAILED)
5704 this.removeTemporaryFile();
5708 * Notify listeners that the download completed.
5710 downloadCompleted: function() {
5712 XPIDatabase.getVisibleAddonForID(this.addon.id, function(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;
5725 self.addon.installDate = self.addon.updateDate;
5728 if (AddonManagerPrivate.callInstallListeners("onDownloadEnded",
5731 // If a listener changed our state then do not proceed with the install
5732 if (self.state != AddonManager.STATE_DOWNLOADED)
5737 if (self.linkedInstalls) {
5738 self.linkedInstalls.forEach(function(aInstall) {
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).
5750 * Installs the add-on into the install location.
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)
5763 // Find and cancel any pending installs for the same add-on in the same
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)
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),
5780 let stagedAddon = this.installLocation.getStagingDir();
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);
5794 LOG("Addon " + this.addon.id + " will be installed as " +
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");
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);
5818 stream.init(stagedJSON, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE |
5819 FileUtils.MODE_TRUNCATE, FileUtils.PERMS_FILE,
5821 converter.init(stream, "UTF-8", 0, 0x0000);
5822 converter.writeString(JSON.stringify(this.addon));
5829 LOG("Install of " + this.sourceURI.spec + " completed.");
5830 this.state = AddonManager.STATE_INSTALLED;
5832 delete this.existingAddon.pendingUpgrade;
5833 this.existingAddon.pendingUpgrade = this.addon;
5835 AddonManagerPrivate.callInstallListeners("onInstallEnded",
5836 this.listeners, this.wrapper,
5837 createWrapper(this.addon));
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.
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;
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);
5863 XPIProvider.callBootstrapMethod(this.existingAddon.id,
5864 this.existingAddon.version,
5865 file, "uninstall", reason);
5866 XPIProvider.unloadBootstrapScope(this.existingAddon.id);
5869 if (!isUpgrade && this.existingAddon.active) {
5870 this.existingAddon.active = false;
5871 XPIDatabase.updateAddonActive(this.existingAddon);
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,
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;
5887 XPIDatabase.updateAddonMetadata(this.existingAddon, this.addon,
5888 file.persistentDescriptor);
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);
5897 // Retrieve the new DBAddonInternal for the add-on we just added
5899 XPIDatabase.getAddonInLocation(this.addon.id, this.installLocation.name,
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);
5910 XPIProvider.unloadBootstrapScope(self.addon.id);
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));
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",
5936 this.removeTemporaryFile();
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);
5946 else if (iid.equals(Ci.nsIChannelEventSink)) {
5950 return this.badCertHandler.getInterface(iid);
5955 * Creates a new AddonInstall to install an add-on from a local file. Installs
5956 * always go into the profile install location.
5959 * The callback to pass the new AddonInstall to
5961 * The file to install
5963 AddonInstall.createInstall = function(aCallback, aFile) {
5964 let location = XPIProvider.installLocationsByName[KEY_APP_PROFILE];
5965 let url = Services.io.newFileURI(aFile);
5968 new AddonInstall(aCallback, location, url);
5971 ERROR("Error creating install", e);
5977 * Creates a new AddonInstall to download and install a URL.
5980 * The callback to pass the new AddonInstall to
5982 * The URI to download
5984 * A hash for the add-on
5986 * A name for the add-on
5988 * An icon URL for the add-on
5990 * A version for the add-on
5992 * An nsILoadGroup to associate the download with
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.
6006 * The callback to pass the new AddonInstall to
6008 * The add-on being updated
6010 * The metadata about the new version from the update manifest
6012 AddonInstall.createUpdate = function(aCallback, aAddon, aUpdate) {
6013 let url = NetUtil.newURI(aUpdate.updateURL);
6014 let releaseNotesURI = null;
6016 if (aUpdate.updateInfoURL)
6017 releaseNotesURI = NetUtil.newURI(escapeAddonURI(aAddon, aUpdate.updateInfoURL));
6020 // If the releaseNotesURI cannot be parsed then just ignore it.
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
6031 * The AddonInstall to create a wrapper for
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]);
6039 this.__defineGetter__("existingAddon", function() {
6040 return createWrapper(aInstall.existingAddon);
6042 this.__defineGetter__("addon", function() createWrapper(aInstall.addon));
6043 this.__defineGetter__("sourceURI", function() aInstall.sourceURI);
6045 this.__defineGetter__("linkedInstalls", function() {
6046 if (!aInstall.linkedInstalls)
6048 return [i.wrapper for each (i in aInstall.linkedInstalls)];
6051 this.install = function() {
6055 this.cancel = function() {
6059 this.addListener = function(listener) {
6060 aInstall.addListener(listener);
6063 this.removeListener = function(listener) {
6064 aInstall.removeListener(listener);
6068 AddonInstallWrapper.prototype = {};
6071 * Creates a new update checker.
6074 * The add-on to check for updates
6076 * An UpdateListener to notify of updates
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
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,
6112 UpdateChecker.prototype = {
6116 platformVersion: null,
6117 syncCompatibility: null,
6120 * Calls a method on the listener passing any number of arguments and
6121 * consuming any exceptions.
6124 * The method to call on the listener
6126 callListener: function(aMethod) {
6127 if (!(aMethod in this.listener))
6130 let args = Array.slice(arguments, 1);
6132 this.listener[aMethod].apply(this.listener, args);
6135 LOG("Exception calling UpdateListener method " + aMethod + ": " + e);
6140 * Called when AddonUpdateChecker completes the update check
6143 * The list of update details for the add-on
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
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);
6169 this.callListener("onCompatibilityUpdateAvailable", createWrapper(this.addon));
6171 this.callListener("onNoCompatibilityUpdateAvailable", createWrapper(this.addon));
6173 function sendUpdateAvailableMessages(aSelf, aInstall) {
6175 aSelf.callListener("onUpdateAvailable", createWrapper(aSelf.addon),
6179 aSelf.callListener("onNoUpdateAvailable", createWrapper(aSelf.addon));
6181 aSelf.callListener("onUpdateFinished", createWrapper(aSelf.addon),
6182 AddonManager.UPDATE_STATUS_NO_ERROR);
6185 let update = AUC.getNewestCompatibleUpdate(aUpdates,
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)
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]);
6202 sendUpdateAvailableMessages(this, null);
6207 AddonInstall.createUpdate(function(aInstall) {
6208 sendUpdateAvailableMessages(self, aInstall);
6209 }, this.addon, update);
6212 sendUpdateAvailableMessages(this, null);
6217 * Called when AddonUpdateChecker fails the update check
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);
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
6234 function AddonInternal() {
6237 AddonInternal.prototype = {
6238 _selectedLocale: null,
6241 userDisabled: false,
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;
6254 get providesUpdatesSecurely() {
6255 return !!(this.updateKey || !this.updateURL ||
6256 this.updateURL.substring(0, 6) == "https:");
6259 get isCompatible() {
6260 return this.isCompatibleWith();
6263 get isPlatformCompatible() {
6264 if (this.targetPlatforms.length == 0)
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.
6276 abi = Services.appinfo.XPCOMABI;
6280 for (let i = 0; i < this.targetPlatforms.length; i++) {
6281 let platform = this.targetPlatforms[i];
6282 if (platform.os == Services.appinfo.OS) {
6285 if (platform.abi === abi)
6294 return matchedOS && !needsABI;
6297 isCompatibleWith: function(aAppVersion, aPlatformVersion) {
6298 let app = this.matchingTargetApplication;
6303 aAppVersion = Services.appinfo.version;
6304 if (!aPlatformVersion)
6305 aPlatformVersion = Services.appinfo.platformVersion;
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)
6317 get matchingTargetApplication() {
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];
6328 get blocklistState() {
6329 let bs = Cc["@mozilla.org/extensions/blocklist;1"].
6330 getService(Ci.nsIBlocklistService);
6331 return bs.getAddonBlocklistState(this.id, this.version);
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;
6344 this.appDisabled = !isUsableAddon(this);
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
6354 * The key that this object is being serialized as in the JSON.
6355 * Unused here since this is always the main object serialized
6357 * @return an object containing copies of the properties of this object
6358 * ignoring private properties, functions, getters and setters
6360 toJSON: function(aKey) {
6362 for (let prop in this) {
6363 // Ignore private properties
6364 if (prop.substring(0, 1) == "_")
6368 if (this.__lookupGetter__(prop))
6372 if (this.__lookupSetter__(prop))
6376 if (typeof this[prop] == "function")
6379 obj[prop] = this[prop];
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.
6392 function DBAddonInternal() {
6393 this.__defineGetter__("targetApplications", function() {
6394 delete this.targetApplications;
6395 return this.targetApplications = XPIDatabase._getTargetApplications(this);
6398 this.__defineGetter__("targetPlatforms", function() {
6399 delete this.targetPlatforms;
6400 return this.targetPlatforms = XPIDatabase._getTargetPlatforms(this);
6403 this.__defineGetter__("locales", function() {
6404 delete this.locales;
6405 return this.locales = XPIDatabase._getLocales(this);
6408 this.__defineGetter__("defaultLocale", function() {
6409 delete this.defaultLocale;
6410 return this.defaultLocale = XPIDatabase._getDefaultLocale(this);
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;
6427 DBAddonInternal.prototype = {
6428 applyCompatibilityUpdate: function(aUpdate, aSyncCompatibility) {
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);
6441 XPIDatabase.updateTargetApplications(this, changes);
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);
6449 XPIProvider.updateAddonDisabledState(this);
6453 DBAddonInternal.prototype.__proto__ = AddonInternal.prototype;
6456 * Creates an AddonWrapper for an AddonInternal.
6459 * The AddonInternal to wrap
6460 * @return an AddonWrapper or null if addon was null
6462 function createWrapper(aAddon) {
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
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];
6484 return [objValue, false];
6487 ["id", "version", "type", "isCompatible", "isPlatformCompatible",
6488 "providesUpdatesSecurely", "blocklistState", "appDisabled",
6489 "userDisabled", "skinnable", "size"].forEach(function(aProp) {
6490 this.__defineGetter__(aProp, function() aAddon[aProp]);
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];
6505 ["optionsURL", "aboutURL"].forEach(function(aProp) {
6506 this.__defineGetter__(aProp, function() {
6507 return this.isActive ? aAddon[aProp] : null;
6511 ["installDate", "updateDate"].forEach(function(aProp) {
6512 this.__defineGetter__(aProp, function() new Date(aAddon[aProp]));
6515 ["sourceURI", "releaseNotesURI"].forEach(function(aProp) {
6516 this.__defineGetter__(aProp, function() {
6517 let target = chooseValue(aAddon, aProp)[0];
6520 return NetUtil.newURI(target);
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"];
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;
6551 if (aAddon.active) {
6553 let pref = PREF_EM_EXTENSION_FORMAT + aAddon.id + "." + aProp;
6554 let value = Services.prefs.getComplexValue(pref,
6555 Ci.nsIPrefLocalizedString);
6557 result = value.data;
6564 [result, ] = chooseValue(aAddon.selectedLocale, aProp);
6566 if (aProp == "creator")
6567 return result ? new AddonManagerPrivate.AddonAuthor(result) : null;
6573 PROP_LOCALE_MULTI.forEach(function(aProp) {
6574 this.__defineGetter__(aProp, function() {
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) {
6585 list.forEach(function(aPref) {
6586 let value = Services.prefs.getComplexValue(aPref,
6587 Ci.nsIPrefLocalizedString);
6589 results.push(value.data);
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);
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;
6615 if (aAddon.type == "theme" && this.hasResource("preview.png")) {
6616 let url = this.getResourceURI("preview.png").spec;
6617 return [new AddonManagerPrivate.AddonScreenshot(url)];
6623 this.__defineGetter__("applyBackgroundUpdates", function() {
6624 return aAddon.applyBackgroundUpdates;
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;
6634 if (val == aAddon.applyBackgroundUpdates)
6637 XPIDatabase.setAddonProperties(aAddon, {
6638 applyBackgroundUpdates: val
6640 AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, ["applyBackgroundUpdates"]);
6645 this.__defineGetter__("install", function() {
6646 if (!("_install" in aAddon) || !aAddon._install)
6648 return aAddon._install.wrapper;
6651 this.__defineGetter__("pendingUpgrade", function() {
6652 return createWrapper(aAddon.pendingUpgrade);
6655 this.__defineGetter__("scope", function() {
6656 if (aAddon._installLocation)
6657 return aAddon._installLocation.scope;
6659 return AddonManager.SCOPE_PROFILE;
6662 this.__defineGetter__("pendingOperations", function() {
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;
6672 else if (aAddon.pendingUninstall) {
6673 pending |= AddonManager.PENDING_UNINSTALL;
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;
6687 this.__defineGetter__("operationsRequiringRestart", function() {
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;
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))
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;
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;
6727 this.__defineGetter__("isActive", function() {
6728 if (Services.appinfo.inSafeMode)
6730 return aAddon.active;
6733 this.__defineSetter__("userDisabled", function(val) {
6734 if (val == aAddon.userDisabled)
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();
6744 XPIProvider.updateAddonDisabledState(aAddon, val);
6748 aAddon.userDisabled = val;
6754 this.isCompatibleWith = function(aAppVersion, aPlatformVersion) {
6755 return aAddon.isCompatibleWith(aAppVersion, aPlatformVersion);
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);
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);
6774 this.findUpdates = function(aListener, aReason, aAppVersion, aPlatformVersion) {
6775 new UpdateChecker(aAddon, aListener, aReason, aAppVersion, aPlatformVersion);
6778 this.hasResource = function(aPath) {
6779 let bundle = aAddon._sourceBundle.clone();
6781 if (bundle.isDirectory()) {
6783 aPath.split("/").forEach(function(aPart) {
6784 bundle.append(aPart);
6787 return bundle.exists();
6790 let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].
6791 createInstance(Ci.nsIZipReader);
6792 zipReader.open(bundle);
6793 let result = zipReader.hasEntry(aPath);
6798 this.getResourceURI = function(aPath) {
6799 let bundle = aAddon._sourceBundle.clone();
6801 if (bundle.isDirectory()) {
6803 aPath.split("/").forEach(function(aPart) {
6804 bundle.append(aPart);
6807 return Services.io.newFileURI(bundle);
6811 return Services.io.newFileURI(bundle);
6812 return buildJarURI(bundle, aPath);
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
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.
6833 * The string identifier for the install location
6835 * The nsIFile directory for the install location
6837 * The scope of add-ons installed in this location
6839 * true if add-ons cannot be installed, uninstalled or upgraded in the
6842 function DirectoryInstallLocation(aName, aDirectory, aScope, aLocked) {
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())
6853 if (!aDirectory.isDirectory())
6854 throw new Error("Location must be a directory.");
6859 DirectoryInstallLocation.prototype = {
6862 _IDToFileMap : null, // mapping from add-on ID to nsIFile
6863 _FileToIDMap : null, // mapping from add-on path to add-on ID
6866 * Reads a directory linked to in a file.
6869 * The file containing the directory path
6870 * @return a nsILocalFile object representing the linked directory.
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)
6881 let linkedDirectory = Cc["@mozilla.org/file/local;1"].
6882 createInstance(Ci.nsILocalFile);
6885 linkedDirectory.initWithPath(line.value);
6888 linkedDirectory.setRelativeDescriptor(file.parent, line.value);
6891 if (!linkedDirectory.exists()) {
6892 WARN("File pointer " + aFile.path + " points to " + linkedDirectory.path +
6893 " which does not exist");
6897 if (!linkedDirectory.isDirectory()) {
6898 WARN("File pointer " + aFile.path + " points to " + linkedDirectory.path +
6899 " which is not a directory");
6903 return linkedDirectory;
6906 WARN("File pointer " + aFile.path + " does not contain a path");
6911 * Finds all the add-ons installed in this location.
6913 _readAddons: function DirInstallLocation__readAddons() {
6914 let entries = this._directory.directoryEntries
6915 .QueryInterface(Ci.nsIDirectoryEnumerator);
6917 while (entry = entries.nextFile) {
6918 // Should never happen really
6919 if (!(entry instanceof Ci.nsILocalFile))
6922 let id = entry.leafName;
6924 if (id == DIR_STAGE || id == DIR_XPI_STAGE || id == DIR_TRASH)
6927 let directLoad = false;
6928 if (entry.isFile() &&
6929 id.substring(id.length - 4).toLowerCase() == ".xpi") {
6931 id = id.substring(0, id.length - 4);
6934 if (!gIDTest.test(id)) {
6935 LOG("Ignoring file entry whose name is not a valid add-on ID: " +
6940 if (entry.isFile() && !directLoad) {
6941 newEntry = this._readDirectoryFromFile(entry);
6946 this._linkedAddons.push(id);
6949 this._IDToFileMap[id] = entry;
6950 this._FileToIDMap[entry.path] = id;
6956 * Gets the name of this install location.
6963 * Gets the scope of this install location.
6970 * Gets an array of nsIFiles for add-ons installed in this location.
6972 get addonLocations() {
6974 for (let id in this._IDToFileMap) {
6975 locations.push(this._IDToFileMap[id].clone()
6976 .QueryInterface(Ci.nsILocalFile));
6982 * Gets the staging directory to put add-ons that are pending install and
6985 * @return an nsIFile
6987 getStagingDir: function DirInstallLocation_getStagingDir() {
6988 let dir = this._directory.clone();
6989 dir.append(DIR_STAGE);
6994 * Gets the directory used by old versions for staging XPI and JAR files ready
6997 * @return an nsIFile
6999 getXPIStagingDir: function DirInstallLocation_getXPIStagingDir() {
7000 let dir = this._directory.clone();
7001 dir.append(DIR_XPI_STAGE);
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.
7011 * @return an nsIFile
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);
7023 * Installs an add-on into the install location.
7026 * The ID of the add-on to install
7028 * The source nsIFile to install from
7029 * @param aExistingAddonID
7030 * The ID of an existing add-on to uninstall at the same time
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
7036 installAddon: function DirInstallLocation_installAddon(aId, aSource,
7039 let trashDir = this.getTrashDir();
7041 let transaction = new SafeInstallOperation();
7044 function moveOldAddon(aId) {
7045 let file = self._directory.clone().QueryInterface(Ci.nsILocalFile);
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);
7059 // If any of these operations fails the finally block will clean up the
7060 // temporary directory
7063 if (aExistingAddonID && aExistingAddonID != aId)
7064 moveOldAddon(aExistingAddonID);
7067 transaction.copy(aSource, this._directory);
7070 if (aSource.isFile())
7071 Services.obs.notifyObservers(aSource, "flush-cache-entry", null);
7073 transaction.move(aSource, this._directory);
7077 // It isn't ideal if this cleanup fails but it isn't worth rolling back
7078 // the install because of it.
7080 recursiveRemove(trashDir);
7083 WARN("Failed to remove trash directory when installing " + aId, e);
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];
7103 * Uninstalls an add-on from this location.
7106 * The ID of the add-on to uninstall
7107 * @throws if the ID does not match any of the add-ons installed
7109 uninstallAddon: function DirInstallLocation_uninstallAddon(aId) {
7110 let file = this._IDToFileMap[aId];
7112 WARN("Attempted to remove " + aId + " from " +
7113 this._name + " but it was already gone");
7117 file = this._directory.clone();
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];
7131 let trashDir = this.getTrashDir();
7133 if (file.leafName != aId)
7134 Services.obs.notifyObservers(file, "flush-cache-entry", null);
7136 let transaction = new SafeInstallOperation();
7139 transaction.move(file, trashDir);
7142 // It isn't ideal if this cleanup fails, but it is probably better than
7143 // rolling back the uninstall at this point
7145 recursiveRemove(trashDir);
7148 WARN("Failed to remove trash directory when uninstalling " + aId, e);
7152 delete this._FileToIDMap[file.path];
7153 delete this._IDToFileMap[aId];
7157 * Gets the ID of the add-on installed in the given nsIFile.
7160 * The nsIFile to look in
7162 * @throws if the file does not represent an installed add-on
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);
7171 * Gets the directory that the add-on with the given ID is installed in.
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
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);
7185 * Returns true if the given addon was installed in this location by a text
7186 * file pointing to its real path.
7189 * The ID of the addon
7191 isLinkedAddon: function(aId) {
7192 return this._linkedAddons.indexOf(aId) != -1;
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
7203 * The string identifier of this Install Location.
7205 * The root key (one of the ROOT_KEY_ values from nsIWindowsRegKey).
7207 * The scope of add-ons installed in this location
7209 function WinRegInstallLocation(aName, aRootKey, aScope) {
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.
7224 key.open(this._rootKey, path, Ci.nsIWindowsRegKey.ACCESS_READ);
7230 this._readAddons(key);
7234 WinRegInstallLocation.prototype = {
7238 _IDToFileMap : null, // mapping from ID to nsIFile
7239 _FileToIDMap : null, // mapping from path to ID
7242 * Retrieves the path of this Application's data key in the registry.
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";
7254 // XULRunner-based apps may intentionally not specify a vendor
7255 if (appVendor != "")
7258 return "SOFTWARE\\" + appVendor + appName;
7262 * Read the registry and build a mapping between ID and path for each
7266 * The key that contains the ID to path mapping
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);
7282 this._IDToFileMap[id] = file;
7283 this._FileToIDMap[file.path] = id;
7288 * Gets the name of this install location.
7295 * Gets the scope of this install location.
7302 * Gets an array of nsIFiles for add-ons installed in this location.
7304 get addonLocations() {
7306 for (let id in this._IDToFileMap) {
7307 locations.push(this._IDToFileMap[id].clone()
7308 .QueryInterface(Ci.nsILocalFile));
7314 * Gets the ID of the add-on installed in the given nsIFile.
7317 * The nsIFile to look in
7319 * @throws if the file does not represent an installed add-on
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");
7328 * Gets the nsIFile that the add-on with the given ID is installed in.
7331 * The ID of the add-on
7332 * @return the nsIFile
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");
7341 * @see DirectoryInstallLocation
7343 isLinkedAddon: function(aId) {
7349 AddonManagerPrivate.registerProvider(XPIProvider);