michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cr = Components.results; michael@0: const Cu = Components.utils; michael@0: michael@0: this.EXPORTED_SYMBOLS = ["XPIProvider"]; michael@0: michael@0: Components.utils.import("resource://gre/modules/Services.jsm"); michael@0: Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Components.utils.import("resource://gre/modules/AddonManager.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository", michael@0: "resource://gre/modules/addons/AddonRepository.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "ChromeManifestParser", michael@0: "resource://gre/modules/ChromeManifestParser.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager", michael@0: "resource://gre/modules/LightweightThemeManager.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", michael@0: "resource://gre/modules/FileUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "ZipUtils", michael@0: "resource://gre/modules/ZipUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", michael@0: "resource://gre/modules/NetUtil.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "PermissionsUtils", michael@0: "resource://gre/modules/PermissionsUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Promise", michael@0: "resource://gre/modules/Promise.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Task", michael@0: "resource://gre/modules/Task.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "OS", michael@0: "resource://gre/modules/osfile.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "BrowserToolboxProcess", michael@0: "resource:///modules/devtools/ToolboxProcess.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, michael@0: "ChromeRegistry", michael@0: "@mozilla.org/chrome/chrome-registry;1", michael@0: "nsIChromeRegistry"); michael@0: XPCOMUtils.defineLazyServiceGetter(this, michael@0: "ResProtocolHandler", michael@0: "@mozilla.org/network/protocol;1?name=resource", michael@0: "nsIResProtocolHandler"); michael@0: michael@0: const nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile", michael@0: "initWithPath"); michael@0: michael@0: const PREF_DB_SCHEMA = "extensions.databaseSchema"; michael@0: const PREF_INSTALL_CACHE = "extensions.installCache"; michael@0: const PREF_BOOTSTRAP_ADDONS = "extensions.bootstrappedAddons"; michael@0: const PREF_PENDING_OPERATIONS = "extensions.pendingOperations"; michael@0: const PREF_MATCH_OS_LOCALE = "intl.locale.matchOS"; michael@0: const PREF_SELECTED_LOCALE = "general.useragent.locale"; michael@0: const PREF_EM_DSS_ENABLED = "extensions.dss.enabled"; michael@0: const PREF_DSS_SWITCHPENDING = "extensions.dss.switchPending"; michael@0: const PREF_DSS_SKIN_TO_SELECT = "extensions.lastSelectedSkin"; michael@0: const PREF_GENERAL_SKINS_SELECTEDSKIN = "general.skins.selectedSkin"; michael@0: const PREF_EM_UPDATE_URL = "extensions.update.url"; michael@0: const PREF_EM_UPDATE_BACKGROUND_URL = "extensions.update.background.url"; michael@0: const PREF_EM_ENABLED_ADDONS = "extensions.enabledAddons"; michael@0: const PREF_EM_EXTENSION_FORMAT = "extensions."; michael@0: const PREF_EM_ENABLED_SCOPES = "extensions.enabledScopes"; michael@0: const PREF_EM_AUTO_DISABLED_SCOPES = "extensions.autoDisableScopes"; michael@0: const PREF_EM_SHOW_MISMATCH_UI = "extensions.showMismatchUI"; michael@0: const PREF_XPI_ENABLED = "xpinstall.enabled"; michael@0: const PREF_XPI_WHITELIST_REQUIRED = "xpinstall.whitelist.required"; michael@0: const PREF_XPI_DIRECT_WHITELISTED = "xpinstall.whitelist.directRequest"; michael@0: const PREF_XPI_FILE_WHITELISTED = "xpinstall.whitelist.fileRequest"; michael@0: const PREF_XPI_PERMISSIONS_BRANCH = "xpinstall."; michael@0: const PREF_XPI_UNPACK = "extensions.alwaysUnpack"; michael@0: const PREF_INSTALL_REQUIREBUILTINCERTS = "extensions.install.requireBuiltInCerts"; michael@0: const PREF_INSTALL_DISTRO_ADDONS = "extensions.installDistroAddons"; michael@0: const PREF_BRANCH_INSTALLED_ADDON = "extensions.installedDistroAddon."; michael@0: const PREF_SHOWN_SELECTION_UI = "extensions.shownSelectionUI"; michael@0: michael@0: const PREF_EM_MIN_COMPAT_APP_VERSION = "extensions.minCompatibleAppVersion"; michael@0: const PREF_EM_MIN_COMPAT_PLATFORM_VERSION = "extensions.minCompatiblePlatformVersion"; michael@0: michael@0: const PREF_CHECKCOMAT_THEMEOVERRIDE = "extensions.checkCompatibility.temporaryThemeOverride_minAppVersion"; michael@0: michael@0: const URI_EXTENSION_SELECT_DIALOG = "chrome://mozapps/content/extensions/selectAddons.xul"; michael@0: const URI_EXTENSION_UPDATE_DIALOG = "chrome://mozapps/content/extensions/update.xul"; michael@0: const URI_EXTENSION_STRINGS = "chrome://mozapps/locale/extensions/extensions.properties"; michael@0: michael@0: const STRING_TYPE_NAME = "type.%ID%.name"; michael@0: michael@0: const DIR_EXTENSIONS = "extensions"; michael@0: const DIR_STAGE = "staged"; michael@0: const DIR_XPI_STAGE = "staged-xpis"; michael@0: const DIR_TRASH = "trash"; michael@0: michael@0: const FILE_DATABASE = "extensions.json"; michael@0: const FILE_OLD_CACHE = "extensions.cache"; michael@0: const FILE_INSTALL_MANIFEST = "install.rdf"; michael@0: const FILE_XPI_ADDONS_LIST = "extensions.ini"; michael@0: michael@0: const KEY_PROFILEDIR = "ProfD"; michael@0: const KEY_APPDIR = "XCurProcD"; michael@0: const KEY_TEMPDIR = "TmpD"; michael@0: const KEY_APP_DISTRIBUTION = "XREAppDist"; michael@0: michael@0: const KEY_APP_PROFILE = "app-profile"; michael@0: const KEY_APP_GLOBAL = "app-global"; michael@0: const KEY_APP_SYSTEM_LOCAL = "app-system-local"; michael@0: const KEY_APP_SYSTEM_SHARE = "app-system-share"; michael@0: const KEY_APP_SYSTEM_USER = "app-system-user"; michael@0: michael@0: const NOTIFICATION_FLUSH_PERMISSIONS = "flush-pending-permissions"; michael@0: const XPI_PERMISSION = "install"; michael@0: michael@0: const RDFURI_INSTALL_MANIFEST_ROOT = "urn:mozilla:install-manifest"; michael@0: const PREFIX_NS_EM = "http://www.mozilla.org/2004/em-rdf#"; michael@0: michael@0: const TOOLKIT_ID = "toolkit@mozilla.org"; michael@0: michael@0: // The value for this is in Makefile.in michael@0: #expand const DB_SCHEMA = __MOZ_EXTENSIONS_DB_SCHEMA__; michael@0: michael@0: // Properties that exist in the install manifest michael@0: const PROP_METADATA = ["id", "version", "type", "internalName", "updateURL", michael@0: "updateKey", "optionsURL", "optionsType", "aboutURL", michael@0: "iconURL", "icon64URL"]; michael@0: const PROP_LOCALE_SINGLE = ["name", "description", "creator", "homepageURL"]; michael@0: const PROP_LOCALE_MULTI = ["developers", "translators", "contributors"]; michael@0: const PROP_TARGETAPP = ["id", "minVersion", "maxVersion"]; michael@0: michael@0: // Properties that should be migrated where possible from an old database. These michael@0: // shouldn't include properties that can be read directly from install.rdf files michael@0: // or calculated michael@0: const DB_MIGRATE_METADATA= ["installDate", "userDisabled", "softDisabled", michael@0: "sourceURI", "applyBackgroundUpdates", michael@0: "releaseNotesURI", "foreignInstall", "syncGUID"]; michael@0: // Properties to cache and reload when an addon installation is pending michael@0: const PENDING_INSTALL_METADATA = michael@0: ["syncGUID", "targetApplications", "userDisabled", "softDisabled", michael@0: "existingAddonID", "sourceURI", "releaseNotesURI", "installDate", michael@0: "updateDate", "applyBackgroundUpdates", "compatibilityOverrides"]; michael@0: michael@0: // Note: When adding/changing/removing items here, remember to change the michael@0: // DB schema version to ensure changes are picked up ASAP. michael@0: const STATIC_BLOCKLIST_PATTERNS = [ michael@0: { creator: "Mozilla Corp.", michael@0: level: Ci.nsIBlocklistService.STATE_BLOCKED, michael@0: blockID: "i162" }, michael@0: { creator: "Mozilla.org", michael@0: level: Ci.nsIBlocklistService.STATE_BLOCKED, michael@0: blockID: "i162" } michael@0: ]; michael@0: michael@0: michael@0: const BOOTSTRAP_REASONS = { michael@0: APP_STARTUP : 1, michael@0: APP_SHUTDOWN : 2, michael@0: ADDON_ENABLE : 3, michael@0: ADDON_DISABLE : 4, michael@0: ADDON_INSTALL : 5, michael@0: ADDON_UNINSTALL : 6, michael@0: ADDON_UPGRADE : 7, michael@0: ADDON_DOWNGRADE : 8 michael@0: }; michael@0: michael@0: // Map new string type identifiers to old style nsIUpdateItem types michael@0: const TYPES = { michael@0: extension: 2, michael@0: theme: 4, michael@0: locale: 8, michael@0: multipackage: 32, michael@0: dictionary: 64, michael@0: experiment: 128, michael@0: }; michael@0: michael@0: const RESTARTLESS_TYPES = new Set([ michael@0: "dictionary", michael@0: "experiment", michael@0: "locale", michael@0: ]); michael@0: michael@0: // Keep track of where we are in startup for telemetry michael@0: // event happened during XPIDatabase.startup() michael@0: const XPI_STARTING = "XPIStarting"; michael@0: // event happened after startup() but before the final-ui-startup event michael@0: const XPI_BEFORE_UI_STARTUP = "BeforeFinalUIStartup"; michael@0: // event happened after final-ui-startup michael@0: const XPI_AFTER_UI_STARTUP = "AfterFinalUIStartup"; michael@0: michael@0: const COMPATIBLE_BY_DEFAULT_TYPES = { michael@0: extension: true, michael@0: dictionary: true michael@0: }; michael@0: michael@0: const MSG_JAR_FLUSH = "AddonJarFlush"; michael@0: michael@0: var gGlobalScope = this; michael@0: michael@0: /** michael@0: * Valid IDs fit this pattern. michael@0: */ michael@0: 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; michael@0: michael@0: Cu.import("resource://gre/modules/Log.jsm"); michael@0: const LOGGER_ID = "addons.xpi"; michael@0: michael@0: // Create a new logger for use by all objects in this Addons XPI Provider module michael@0: // (Requires AddonManager.jsm) michael@0: let logger = Log.repository.getLogger(LOGGER_ID); michael@0: michael@0: const LAZY_OBJECTS = ["XPIDatabase"]; michael@0: michael@0: var gLazyObjectsLoaded = false; michael@0: michael@0: function loadLazyObjects() { michael@0: let scope = {}; michael@0: scope.AddonInternal = AddonInternal; michael@0: scope.XPIProvider = XPIProvider; michael@0: Services.scriptloader.loadSubScript("resource://gre/modules/addons/XPIProviderUtils.js", michael@0: scope); michael@0: michael@0: for (let name of LAZY_OBJECTS) { michael@0: delete gGlobalScope[name]; michael@0: gGlobalScope[name] = scope[name]; michael@0: } michael@0: gLazyObjectsLoaded = true; michael@0: return scope; michael@0: } michael@0: michael@0: for (let name of LAZY_OBJECTS) { michael@0: Object.defineProperty(gGlobalScope, name, { michael@0: get: function lazyObjectGetter() { michael@0: let objs = loadLazyObjects(); michael@0: return objs[name]; michael@0: }, michael@0: configurable: true michael@0: }); michael@0: } michael@0: michael@0: michael@0: function findMatchingStaticBlocklistItem(aAddon) { michael@0: for (let item of STATIC_BLOCKLIST_PATTERNS) { michael@0: if ("creator" in item && typeof item.creator == "string") { michael@0: if ((aAddon.defaultLocale && aAddon.defaultLocale.creator == item.creator) || michael@0: (aAddon.selectedLocale && aAddon.selectedLocale.creator == item.creator)) { michael@0: return item; michael@0: } michael@0: } michael@0: } michael@0: return null; michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Sets permissions on a file michael@0: * michael@0: * @param aFile michael@0: * The file or directory to operate on. michael@0: * @param aPermissions michael@0: * The permisions to set michael@0: */ michael@0: function setFilePermissions(aFile, aPermissions) { michael@0: try { michael@0: aFile.permissions = aPermissions; michael@0: } michael@0: catch (e) { michael@0: logger.warn("Failed to set permissions " + aPermissions.toString(8) + " on " + michael@0: aFile.path, e); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * A safe way to install a file or the contents of a directory to a new michael@0: * directory. The file or directory is moved or copied recursively and if michael@0: * anything fails an attempt is made to rollback the entire operation. The michael@0: * operation may also be rolled back to its original state after it has michael@0: * completed by calling the rollback method. michael@0: * michael@0: * Operations can be chained. Calling move or copy multiple times will remember michael@0: * the whole set and if one fails all of the operations will be rolled back. michael@0: */ michael@0: function SafeInstallOperation() { michael@0: this._installedFiles = []; michael@0: this._createdDirs = []; michael@0: } michael@0: michael@0: SafeInstallOperation.prototype = { michael@0: _installedFiles: null, michael@0: _createdDirs: null, michael@0: michael@0: _installFile: function SIO_installFile(aFile, aTargetDirectory, aCopy) { michael@0: let oldFile = aCopy ? null : aFile.clone(); michael@0: let newFile = aFile.clone(); michael@0: try { michael@0: if (aCopy) michael@0: newFile.copyTo(aTargetDirectory, null); michael@0: else michael@0: newFile.moveTo(aTargetDirectory, null); michael@0: } michael@0: catch (e) { michael@0: logger.error("Failed to " + (aCopy ? "copy" : "move") + " file " + aFile.path + michael@0: " to " + aTargetDirectory.path, e); michael@0: throw e; michael@0: } michael@0: this._installedFiles.push({ oldFile: oldFile, newFile: newFile }); michael@0: }, michael@0: michael@0: _installDirectory: function SIO_installDirectory(aDirectory, aTargetDirectory, aCopy) { michael@0: let newDir = aTargetDirectory.clone(); michael@0: newDir.append(aDirectory.leafName); michael@0: try { michael@0: newDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); michael@0: } michael@0: catch (e) { michael@0: logger.error("Failed to create directory " + newDir.path, e); michael@0: throw e; michael@0: } michael@0: this._createdDirs.push(newDir); michael@0: michael@0: // Use a snapshot of the directory contents to avoid possible issues with michael@0: // iterating over a directory while removing files from it (the YAFFS2 michael@0: // embedded filesystem has this issue, see bug 772238), and to remove michael@0: // normal files before their resource forks on OSX (see bug 733436). michael@0: let entries = getDirectoryEntries(aDirectory, true); michael@0: entries.forEach(function(aEntry) { michael@0: try { michael@0: this._installDirEntry(aEntry, newDir, aCopy); michael@0: } michael@0: catch (e) { michael@0: logger.error("Failed to " + (aCopy ? "copy" : "move") + " entry " + michael@0: aEntry.path, e); michael@0: throw e; michael@0: } michael@0: }, this); michael@0: michael@0: // If this is only a copy operation then there is nothing else to do michael@0: if (aCopy) michael@0: return; michael@0: michael@0: // The directory should be empty by this point. If it isn't this will throw michael@0: // and all of the operations will be rolled back michael@0: try { michael@0: setFilePermissions(aDirectory, FileUtils.PERMS_DIRECTORY); michael@0: aDirectory.remove(false); michael@0: } michael@0: catch (e) { michael@0: logger.error("Failed to remove directory " + aDirectory.path, e); michael@0: throw e; michael@0: } michael@0: michael@0: // Note we put the directory move in after all the file moves so the michael@0: // directory is recreated before all the files are moved back michael@0: this._installedFiles.push({ oldFile: aDirectory, newFile: newDir }); michael@0: }, michael@0: michael@0: _installDirEntry: function SIO_installDirEntry(aDirEntry, aTargetDirectory, aCopy) { michael@0: let isDir = null; michael@0: michael@0: try { michael@0: isDir = aDirEntry.isDirectory(); michael@0: } michael@0: catch (e) { michael@0: // If the file has already gone away then don't worry about it, this can michael@0: // happen on OSX where the resource fork is automatically moved with the michael@0: // data fork for the file. See bug 733436. michael@0: if (e.result == Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) michael@0: return; michael@0: michael@0: logger.error("Failure " + (aCopy ? "copying" : "moving") + " " + aDirEntry.path + michael@0: " to " + aTargetDirectory.path); michael@0: throw e; michael@0: } michael@0: michael@0: try { michael@0: if (isDir) michael@0: this._installDirectory(aDirEntry, aTargetDirectory, aCopy); michael@0: else michael@0: this._installFile(aDirEntry, aTargetDirectory, aCopy); michael@0: } michael@0: catch (e) { michael@0: logger.error("Failure " + (aCopy ? "copying" : "moving") + " " + aDirEntry.path + michael@0: " to " + aTargetDirectory.path); michael@0: throw e; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Moves a file or directory into a new directory. If an error occurs then all michael@0: * files that have been moved will be moved back to their original location. michael@0: * michael@0: * @param aFile michael@0: * The file or directory to be moved. michael@0: * @param aTargetDirectory michael@0: * The directory to move into, this is expected to be an empty michael@0: * directory. michael@0: */ michael@0: move: function SIO_move(aFile, aTargetDirectory) { michael@0: try { michael@0: this._installDirEntry(aFile, aTargetDirectory, false); michael@0: } michael@0: catch (e) { michael@0: this.rollback(); michael@0: throw e; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Copies a file or directory into a new directory. If an error occurs then michael@0: * all new files that have been created will be removed. michael@0: * michael@0: * @param aFile michael@0: * The file or directory to be copied. michael@0: * @param aTargetDirectory michael@0: * The directory to copy into, this is expected to be an empty michael@0: * directory. michael@0: */ michael@0: copy: function SIO_copy(aFile, aTargetDirectory) { michael@0: try { michael@0: this._installDirEntry(aFile, aTargetDirectory, true); michael@0: } michael@0: catch (e) { michael@0: this.rollback(); michael@0: throw e; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Rolls back all the moves that this operation performed. If an exception michael@0: * occurs here then both old and new directories are left in an indeterminate michael@0: * state michael@0: */ michael@0: rollback: function SIO_rollback() { michael@0: while (this._installedFiles.length > 0) { michael@0: let move = this._installedFiles.pop(); michael@0: if (move.newFile.isDirectory()) { michael@0: let oldDir = move.oldFile.parent.clone(); michael@0: oldDir.append(move.oldFile.leafName); michael@0: oldDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); michael@0: } michael@0: else if (!move.oldFile) { michael@0: // No old file means this was a copied file michael@0: move.newFile.remove(true); michael@0: } michael@0: else { michael@0: move.newFile.moveTo(move.oldFile.parent, null); michael@0: } michael@0: } michael@0: michael@0: while (this._createdDirs.length > 0) michael@0: recursiveRemove(this._createdDirs.pop()); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Gets the currently selected locale for display. michael@0: * @return the selected locale or "en-US" if none is selected michael@0: */ michael@0: function getLocale() { michael@0: if (Prefs.getBoolPref(PREF_MATCH_OS_LOCALE, false)) michael@0: return Services.locale.getLocaleComponentForUserAgent(); michael@0: let locale = Prefs.getComplexValue(PREF_SELECTED_LOCALE, Ci.nsIPrefLocalizedString); michael@0: if (locale) michael@0: return locale; michael@0: return Prefs.getCharPref(PREF_SELECTED_LOCALE, "en-US"); michael@0: } michael@0: michael@0: /** michael@0: * Selects the closest matching locale from a list of locales. michael@0: * michael@0: * @param aLocales michael@0: * An array of locales michael@0: * @return the best match for the currently selected locale michael@0: */ michael@0: function findClosestLocale(aLocales) { michael@0: let appLocale = getLocale(); michael@0: michael@0: // Holds the best matching localized resource michael@0: var bestmatch = null; michael@0: // The number of locale parts it matched with michael@0: var bestmatchcount = 0; michael@0: // The number of locale parts in the match michael@0: var bestpartcount = 0; michael@0: michael@0: var matchLocales = [appLocale.toLowerCase()]; michael@0: /* If the current locale is English then it will find a match if there is michael@0: a valid match for en-US so no point searching that locale too. */ michael@0: if (matchLocales[0].substring(0, 3) != "en-") michael@0: matchLocales.push("en-us"); michael@0: michael@0: for each (var locale in matchLocales) { michael@0: var lparts = locale.split("-"); michael@0: for each (var localized in aLocales) { michael@0: for each (let found in localized.locales) { michael@0: found = found.toLowerCase(); michael@0: // Exact match is returned immediately michael@0: if (locale == found) michael@0: return localized; michael@0: michael@0: var fparts = found.split("-"); michael@0: /* If we have found a possible match and this one isn't any longer michael@0: then we dont need to check further. */ michael@0: if (bestmatch && fparts.length < bestmatchcount) michael@0: continue; michael@0: michael@0: // Count the number of parts that match michael@0: var maxmatchcount = Math.min(fparts.length, lparts.length); michael@0: var matchcount = 0; michael@0: while (matchcount < maxmatchcount && michael@0: fparts[matchcount] == lparts[matchcount]) michael@0: matchcount++; michael@0: michael@0: /* If we matched more than the last best match or matched the same and michael@0: this locale is less specific than the last best match. */ michael@0: if (matchcount > bestmatchcount || michael@0: (matchcount == bestmatchcount && fparts.length < bestpartcount)) { michael@0: bestmatch = localized; michael@0: bestmatchcount = matchcount; michael@0: bestpartcount = fparts.length; michael@0: } michael@0: } michael@0: } michael@0: // If we found a valid match for this locale return it michael@0: if (bestmatch) michael@0: return bestmatch; michael@0: } michael@0: return null; michael@0: } michael@0: michael@0: /** michael@0: * Sets the userDisabled and softDisabled properties of an add-on based on what michael@0: * values those properties had for a previous instance of the add-on. The michael@0: * previous instance may be a previous install or in the case of an application michael@0: * version change the same add-on. michael@0: * michael@0: * NOTE: this may modify aNewAddon in place; callers should save the database if michael@0: * necessary michael@0: * michael@0: * @param aOldAddon michael@0: * The previous instance of the add-on michael@0: * @param aNewAddon michael@0: * The new instance of the add-on michael@0: * @param aAppVersion michael@0: * The optional application version to use when checking the blocklist michael@0: * or undefined to use the current application michael@0: * @param aPlatformVersion michael@0: * The optional platform version to use when checking the blocklist or michael@0: * undefined to use the current platform michael@0: */ michael@0: function applyBlocklistChanges(aOldAddon, aNewAddon, aOldAppVersion, michael@0: aOldPlatformVersion) { michael@0: // Copy the properties by default michael@0: aNewAddon.userDisabled = aOldAddon.userDisabled; michael@0: aNewAddon.softDisabled = aOldAddon.softDisabled; michael@0: michael@0: let bs = Cc["@mozilla.org/extensions/blocklist;1"]. michael@0: getService(Ci.nsIBlocklistService); michael@0: michael@0: let oldBlocklistState = bs.getAddonBlocklistState(createWrapper(aOldAddon), michael@0: aOldAppVersion, michael@0: aOldPlatformVersion); michael@0: let newBlocklistState = bs.getAddonBlocklistState(createWrapper(aNewAddon)); michael@0: michael@0: // If the blocklist state hasn't changed then the properties don't need to michael@0: // change michael@0: if (newBlocklistState == oldBlocklistState) michael@0: return; michael@0: michael@0: if (newBlocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED) { michael@0: if (aNewAddon.type != "theme") { michael@0: // The add-on has become softblocked, set softDisabled if it isn't already michael@0: // userDisabled michael@0: aNewAddon.softDisabled = !aNewAddon.userDisabled; michael@0: } michael@0: else { michael@0: // Themes just get userDisabled to switch back to the default theme michael@0: aNewAddon.userDisabled = true; michael@0: } michael@0: } michael@0: else { michael@0: // If the new add-on is not softblocked then it cannot be softDisabled michael@0: aNewAddon.softDisabled = false; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Calculates whether an add-on should be appDisabled or not. michael@0: * michael@0: * @param aAddon michael@0: * The add-on to check michael@0: * @return true if the add-on should not be appDisabled michael@0: */ michael@0: function isUsableAddon(aAddon) { michael@0: // Hack to ensure the default theme is always usable michael@0: if (aAddon.type == "theme" && aAddon.internalName == XPIProvider.defaultSkin) michael@0: return true; michael@0: michael@0: if (aAddon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) michael@0: return false; michael@0: michael@0: if (AddonManager.checkUpdateSecurity && !aAddon.providesUpdatesSecurely) michael@0: return false; michael@0: michael@0: if (!aAddon.isPlatformCompatible) michael@0: return false; michael@0: michael@0: if (AddonManager.checkCompatibility) { michael@0: if (!aAddon.isCompatible) michael@0: return false; michael@0: } michael@0: else { michael@0: let app = aAddon.matchingTargetApplication; michael@0: if (!app) michael@0: return false; michael@0: michael@0: // XXX Temporary solution to let applications opt-in to make themes safer michael@0: // following significant UI changes even if checkCompatibility=false has michael@0: // been set, until we get bug 962001. michael@0: if (aAddon.type == "theme" && app.id == Services.appinfo.ID) { michael@0: try { michael@0: let minCompatVersion = Services.prefs.getCharPref(PREF_CHECKCOMAT_THEMEOVERRIDE); michael@0: if (minCompatVersion && michael@0: Services.vc.compare(minCompatVersion, app.maxVersion) > 0) { michael@0: return false; michael@0: } michael@0: } catch (e) {} michael@0: } michael@0: } michael@0: michael@0: return true; michael@0: } michael@0: michael@0: function isAddonDisabled(aAddon) { michael@0: return aAddon.appDisabled || aAddon.softDisabled || aAddon.userDisabled; michael@0: } michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "gRDF", "@mozilla.org/rdf/rdf-service;1", michael@0: Ci.nsIRDFService); michael@0: michael@0: function EM_R(aProperty) { michael@0: return gRDF.GetResource(PREFIX_NS_EM + aProperty); michael@0: } michael@0: michael@0: /** michael@0: * Converts an RDF literal, resource or integer into a string. michael@0: * michael@0: * @param aLiteral michael@0: * The RDF object to convert michael@0: * @return a string if the object could be converted or null michael@0: */ michael@0: function getRDFValue(aLiteral) { michael@0: if (aLiteral instanceof Ci.nsIRDFLiteral) michael@0: return aLiteral.Value; michael@0: if (aLiteral instanceof Ci.nsIRDFResource) michael@0: return aLiteral.Value; michael@0: if (aLiteral instanceof Ci.nsIRDFInt) michael@0: return aLiteral.Value; michael@0: return null; michael@0: } michael@0: michael@0: /** michael@0: * Gets an RDF property as a string michael@0: * michael@0: * @param aDs michael@0: * The RDF datasource to read the property from michael@0: * @param aResource michael@0: * The RDF resource to read the property from michael@0: * @param aProperty michael@0: * The property to read michael@0: * @return a string if the property existed or null michael@0: */ michael@0: function getRDFProperty(aDs, aResource, aProperty) { michael@0: return getRDFValue(aDs.GetTarget(aResource, EM_R(aProperty), true)); michael@0: } michael@0: michael@0: /** michael@0: * Reads an AddonInternal object from an RDF stream. michael@0: * michael@0: * @param aUri michael@0: * The URI that the manifest is being read from michael@0: * @param aStream michael@0: * An open stream to read the RDF from michael@0: * @return an AddonInternal object michael@0: * @throws if the install manifest in the RDF stream is corrupt or could not michael@0: * be read michael@0: */ michael@0: function loadManifestFromRDF(aUri, aStream) { michael@0: function getPropertyArray(aDs, aSource, aProperty) { michael@0: let values = []; michael@0: let targets = aDs.GetTargets(aSource, EM_R(aProperty), true); michael@0: while (targets.hasMoreElements()) michael@0: values.push(getRDFValue(targets.getNext())); michael@0: michael@0: return values; michael@0: } michael@0: michael@0: /** michael@0: * Reads locale properties from either the main install manifest root or michael@0: * an em:localized section in the install manifest. michael@0: * michael@0: * @param aDs michael@0: * The nsIRDFDatasource to read from michael@0: * @param aSource michael@0: * The nsIRDFResource to read the properties from michael@0: * @param isDefault michael@0: * True if the locale is to be read from the main install manifest michael@0: * root michael@0: * @param aSeenLocales michael@0: * An array of locale names already seen for this install manifest. michael@0: * Any locale names seen as a part of this function will be added to michael@0: * this array michael@0: * @return an object containing the locale properties michael@0: */ michael@0: function readLocale(aDs, aSource, isDefault, aSeenLocales) { michael@0: let locale = { }; michael@0: if (!isDefault) { michael@0: locale.locales = []; michael@0: let targets = ds.GetTargets(aSource, EM_R("locale"), true); michael@0: while (targets.hasMoreElements()) { michael@0: let localeName = getRDFValue(targets.getNext()); michael@0: if (!localeName) { michael@0: logger.warn("Ignoring empty locale in localized properties"); michael@0: continue; michael@0: } michael@0: if (aSeenLocales.indexOf(localeName) != -1) { michael@0: logger.warn("Ignoring duplicate locale in localized properties"); michael@0: continue; michael@0: } michael@0: aSeenLocales.push(localeName); michael@0: locale.locales.push(localeName); michael@0: } michael@0: michael@0: if (locale.locales.length == 0) { michael@0: logger.warn("Ignoring localized properties with no listed locales"); michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: PROP_LOCALE_SINGLE.forEach(function(aProp) { michael@0: locale[aProp] = getRDFProperty(aDs, aSource, aProp); michael@0: }); michael@0: michael@0: PROP_LOCALE_MULTI.forEach(function(aProp) { michael@0: // Don't store empty arrays michael@0: let props = getPropertyArray(aDs, aSource, michael@0: aProp.substring(0, aProp.length - 1)); michael@0: if (props.length > 0) michael@0: locale[aProp] = props; michael@0: }); michael@0: michael@0: return locale; michael@0: } michael@0: michael@0: let rdfParser = Cc["@mozilla.org/rdf/xml-parser;1"]. michael@0: createInstance(Ci.nsIRDFXMLParser) michael@0: let ds = Cc["@mozilla.org/rdf/datasource;1?name=in-memory-datasource"]. michael@0: createInstance(Ci.nsIRDFDataSource); michael@0: let listener = rdfParser.parseAsync(ds, aUri); michael@0: let channel = Cc["@mozilla.org/network/input-stream-channel;1"]. michael@0: createInstance(Ci.nsIInputStreamChannel); michael@0: channel.setURI(aUri); michael@0: channel.contentStream = aStream; michael@0: channel.QueryInterface(Ci.nsIChannel); michael@0: channel.contentType = "text/xml"; michael@0: michael@0: listener.onStartRequest(channel, null); michael@0: michael@0: try { michael@0: let pos = 0; michael@0: let count = aStream.available(); michael@0: while (count > 0) { michael@0: listener.onDataAvailable(channel, null, aStream, pos, count); michael@0: pos += count; michael@0: count = aStream.available(); michael@0: } michael@0: listener.onStopRequest(channel, null, Components.results.NS_OK); michael@0: } michael@0: catch (e) { michael@0: listener.onStopRequest(channel, null, e.result); michael@0: throw e; michael@0: } michael@0: michael@0: let root = gRDF.GetResource(RDFURI_INSTALL_MANIFEST_ROOT); michael@0: let addon = new AddonInternal(); michael@0: PROP_METADATA.forEach(function(aProp) { michael@0: addon[aProp] = getRDFProperty(ds, root, aProp); michael@0: }); michael@0: addon.unpack = getRDFProperty(ds, root, "unpack") == "true"; michael@0: michael@0: if (!addon.type) { michael@0: addon.type = addon.internalName ? "theme" : "extension"; michael@0: } michael@0: else { michael@0: let type = addon.type; michael@0: addon.type = null; michael@0: for (let name in TYPES) { michael@0: if (TYPES[name] == type) { michael@0: addon.type = name; michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: michael@0: if (!(addon.type in TYPES)) michael@0: throw new Error("Install manifest specifies unknown type: " + addon.type); michael@0: michael@0: if (addon.type != "multipackage") { michael@0: if (!addon.id) michael@0: throw new Error("No ID in install manifest"); michael@0: if (!gIDTest.test(addon.id)) michael@0: throw new Error("Illegal add-on ID " + addon.id); michael@0: if (!addon.version) michael@0: throw new Error("No version in install manifest"); michael@0: } michael@0: michael@0: addon.strictCompatibility = !(addon.type in COMPATIBLE_BY_DEFAULT_TYPES) || michael@0: getRDFProperty(ds, root, "strictCompatibility") == "true"; michael@0: michael@0: // Only read the bootstrap property for extensions. michael@0: if (addon.type == "extension") { michael@0: addon.bootstrap = getRDFProperty(ds, root, "bootstrap") == "true"; michael@0: if (addon.optionsType && michael@0: addon.optionsType != AddonManager.OPTIONS_TYPE_DIALOG && michael@0: addon.optionsType != AddonManager.OPTIONS_TYPE_INLINE && michael@0: addon.optionsType != AddonManager.OPTIONS_TYPE_TAB && michael@0: addon.optionsType != AddonManager.OPTIONS_TYPE_INLINE_INFO) { michael@0: throw new Error("Install manifest specifies unknown type: " + addon.optionsType); michael@0: } michael@0: } michael@0: else { michael@0: // Some add-on types are always restartless. michael@0: if (RESTARTLESS_TYPES.has(addon.type)) { michael@0: addon.bootstrap = true; michael@0: } michael@0: michael@0: // Only extensions are allowed to provide an optionsURL, optionsType or aboutURL. For michael@0: // all other types they are silently ignored michael@0: addon.optionsURL = null; michael@0: addon.optionsType = null; michael@0: addon.aboutURL = null; michael@0: michael@0: if (addon.type == "theme") { michael@0: if (!addon.internalName) michael@0: throw new Error("Themes must include an internalName property"); michael@0: addon.skinnable = getRDFProperty(ds, root, "skinnable") == "true"; michael@0: } michael@0: } michael@0: michael@0: addon.defaultLocale = readLocale(ds, root, true); michael@0: michael@0: let seenLocales = []; michael@0: addon.locales = []; michael@0: let targets = ds.GetTargets(root, EM_R("localized"), true); michael@0: while (targets.hasMoreElements()) { michael@0: let target = targets.getNext().QueryInterface(Ci.nsIRDFResource); michael@0: let locale = readLocale(ds, target, false, seenLocales); michael@0: if (locale) michael@0: addon.locales.push(locale); michael@0: } michael@0: michael@0: let seenApplications = []; michael@0: addon.targetApplications = []; michael@0: targets = ds.GetTargets(root, EM_R("targetApplication"), true); michael@0: while (targets.hasMoreElements()) { michael@0: let target = targets.getNext().QueryInterface(Ci.nsIRDFResource); michael@0: let targetAppInfo = {}; michael@0: PROP_TARGETAPP.forEach(function(aProp) { michael@0: targetAppInfo[aProp] = getRDFProperty(ds, target, aProp); michael@0: }); michael@0: if (!targetAppInfo.id || !targetAppInfo.minVersion || michael@0: !targetAppInfo.maxVersion) { michael@0: logger.warn("Ignoring invalid targetApplication entry in install manifest"); michael@0: continue; michael@0: } michael@0: if (seenApplications.indexOf(targetAppInfo.id) != -1) { michael@0: logger.warn("Ignoring duplicate targetApplication entry for " + targetAppInfo.id + michael@0: " in install manifest"); michael@0: continue; michael@0: } michael@0: seenApplications.push(targetAppInfo.id); michael@0: addon.targetApplications.push(targetAppInfo); michael@0: } michael@0: michael@0: // Note that we don't need to check for duplicate targetPlatform entries since michael@0: // the RDF service coalesces them for us. michael@0: let targetPlatforms = getPropertyArray(ds, root, "targetPlatform"); michael@0: addon.targetPlatforms = []; michael@0: targetPlatforms.forEach(function(aPlatform) { michael@0: let platform = { michael@0: os: null, michael@0: abi: null michael@0: }; michael@0: michael@0: let pos = aPlatform.indexOf("_"); michael@0: if (pos != -1) { michael@0: platform.os = aPlatform.substring(0, pos); michael@0: platform.abi = aPlatform.substring(pos + 1); michael@0: } michael@0: else { michael@0: platform.os = aPlatform; michael@0: } michael@0: michael@0: addon.targetPlatforms.push(platform); michael@0: }); michael@0: michael@0: // A theme's userDisabled value is true if the theme is not the selected skin michael@0: // or if there is an active lightweight theme. We ignore whether softblocking michael@0: // is in effect since it would change the active theme. michael@0: if (addon.type == "theme") { michael@0: addon.userDisabled = !!LightweightThemeManager.currentTheme || michael@0: addon.internalName != XPIProvider.selectedSkin; michael@0: } michael@0: // Experiments are disabled by default. It is up to the Experiments Manager michael@0: // to enable them (it drives installation). michael@0: else if (addon.type == "experiment") { michael@0: addon.userDisabled = true; michael@0: } michael@0: else { michael@0: addon.userDisabled = false; michael@0: addon.softDisabled = addon.blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED; michael@0: } michael@0: michael@0: addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT; michael@0: michael@0: // Experiments are managed and updated through an external "experiments michael@0: // manager." So disable some built-in mechanisms. michael@0: if (addon.type == "experiment") { michael@0: addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DISABLE; michael@0: addon.updateURL = null; michael@0: addon.updateKey = null; michael@0: michael@0: addon.targetApplications = []; michael@0: addon.targetPlatforms = []; michael@0: } michael@0: michael@0: // Load the storage service before NSS (nsIRandomGenerator), michael@0: // to avoid a SQLite initialization error (bug 717904). michael@0: let storage = Services.storage; michael@0: michael@0: // Generate random GUID used for Sync. michael@0: // This was lifted from util.js:makeGUID() from services-sync. michael@0: let rng = Cc["@mozilla.org/security/random-generator;1"]. michael@0: createInstance(Ci.nsIRandomGenerator); michael@0: let bytes = rng.generateRandomBytes(9); michael@0: let byte_string = [String.fromCharCode(byte) for each (byte in bytes)] michael@0: .join(""); michael@0: // Base64 encode michael@0: addon.syncGUID = btoa(byte_string).replace(/\+/g, '-') michael@0: .replace(/\//g, '_'); michael@0: michael@0: return addon; michael@0: } michael@0: michael@0: /** michael@0: * Loads an AddonInternal object from an add-on extracted in a directory. michael@0: * michael@0: * @param aDir michael@0: * The nsIFile directory holding the add-on michael@0: * @return an AddonInternal object michael@0: * @throws if the directory does not contain a valid install manifest michael@0: */ michael@0: function loadManifestFromDir(aDir) { michael@0: function getFileSize(aFile) { michael@0: if (aFile.isSymlink()) michael@0: return 0; michael@0: michael@0: if (!aFile.isDirectory()) michael@0: return aFile.fileSize; michael@0: michael@0: let size = 0; michael@0: let entries = aFile.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator); michael@0: let entry; michael@0: while ((entry = entries.nextFile)) michael@0: size += getFileSize(entry); michael@0: entries.close(); michael@0: return size; michael@0: } michael@0: michael@0: let file = aDir.clone(); michael@0: file.append(FILE_INSTALL_MANIFEST); michael@0: if (!file.exists() || !file.isFile()) michael@0: throw new Error("Directory " + aDir.path + " does not contain a valid " + michael@0: "install manifest"); michael@0: michael@0: let fis = Cc["@mozilla.org/network/file-input-stream;1"]. michael@0: createInstance(Ci.nsIFileInputStream); michael@0: fis.init(file, -1, -1, false); michael@0: let bis = Cc["@mozilla.org/network/buffered-input-stream;1"]. michael@0: createInstance(Ci.nsIBufferedInputStream); michael@0: bis.init(fis, 4096); michael@0: michael@0: try { michael@0: let addon = loadManifestFromRDF(Services.io.newFileURI(file), bis); michael@0: addon._sourceBundle = aDir.clone(); michael@0: addon.size = getFileSize(aDir); michael@0: michael@0: file = aDir.clone(); michael@0: file.append("chrome.manifest"); michael@0: let chromeManifest = ChromeManifestParser.parseSync(Services.io.newFileURI(file)); michael@0: addon.hasBinaryComponents = ChromeManifestParser.hasType(chromeManifest, michael@0: "binary-component"); michael@0: michael@0: addon.appDisabled = !isUsableAddon(addon); michael@0: return addon; michael@0: } michael@0: finally { michael@0: bis.close(); michael@0: fis.close(); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Loads an AddonInternal object from an nsIZipReader for an add-on. michael@0: * michael@0: * @param aZipReader michael@0: * An open nsIZipReader for the add-on's files michael@0: * @return an AddonInternal object michael@0: * @throws if the XPI file does not contain a valid install manifest michael@0: */ michael@0: function loadManifestFromZipReader(aZipReader) { michael@0: let zis = aZipReader.getInputStream(FILE_INSTALL_MANIFEST); michael@0: let bis = Cc["@mozilla.org/network/buffered-input-stream;1"]. michael@0: createInstance(Ci.nsIBufferedInputStream); michael@0: bis.init(zis, 4096); michael@0: michael@0: try { michael@0: let uri = buildJarURI(aZipReader.file, FILE_INSTALL_MANIFEST); michael@0: let addon = loadManifestFromRDF(uri, bis); michael@0: addon._sourceBundle = aZipReader.file; michael@0: michael@0: addon.size = 0; michael@0: let entries = aZipReader.findEntries(null); michael@0: while (entries.hasMore()) michael@0: addon.size += aZipReader.getEntry(entries.getNext()).realSize; michael@0: michael@0: // Binary components can only be loaded from unpacked addons. michael@0: if (addon.unpack) { michael@0: uri = buildJarURI(aZipReader.file, "chrome.manifest"); michael@0: let chromeManifest = ChromeManifestParser.parseSync(uri); michael@0: addon.hasBinaryComponents = ChromeManifestParser.hasType(chromeManifest, michael@0: "binary-component"); michael@0: } else { michael@0: addon.hasBinaryComponents = false; michael@0: } michael@0: michael@0: addon.appDisabled = !isUsableAddon(addon); michael@0: return addon; michael@0: } michael@0: finally { michael@0: bis.close(); michael@0: zis.close(); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Loads an AddonInternal object from an add-on in an XPI file. michael@0: * michael@0: * @param aXPIFile michael@0: * An nsIFile pointing to the add-on's XPI file michael@0: * @return an AddonInternal object michael@0: * @throws if the XPI file does not contain a valid install manifest michael@0: */ michael@0: function loadManifestFromZipFile(aXPIFile) { michael@0: let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"]. michael@0: createInstance(Ci.nsIZipReader); michael@0: try { michael@0: zipReader.open(aXPIFile); michael@0: michael@0: return loadManifestFromZipReader(zipReader); michael@0: } michael@0: finally { michael@0: zipReader.close(); michael@0: } michael@0: } michael@0: michael@0: function loadManifestFromFile(aFile) { michael@0: if (aFile.isFile()) michael@0: return loadManifestFromZipFile(aFile); michael@0: else michael@0: return loadManifestFromDir(aFile); michael@0: } michael@0: michael@0: /** michael@0: * Gets an nsIURI for a file within another file, either a directory or an XPI michael@0: * file. If aFile is a directory then this will return a file: URI, if it is an michael@0: * XPI file then it will return a jar: URI. michael@0: * michael@0: * @param aFile michael@0: * The file containing the resources, must be either a directory or an michael@0: * XPI file michael@0: * @param aPath michael@0: * The path to find the resource at, "/" separated. If aPath is empty michael@0: * then the uri to the root of the contained files will be returned michael@0: * @return an nsIURI pointing at the resource michael@0: */ michael@0: function getURIForResourceInFile(aFile, aPath) { michael@0: if (aFile.isDirectory()) { michael@0: let resource = aFile.clone(); michael@0: if (aPath) { michael@0: aPath.split("/").forEach(function(aPart) { michael@0: resource.append(aPart); michael@0: }); michael@0: } michael@0: return NetUtil.newURI(resource); michael@0: } michael@0: michael@0: return buildJarURI(aFile, aPath); michael@0: } michael@0: michael@0: /** michael@0: * Creates a jar: URI for a file inside a ZIP file. michael@0: * michael@0: * @param aJarfile michael@0: * The ZIP file as an nsIFile michael@0: * @param aPath michael@0: * The path inside the ZIP file michael@0: * @return an nsIURI for the file michael@0: */ michael@0: function buildJarURI(aJarfile, aPath) { michael@0: let uri = Services.io.newFileURI(aJarfile); michael@0: uri = "jar:" + uri.spec + "!/" + aPath; michael@0: return NetUtil.newURI(uri); michael@0: } michael@0: michael@0: /** michael@0: * Sends local and remote notifications to flush a JAR file cache entry michael@0: * michael@0: * @param aJarFile michael@0: * The ZIP/XPI/JAR file as a nsIFile michael@0: */ michael@0: function flushJarCache(aJarFile) { michael@0: Services.obs.notifyObservers(aJarFile, "flush-cache-entry", null); michael@0: Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageBroadcaster) michael@0: .broadcastAsyncMessage(MSG_JAR_FLUSH, aJarFile.path); michael@0: } michael@0: michael@0: function flushStartupCache() { michael@0: // Init this, so it will get the notification. michael@0: Services.obs.notifyObservers(null, "startupcache-invalidate", null); michael@0: } michael@0: michael@0: /** michael@0: * Creates and returns a new unique temporary file. The caller should delete michael@0: * the file when it is no longer needed. michael@0: * michael@0: * @return an nsIFile that points to a randomly named, initially empty file in michael@0: * the OS temporary files directory michael@0: */ michael@0: function getTemporaryFile() { michael@0: let file = FileUtils.getDir(KEY_TEMPDIR, []); michael@0: let random = Math.random().toString(36).replace(/0./, '').substr(-3); michael@0: file.append("tmp-" + random + ".xpi"); michael@0: file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE); michael@0: michael@0: return file; michael@0: } michael@0: michael@0: /** michael@0: * Verifies that a zip file's contents are all signed by the same principal. michael@0: * Directory entries and anything in the META-INF directory are not checked. michael@0: * michael@0: * @param aZip michael@0: * A nsIZipReader to check michael@0: * @param aPrincipal michael@0: * The nsIPrincipal to compare against michael@0: * @return true if all the contents that should be signed were signed by the michael@0: * principal michael@0: */ michael@0: function verifyZipSigning(aZip, aPrincipal) { michael@0: var count = 0; michael@0: var entries = aZip.findEntries(null); michael@0: while (entries.hasMore()) { michael@0: var entry = entries.getNext(); michael@0: // Nothing in META-INF is in the manifest. michael@0: if (entry.substr(0, 9) == "META-INF/") michael@0: continue; michael@0: // Directory entries aren't in the manifest. michael@0: if (entry.substr(-1) == "/") michael@0: continue; michael@0: count++; michael@0: var entryPrincipal = aZip.getCertificatePrincipal(entry); michael@0: if (!entryPrincipal || !aPrincipal.equals(entryPrincipal)) michael@0: return false; michael@0: } michael@0: return aZip.manifestEntriesCount == count; michael@0: } michael@0: michael@0: /** michael@0: * Replaces %...% strings in an addon url (update and updateInfo) with michael@0: * appropriate values. michael@0: * michael@0: * @param aAddon michael@0: * The AddonInternal representing the add-on michael@0: * @param aUri michael@0: * The uri to escape michael@0: * @param aUpdateType michael@0: * An optional number representing the type of update, only applicable michael@0: * when creating a url for retrieving an update manifest michael@0: * @param aAppVersion michael@0: * The optional application version to use for %APP_VERSION% michael@0: * @return the appropriately escaped uri. michael@0: */ michael@0: function escapeAddonURI(aAddon, aUri, aUpdateType, aAppVersion) michael@0: { michael@0: let uri = AddonManager.escapeAddonURI(aAddon, aUri, aAppVersion); michael@0: michael@0: // If there is an updateType then replace the UPDATE_TYPE string michael@0: if (aUpdateType) michael@0: uri = uri.replace(/%UPDATE_TYPE%/g, aUpdateType); michael@0: michael@0: // If this add-on has compatibility information for either the current michael@0: // application or toolkit then replace the ITEM_MAXAPPVERSION with the michael@0: // maxVersion michael@0: let app = aAddon.matchingTargetApplication; michael@0: if (app) michael@0: var maxVersion = app.maxVersion; michael@0: else michael@0: maxVersion = ""; michael@0: uri = uri.replace(/%ITEM_MAXAPPVERSION%/g, maxVersion); michael@0: michael@0: let compatMode = "normal"; michael@0: if (!AddonManager.checkCompatibility) michael@0: compatMode = "ignore"; michael@0: else if (AddonManager.strictCompatibility) michael@0: compatMode = "strict"; michael@0: uri = uri.replace(/%COMPATIBILITY_MODE%/g, compatMode); michael@0: michael@0: return uri; michael@0: } michael@0: michael@0: function removeAsync(aFile) { michael@0: return Task.spawn(function () { michael@0: let info = null; michael@0: try { michael@0: info = yield OS.File.stat(aFile.path); michael@0: if (info.isDir) michael@0: yield OS.File.removeDir(aFile.path); michael@0: else michael@0: yield OS.File.remove(aFile.path); michael@0: } michael@0: catch (e if e instanceof OS.File.Error && e.becauseNoSuchFile) { michael@0: // The file has already gone away michael@0: return; michael@0: } michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * Recursively removes a directory or file fixing permissions when necessary. michael@0: * michael@0: * @param aFile michael@0: * The nsIFile to remove michael@0: */ michael@0: function recursiveRemove(aFile) { michael@0: let isDir = null; michael@0: michael@0: try { michael@0: isDir = aFile.isDirectory(); michael@0: } michael@0: catch (e) { michael@0: // If the file has already gone away then don't worry about it, this can michael@0: // happen on OSX where the resource fork is automatically moved with the michael@0: // data fork for the file. See bug 733436. michael@0: if (e.result == Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) michael@0: return; michael@0: if (e.result == Cr.NS_ERROR_FILE_NOT_FOUND) michael@0: return; michael@0: michael@0: throw e; michael@0: } michael@0: michael@0: setFilePermissions(aFile, isDir ? FileUtils.PERMS_DIRECTORY michael@0: : FileUtils.PERMS_FILE); michael@0: michael@0: try { michael@0: aFile.remove(true); michael@0: return; michael@0: } michael@0: catch (e) { michael@0: if (!aFile.isDirectory()) { michael@0: logger.error("Failed to remove file " + aFile.path, e); michael@0: throw e; michael@0: } michael@0: } michael@0: michael@0: // Use a snapshot of the directory contents to avoid possible issues with michael@0: // iterating over a directory while removing files from it (the YAFFS2 michael@0: // embedded filesystem has this issue, see bug 772238), and to remove michael@0: // normal files before their resource forks on OSX (see bug 733436). michael@0: let entries = getDirectoryEntries(aFile, true); michael@0: entries.forEach(recursiveRemove); michael@0: michael@0: try { michael@0: aFile.remove(true); michael@0: } michael@0: catch (e) { michael@0: logger.error("Failed to remove empty directory " + aFile.path, e); michael@0: throw e; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Returns the timestamp and leaf file name of the most recently modified michael@0: * entry in a directory, michael@0: * or simply the file's own timestamp if it is not a directory. michael@0: * Also returns the total number of items (directories and files) visited in the scan michael@0: * michael@0: * @param aFile michael@0: * A non-null nsIFile object michael@0: * @return [File Name, Epoch time, items visited], as described above. michael@0: */ michael@0: function recursiveLastModifiedTime(aFile) { michael@0: try { michael@0: let modTime = aFile.lastModifiedTime; michael@0: let fileName = aFile.leafName; michael@0: if (aFile.isFile()) michael@0: return [fileName, modTime, 1]; michael@0: michael@0: if (aFile.isDirectory()) { michael@0: let entries = aFile.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator); michael@0: let entry; michael@0: let totalItems = 1; michael@0: while ((entry = entries.nextFile)) { michael@0: let [subName, subTime, items] = recursiveLastModifiedTime(entry); michael@0: totalItems += items; michael@0: if (subTime > modTime) { michael@0: modTime = subTime; michael@0: fileName = subName; michael@0: } michael@0: } michael@0: entries.close(); michael@0: return [fileName, modTime, totalItems]; michael@0: } michael@0: } michael@0: catch (e) { michael@0: logger.warn("Problem getting last modified time for " + aFile.path, e); michael@0: } michael@0: michael@0: // If the file is something else, just ignore it. michael@0: return ["", 0, 0]; michael@0: } michael@0: michael@0: /** michael@0: * Gets a snapshot of directory entries. michael@0: * michael@0: * @param aDir michael@0: * Directory to look at michael@0: * @param aSortEntries michael@0: * True to sort entries by filename michael@0: * @return An array of nsIFile, or an empty array if aDir is not a readable directory michael@0: */ michael@0: function getDirectoryEntries(aDir, aSortEntries) { michael@0: let dirEnum; michael@0: try { michael@0: dirEnum = aDir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator); michael@0: let entries = []; michael@0: while (dirEnum.hasMoreElements()) michael@0: entries.push(dirEnum.nextFile); michael@0: michael@0: if (aSortEntries) { michael@0: entries.sort(function sortDirEntries(a, b) { michael@0: return a.path > b.path ? -1 : 1; michael@0: }); michael@0: } michael@0: michael@0: return entries michael@0: } michael@0: catch (e) { michael@0: logger.warn("Can't iterate directory " + aDir.path, e); michael@0: return []; michael@0: } michael@0: finally { michael@0: if (dirEnum) { michael@0: dirEnum.close(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * A helpful wrapper around the prefs service that allows for default values michael@0: * when requested values aren't set. michael@0: */ michael@0: var Prefs = { michael@0: /** michael@0: * Gets a preference from the default branch ignoring user-set values. michael@0: * michael@0: * @param aName michael@0: * The name of the preference michael@0: * @param aDefaultValue michael@0: * A value to return if the preference does not exist michael@0: * @return the default value of the preference or aDefaultValue if there is michael@0: * none michael@0: */ michael@0: getDefaultCharPref: function Prefs_getDefaultCharPref(aName, aDefaultValue) { michael@0: try { michael@0: return Services.prefs.getDefaultBranch("").getCharPref(aName); michael@0: } michael@0: catch (e) { michael@0: } michael@0: return aDefaultValue; michael@0: }, michael@0: michael@0: /** michael@0: * Gets a string preference. michael@0: * michael@0: * @param aName michael@0: * The name of the preference michael@0: * @param aDefaultValue michael@0: * A value to return if the preference does not exist michael@0: * @return the value of the preference or aDefaultValue if there is none michael@0: */ michael@0: getCharPref: function Prefs_getCharPref(aName, aDefaultValue) { michael@0: try { michael@0: return Services.prefs.getCharPref(aName); michael@0: } michael@0: catch (e) { michael@0: } michael@0: return aDefaultValue; michael@0: }, michael@0: michael@0: /** michael@0: * Gets a complex preference. michael@0: * michael@0: * @param aName michael@0: * The name of the preference michael@0: * @param aType michael@0: * The interface type of the preference michael@0: * @param aDefaultValue michael@0: * A value to return if the preference does not exist michael@0: * @return the value of the preference or aDefaultValue if there is none michael@0: */ michael@0: getComplexValue: function Prefs_getComplexValue(aName, aType, aDefaultValue) { michael@0: try { michael@0: return Services.prefs.getComplexValue(aName, aType).data; michael@0: } michael@0: catch (e) { michael@0: } michael@0: return aDefaultValue; michael@0: }, michael@0: michael@0: /** michael@0: * Gets a boolean preference. michael@0: * michael@0: * @param aName michael@0: * The name of the preference michael@0: * @param aDefaultValue michael@0: * A value to return if the preference does not exist michael@0: * @return the value of the preference or aDefaultValue if there is none michael@0: */ michael@0: getBoolPref: function Prefs_getBoolPref(aName, aDefaultValue) { michael@0: try { michael@0: return Services.prefs.getBoolPref(aName); michael@0: } michael@0: catch (e) { michael@0: } michael@0: return aDefaultValue; michael@0: }, michael@0: michael@0: /** michael@0: * Gets an integer preference. michael@0: * michael@0: * @param aName michael@0: * The name of the preference michael@0: * @param defaultValue michael@0: * A value to return if the preference does not exist michael@0: * @return the value of the preference or defaultValue if there is none michael@0: */ michael@0: getIntPref: function Prefs_getIntPref(aName, defaultValue) { michael@0: try { michael@0: return Services.prefs.getIntPref(aName); michael@0: } michael@0: catch (e) { michael@0: } michael@0: return defaultValue; michael@0: }, michael@0: michael@0: /** michael@0: * Clears a preference if it has a user value michael@0: * michael@0: * @param aName michael@0: * The name of the preference michael@0: */ michael@0: clearUserPref: function Prefs_clearUserPref(aName) { michael@0: if (Services.prefs.prefHasUserValue(aName)) michael@0: Services.prefs.clearUserPref(aName); michael@0: } michael@0: } michael@0: michael@0: // Helper function to compare JSON saved version of the directory state michael@0: // with the new state returned by getInstallLocationStates() michael@0: // Structure is: ordered array of {'name':?, 'addons': {addonID: {'descriptor':?, 'mtime':?} ...}} michael@0: function directoryStateDiffers(aState, aCache) michael@0: { michael@0: // check equality of an object full of addons; fortunately we can destroy the 'aOld' object michael@0: function addonsMismatch(aNew, aOld) { michael@0: for (let [id, val] of aNew) { michael@0: if (!id in aOld) michael@0: return true; michael@0: if (val.descriptor != aOld[id].descriptor || michael@0: val.mtime != aOld[id].mtime) michael@0: return true; michael@0: delete aOld[id]; michael@0: } michael@0: // make sure aOld doesn't have any extra entries michael@0: for (let id in aOld) michael@0: return true; michael@0: return false; michael@0: } michael@0: michael@0: if (!aCache) michael@0: return true; michael@0: try { michael@0: let old = JSON.parse(aCache); michael@0: if (aState.length != old.length) michael@0: return true; michael@0: for (let i = 0; i < aState.length; i++) { michael@0: // conveniently, any missing fields would require a 'true' return, which is michael@0: // handled by our catch wrapper michael@0: if (aState[i].name != old[i].name) michael@0: return true; michael@0: if (addonsMismatch(aState[i].addons, old[i].addons)) michael@0: return true; michael@0: } michael@0: } michael@0: catch (e) { michael@0: return true; michael@0: } michael@0: return false; michael@0: } michael@0: michael@0: /** michael@0: * Wraps a function in an exception handler to protect against exceptions inside callbacks michael@0: * @param aFunction function(args...) michael@0: * @return function(args...), a function that takes the same arguments as aFunction michael@0: * and returns the same result unless aFunction throws, in which case it logs michael@0: * a warning and returns undefined. michael@0: */ michael@0: function makeSafe(aFunction) { michael@0: return function(...aArgs) { michael@0: try { michael@0: return aFunction(...aArgs); michael@0: } michael@0: catch(ex) { michael@0: logger.warn("XPIProvider callback failed", ex); michael@0: } michael@0: return undefined; michael@0: } michael@0: } michael@0: michael@0: this.XPIProvider = { michael@0: // An array of known install locations michael@0: installLocations: null, michael@0: // A dictionary of known install locations by name michael@0: installLocationsByName: null, michael@0: // An array of currently active AddonInstalls michael@0: installs: null, michael@0: // The default skin for the application michael@0: defaultSkin: "classic/1.0", michael@0: // The current skin used by the application michael@0: currentSkin: null, michael@0: // The selected skin to be used by the application when it is restarted. This michael@0: // will be the same as currentSkin when it is the skin to be used when the michael@0: // application is restarted michael@0: selectedSkin: null, michael@0: // The value of the minCompatibleAppVersion preference michael@0: minCompatibleAppVersion: null, michael@0: // The value of the minCompatiblePlatformVersion preference michael@0: minCompatiblePlatformVersion: null, michael@0: // A dictionary of the file descriptors for bootstrappable add-ons by ID michael@0: bootstrappedAddons: {}, michael@0: // A dictionary of JS scopes of loaded bootstrappable add-ons by ID michael@0: bootstrapScopes: {}, michael@0: // True if the platform could have activated extensions michael@0: extensionsActive: false, michael@0: // File / directory state of installed add-ons michael@0: installStates: [], michael@0: // True if all of the add-ons found during startup were installed in the michael@0: // application install location michael@0: allAppGlobal: true, michael@0: // A string listing the enabled add-ons for annotating crash reports michael@0: enabledAddons: null, michael@0: // An array of add-on IDs of add-ons that were inactive during startup michael@0: inactiveAddonIDs: [], michael@0: // Keep track of startup phases for telemetry michael@0: runPhase: XPI_STARTING, michael@0: // Keep track of the newest file in each add-on, in case we want to michael@0: // report it to telemetry. michael@0: _mostRecentlyModifiedFile: {}, michael@0: // Per-addon telemetry information michael@0: _telemetryDetails: {}, michael@0: // Experiments are disabled by default. Track ones that are locally enabled. michael@0: _enabledExperiments: null, michael@0: michael@0: /* michael@0: * Set a value in the telemetry hash for a given ID michael@0: */ michael@0: setTelemetry: function XPI_setTelemetry(aId, aName, aValue) { michael@0: if (!this._telemetryDetails[aId]) michael@0: this._telemetryDetails[aId] = {}; michael@0: this._telemetryDetails[aId][aName] = aValue; michael@0: }, michael@0: michael@0: // Keep track of in-progress operations that support cancel() michael@0: _inProgress: new Set(), michael@0: michael@0: doing: function XPI_doing(aCancellable) { michael@0: this._inProgress.add(aCancellable); michael@0: }, michael@0: michael@0: done: function XPI_done(aCancellable) { michael@0: return this._inProgress.delete(aCancellable); michael@0: }, michael@0: michael@0: cancelAll: function XPI_cancelAll() { michael@0: // Cancelling one may alter _inProgress, so restart the iterator after each michael@0: while (this._inProgress.size > 0) { michael@0: for (let c of this._inProgress) { michael@0: try { michael@0: c.cancel(); michael@0: } michael@0: catch (e) { michael@0: logger.warn("Cancel failed", e); michael@0: } michael@0: this._inProgress.delete(c); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Adds or updates a URI mapping for an Addon.id. michael@0: * michael@0: * Mappings should not be removed at any point. This is so that the mappings michael@0: * will be still valid after an add-on gets disabled or uninstalled, as michael@0: * consumers may still have URIs of (leaked) resources they want to map. michael@0: */ michael@0: _addURIMapping: function XPI__addURIMapping(aID, aFile) { michael@0: try { michael@0: // Always use our own mechanics instead of nsIIOService.newFileURI, so michael@0: // that we can be sure to map things as we want them mapped. michael@0: let uri = this._resolveURIToFile(getURIForResourceInFile(aFile, ".")); michael@0: if (!uri) { michael@0: throw new Error("Cannot resolve"); michael@0: } michael@0: this._ensureURIMappings(); michael@0: this._uriMappings[aID] = uri.spec; michael@0: } michael@0: catch (ex) { michael@0: logger.warn("Failed to add URI mapping", ex); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Ensures that the URI to Addon mappings are available. michael@0: * michael@0: * The function will add mappings for all non-bootstrapped but enabled michael@0: * add-ons. michael@0: * Bootstrapped add-on mappings will be added directly when the bootstrap michael@0: * scope get loaded. (See XPIProvider._addURIMapping() and callers) michael@0: */ michael@0: _ensureURIMappings: function XPI__ensureURIMappings() { michael@0: if (this._uriMappings) { michael@0: return; michael@0: } michael@0: // XXX Convert to Map(), once it gets stable with stable iterators michael@0: this._uriMappings = Object.create(null); michael@0: michael@0: // XXX Convert to Set(), once it gets stable with stable iterators michael@0: let enabled = Object.create(null); michael@0: let enabledAddons = this.enabledAddons || ""; michael@0: for (let a of enabledAddons.split(",")) { michael@0: a = decodeURIComponent(a.split(":")[0]); michael@0: enabled[a] = null; michael@0: } michael@0: michael@0: let cache = JSON.parse(Prefs.getCharPref(PREF_INSTALL_CACHE, "[]")); michael@0: for (let loc of cache) { michael@0: for (let [id, val] in Iterator(loc.addons)) { michael@0: if (!(id in enabled)) { michael@0: continue; michael@0: } michael@0: let file = new nsIFile(val.descriptor); michael@0: let spec = Services.io.newFileURI(file).spec; michael@0: this._uriMappings[id] = spec; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Resolve a URI back to physical file. michael@0: * michael@0: * Of course, this works only for URIs pointing to local resources. michael@0: * michael@0: * @param aURI michael@0: * URI to resolve michael@0: * @return michael@0: * resolved nsIFileURL michael@0: */ michael@0: _resolveURIToFile: function XPI__resolveURIToFile(aURI) { michael@0: switch (aURI.scheme) { michael@0: case "jar": michael@0: case "file": michael@0: if (aURI instanceof Ci.nsIJARURI) { michael@0: return this._resolveURIToFile(aURI.JARFile); michael@0: } michael@0: return aURI; michael@0: michael@0: case "chrome": michael@0: aURI = ChromeRegistry.convertChromeURL(aURI); michael@0: return this._resolveURIToFile(aURI); michael@0: michael@0: case "resource": michael@0: aURI = Services.io.newURI(ResProtocolHandler.resolveURI(aURI), null, michael@0: null); michael@0: return this._resolveURIToFile(aURI); michael@0: michael@0: case "view-source": michael@0: aURI = Services.io.newURI(aURI.path, null, null); michael@0: return this._resolveURIToFile(aURI); michael@0: michael@0: case "about": michael@0: if (aURI.spec == "about:blank") { michael@0: // Do not attempt to map about:blank michael@0: return null; michael@0: } michael@0: michael@0: let chan; michael@0: try { michael@0: chan = Services.io.newChannelFromURI(aURI); michael@0: } michael@0: catch (ex) { michael@0: return null; michael@0: } michael@0: // Avoid looping michael@0: if (chan.URI.equals(aURI)) { michael@0: return null; michael@0: } michael@0: // We want to clone the channel URI to avoid accidentially keeping michael@0: // unnecessary references to the channel or implementation details michael@0: // around. michael@0: return this._resolveURIToFile(chan.URI.clone()); michael@0: michael@0: default: michael@0: return null; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Starts the XPI provider initializes the install locations and prefs. michael@0: * michael@0: * @param aAppChanged michael@0: * A tri-state value. Undefined means the current profile was created michael@0: * for this session, true means the profile already existed but was michael@0: * last used with an application with a different version number, michael@0: * false means that the profile was last used by this version of the michael@0: * application. michael@0: * @param aOldAppVersion michael@0: * The version of the application last run with this profile or null michael@0: * if it is a new profile or the version is unknown michael@0: * @param aOldPlatformVersion michael@0: * The version of the platform last run with this profile or null michael@0: * if it is a new profile or the version is unknown michael@0: */ michael@0: startup: function XPI_startup(aAppChanged, aOldAppVersion, aOldPlatformVersion) { michael@0: function addDirectoryInstallLocation(aName, aKey, aPaths, aScope, aLocked) { michael@0: try { michael@0: var dir = FileUtils.getDir(aKey, aPaths); michael@0: } michael@0: catch (e) { michael@0: // Some directories aren't defined on some platforms, ignore them michael@0: logger.debug("Skipping unavailable install location " + aName); michael@0: return; michael@0: } michael@0: michael@0: try { michael@0: var location = new DirectoryInstallLocation(aName, dir, aScope, aLocked); michael@0: } michael@0: catch (e) { michael@0: logger.warn("Failed to add directory install location " + aName, e); michael@0: return; michael@0: } michael@0: michael@0: XPIProvider.installLocations.push(location); michael@0: XPIProvider.installLocationsByName[location.name] = location; michael@0: } michael@0: michael@0: function addRegistryInstallLocation(aName, aRootkey, aScope) { michael@0: try { michael@0: var location = new WinRegInstallLocation(aName, aRootkey, aScope); michael@0: } michael@0: catch (e) { michael@0: logger.warn("Failed to add registry install location " + aName, e); michael@0: return; michael@0: } michael@0: michael@0: XPIProvider.installLocations.push(location); michael@0: XPIProvider.installLocationsByName[location.name] = location; michael@0: } michael@0: michael@0: try { michael@0: AddonManagerPrivate.recordTimestamp("XPI_startup_begin"); michael@0: michael@0: logger.debug("startup"); michael@0: this.runPhase = XPI_STARTING; michael@0: this.installs = []; michael@0: this.installLocations = []; michael@0: this.installLocationsByName = {}; michael@0: // Hook for tests to detect when saving database at shutdown time fails michael@0: this._shutdownError = null; michael@0: // Clear this at startup for xpcshell test restarts michael@0: this._telemetryDetails = {}; michael@0: // Clear the set of enabled experiments (experiments disabled by default). michael@0: this._enabledExperiments = new Set(); michael@0: // Register our details structure with AddonManager michael@0: AddonManagerPrivate.setTelemetryDetails("XPI", this._telemetryDetails); michael@0: michael@0: let hasRegistry = ("nsIWindowsRegKey" in Ci); michael@0: michael@0: let enabledScopes = Prefs.getIntPref(PREF_EM_ENABLED_SCOPES, michael@0: AddonManager.SCOPE_ALL); michael@0: michael@0: // These must be in order of priority for processFileChanges etc. to work michael@0: if (enabledScopes & AddonManager.SCOPE_SYSTEM) { michael@0: if (hasRegistry) { michael@0: addRegistryInstallLocation("winreg-app-global", michael@0: Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE, michael@0: AddonManager.SCOPE_SYSTEM); michael@0: } michael@0: addDirectoryInstallLocation(KEY_APP_SYSTEM_LOCAL, "XRESysLExtPD", michael@0: [Services.appinfo.ID], michael@0: AddonManager.SCOPE_SYSTEM, true); michael@0: addDirectoryInstallLocation(KEY_APP_SYSTEM_SHARE, "XRESysSExtPD", michael@0: [Services.appinfo.ID], michael@0: AddonManager.SCOPE_SYSTEM, true); michael@0: } michael@0: michael@0: if (enabledScopes & AddonManager.SCOPE_APPLICATION) { michael@0: addDirectoryInstallLocation(KEY_APP_GLOBAL, KEY_APPDIR, michael@0: [DIR_EXTENSIONS], michael@0: AddonManager.SCOPE_APPLICATION, true); michael@0: } michael@0: michael@0: if (enabledScopes & AddonManager.SCOPE_USER) { michael@0: if (hasRegistry) { michael@0: addRegistryInstallLocation("winreg-app-user", michael@0: Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, michael@0: AddonManager.SCOPE_USER); michael@0: } michael@0: addDirectoryInstallLocation(KEY_APP_SYSTEM_USER, "XREUSysExt", michael@0: [Services.appinfo.ID], michael@0: AddonManager.SCOPE_USER, true); michael@0: } michael@0: michael@0: // The profile location is always enabled michael@0: addDirectoryInstallLocation(KEY_APP_PROFILE, KEY_PROFILEDIR, michael@0: [DIR_EXTENSIONS], michael@0: AddonManager.SCOPE_PROFILE, false); michael@0: michael@0: this.defaultSkin = Prefs.getDefaultCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN, michael@0: "classic/1.0"); michael@0: this.currentSkin = Prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN, michael@0: this.defaultSkin); michael@0: this.selectedSkin = this.currentSkin; michael@0: this.applyThemeChange(); michael@0: michael@0: this.minCompatibleAppVersion = Prefs.getCharPref(PREF_EM_MIN_COMPAT_APP_VERSION, michael@0: null); michael@0: this.minCompatiblePlatformVersion = Prefs.getCharPref(PREF_EM_MIN_COMPAT_PLATFORM_VERSION, michael@0: null); michael@0: this.enabledAddons = ""; michael@0: michael@0: Services.prefs.addObserver(PREF_EM_MIN_COMPAT_APP_VERSION, this, false); michael@0: Services.prefs.addObserver(PREF_EM_MIN_COMPAT_PLATFORM_VERSION, this, false); michael@0: Services.obs.addObserver(this, NOTIFICATION_FLUSH_PERMISSIONS, false); michael@0: michael@0: try { michael@0: BrowserToolboxProcess.on("connectionchange", michael@0: this.onDebugConnectionChange.bind(this)); michael@0: } michael@0: catch (e) { michael@0: // BrowserToolboxProcess is not available in all applications michael@0: } michael@0: michael@0: let flushCaches = this.checkForChanges(aAppChanged, aOldAppVersion, michael@0: aOldPlatformVersion); michael@0: michael@0: // Changes to installed extensions may have changed which theme is selected michael@0: this.applyThemeChange(); michael@0: michael@0: // If the application has been upgraded and there are add-ons outside the michael@0: // application directory then we may need to synchronize compatibility michael@0: // information but only if the mismatch UI isn't disabled michael@0: if (aAppChanged && !this.allAppGlobal && michael@0: Prefs.getBoolPref(PREF_EM_SHOW_MISMATCH_UI, true)) { michael@0: this.showUpgradeUI(); michael@0: flushCaches = true; michael@0: } michael@0: else if (aAppChanged === undefined) { michael@0: // For new profiles we will never need to show the add-on selection UI michael@0: Services.prefs.setBoolPref(PREF_SHOWN_SELECTION_UI, true); michael@0: } michael@0: michael@0: if (flushCaches) { michael@0: flushStartupCache(); michael@0: michael@0: // UI displayed early in startup (like the compatibility UI) may have michael@0: // caused us to cache parts of the skin or locale in memory. These must michael@0: // be flushed to allow extension provided skins and locales to take full michael@0: // effect michael@0: Services.obs.notifyObservers(null, "chrome-flush-skin-caches", null); michael@0: Services.obs.notifyObservers(null, "chrome-flush-caches", null); michael@0: } michael@0: michael@0: this.enabledAddons = Prefs.getCharPref(PREF_EM_ENABLED_ADDONS, ""); michael@0: michael@0: // Invalidate the URI mappings now that |enabledAddons| was updated. michael@0: // |_ensureMappings()| will re-create the mappings when needed. michael@0: delete this._uriMappings; michael@0: michael@0: if ("nsICrashReporter" in Ci && michael@0: Services.appinfo instanceof Ci.nsICrashReporter) { michael@0: // Annotate the crash report with relevant add-on information. michael@0: try { michael@0: Services.appinfo.annotateCrashReport("Theme", this.currentSkin); michael@0: } catch (e) { } michael@0: try { michael@0: Services.appinfo.annotateCrashReport("EMCheckCompatibility", michael@0: AddonManager.checkCompatibility); michael@0: } catch (e) { } michael@0: this.addAddonsToCrashReporter(); michael@0: } michael@0: michael@0: try { michael@0: AddonManagerPrivate.recordTimestamp("XPI_bootstrap_addons_begin"); michael@0: for (let id in this.bootstrappedAddons) { michael@0: try { michael@0: let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); michael@0: file.persistentDescriptor = this.bootstrappedAddons[id].descriptor; michael@0: let reason = BOOTSTRAP_REASONS.APP_STARTUP; michael@0: // Eventually set INSTALLED reason when a bootstrap addon michael@0: // is dropped in profile folder and automatically installed michael@0: if (AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_INSTALLED) michael@0: .indexOf(id) !== -1) michael@0: reason = BOOTSTRAP_REASONS.ADDON_INSTALL; michael@0: this.callBootstrapMethod(id, this.bootstrappedAddons[id].version, michael@0: this.bootstrappedAddons[id].type, file, michael@0: "startup", reason); michael@0: } michael@0: catch (e) { michael@0: logger.error("Failed to load bootstrap addon " + id + " from " + michael@0: this.bootstrappedAddons[id].descriptor, e); michael@0: } michael@0: } michael@0: AddonManagerPrivate.recordTimestamp("XPI_bootstrap_addons_end"); michael@0: } michael@0: catch (e) { michael@0: logger.error("bootstrap startup failed", e); michael@0: AddonManagerPrivate.recordException("XPI-BOOTSTRAP", "startup failed", e); michael@0: } michael@0: michael@0: // Let these shutdown a little earlier when they still have access to most michael@0: // of XPCOM michael@0: Services.obs.addObserver({ michael@0: observe: function shutdownObserver(aSubject, aTopic, aData) { michael@0: for (let id in XPIProvider.bootstrappedAddons) { michael@0: let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); michael@0: file.persistentDescriptor = XPIProvider.bootstrappedAddons[id].descriptor; michael@0: XPIProvider.callBootstrapMethod(id, XPIProvider.bootstrappedAddons[id].version, michael@0: XPIProvider.bootstrappedAddons[id].type, file, "shutdown", michael@0: BOOTSTRAP_REASONS.APP_SHUTDOWN); michael@0: } michael@0: Services.obs.removeObserver(this, "quit-application-granted"); michael@0: } michael@0: }, "quit-application-granted", false); michael@0: michael@0: // Detect final-ui-startup for telemetry reporting michael@0: Services.obs.addObserver({ michael@0: observe: function uiStartupObserver(aSubject, aTopic, aData) { michael@0: AddonManagerPrivate.recordTimestamp("XPI_finalUIStartup"); michael@0: XPIProvider.runPhase = XPI_AFTER_UI_STARTUP; michael@0: Services.obs.removeObserver(this, "final-ui-startup"); michael@0: } michael@0: }, "final-ui-startup", false); michael@0: michael@0: AddonManagerPrivate.recordTimestamp("XPI_startup_end"); michael@0: michael@0: this.extensionsActive = true; michael@0: this.runPhase = XPI_BEFORE_UI_STARTUP; michael@0: } michael@0: catch (e) { michael@0: logger.error("startup failed", e); michael@0: AddonManagerPrivate.recordException("XPI", "startup failed", e); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Shuts down the database and releases all references. michael@0: * Return: Promise{integer} resolves / rejects with the result of michael@0: * flushing the XPI Database if it was loaded, michael@0: * 0 otherwise. michael@0: */ michael@0: shutdown: function XPI_shutdown() { michael@0: logger.debug("shutdown"); michael@0: michael@0: // Stop anything we were doing asynchronously michael@0: this.cancelAll(); michael@0: michael@0: this.bootstrappedAddons = {}; michael@0: this.bootstrapScopes = {}; michael@0: this.enabledAddons = null; michael@0: this.allAppGlobal = true; michael@0: michael@0: this.inactiveAddonIDs = []; michael@0: michael@0: // If there are pending operations then we must update the list of active michael@0: // add-ons michael@0: if (Prefs.getBoolPref(PREF_PENDING_OPERATIONS, false)) { michael@0: XPIDatabase.updateActiveAddons(); michael@0: Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, michael@0: !XPIDatabase.writeAddonsList()); michael@0: } michael@0: michael@0: this.installs = null; michael@0: this.installLocations = null; michael@0: this.installLocationsByName = null; michael@0: michael@0: // This is needed to allow xpcshell tests to simulate a restart michael@0: this.extensionsActive = false; michael@0: michael@0: // Remove URI mappings again michael@0: delete this._uriMappings; michael@0: michael@0: if (gLazyObjectsLoaded) { michael@0: let done = XPIDatabase.shutdown(); michael@0: done.then( michael@0: ret => { michael@0: logger.debug("Notifying XPI shutdown observers"); michael@0: Services.obs.notifyObservers(null, "xpi-provider-shutdown", null); michael@0: }, michael@0: err => { michael@0: logger.debug("Notifying XPI shutdown observers"); michael@0: this._shutdownError = err; michael@0: Services.obs.notifyObservers(null, "xpi-provider-shutdown", err); michael@0: } michael@0: ); michael@0: return done; michael@0: } michael@0: else { michael@0: logger.debug("Notifying XPI shutdown observers"); michael@0: Services.obs.notifyObservers(null, "xpi-provider-shutdown", null); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Applies any pending theme change to the preferences. michael@0: */ michael@0: applyThemeChange: function XPI_applyThemeChange() { michael@0: if (!Prefs.getBoolPref(PREF_DSS_SWITCHPENDING, false)) michael@0: return; michael@0: michael@0: // Tell the Chrome Registry which Skin to select michael@0: try { michael@0: this.selectedSkin = Prefs.getCharPref(PREF_DSS_SKIN_TO_SELECT); michael@0: Services.prefs.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN, michael@0: this.selectedSkin); michael@0: Services.prefs.clearUserPref(PREF_DSS_SKIN_TO_SELECT); michael@0: logger.debug("Changed skin to " + this.selectedSkin); michael@0: this.currentSkin = this.selectedSkin; michael@0: } michael@0: catch (e) { michael@0: logger.error("Error applying theme change", e); michael@0: } michael@0: Services.prefs.clearUserPref(PREF_DSS_SWITCHPENDING); michael@0: }, michael@0: michael@0: /** michael@0: * Shows the "Compatibility Updates" UI michael@0: */ michael@0: showUpgradeUI: function XPI_showUpgradeUI() { michael@0: // Flip a flag to indicate that we interrupted startup with an interactive prompt michael@0: Services.startup.interrupted = true; michael@0: michael@0: if (!Prefs.getBoolPref(PREF_SHOWN_SELECTION_UI, false)) { michael@0: // This *must* be modal as it has to block startup. michael@0: var features = "chrome,centerscreen,dialog,titlebar,modal"; michael@0: Services.ww.openWindow(null, URI_EXTENSION_SELECT_DIALOG, "", features, null); michael@0: Services.prefs.setBoolPref(PREF_SHOWN_SELECTION_UI, true); michael@0: } michael@0: else { michael@0: var variant = Cc["@mozilla.org/variant;1"]. michael@0: createInstance(Ci.nsIWritableVariant); michael@0: variant.setFromVariant(this.inactiveAddonIDs); michael@0: michael@0: // This *must* be modal as it has to block startup. michael@0: var features = "chrome,centerscreen,dialog,titlebar,modal"; michael@0: var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"]. michael@0: getService(Ci.nsIWindowWatcher); michael@0: ww.openWindow(null, URI_EXTENSION_UPDATE_DIALOG, "", features, variant); michael@0: } michael@0: michael@0: // Ensure any changes to the add-ons list are flushed to disk michael@0: Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, michael@0: !XPIDatabase.writeAddonsList()); michael@0: }, michael@0: michael@0: /** michael@0: * Persists changes to XPIProvider.bootstrappedAddons to its store (a pref). michael@0: */ michael@0: persistBootstrappedAddons: function XPI_persistBootstrappedAddons() { michael@0: // Experiments are disabled upon app load, so don't persist references. michael@0: let filtered = {}; michael@0: for (let id in this.bootstrappedAddons) { michael@0: let entry = this.bootstrappedAddons[id]; michael@0: if (entry.type == "experiment") { michael@0: continue; michael@0: } michael@0: michael@0: filtered[id] = entry; michael@0: } michael@0: michael@0: Services.prefs.setCharPref(PREF_BOOTSTRAP_ADDONS, michael@0: JSON.stringify(filtered)); michael@0: }, michael@0: michael@0: /** michael@0: * Adds a list of currently active add-ons to the next crash report. michael@0: */ michael@0: addAddonsToCrashReporter: function XPI_addAddonsToCrashReporter() { michael@0: if (!("nsICrashReporter" in Ci) || michael@0: !(Services.appinfo instanceof Ci.nsICrashReporter)) michael@0: return; michael@0: michael@0: // In safe mode no add-ons are loaded so we should not include them in the michael@0: // crash report michael@0: if (Services.appinfo.inSafeMode) michael@0: return; michael@0: michael@0: let data = this.enabledAddons; michael@0: for (let id in this.bootstrappedAddons) { michael@0: data += (data ? "," : "") + encodeURIComponent(id) + ":" + michael@0: encodeURIComponent(this.bootstrappedAddons[id].version); michael@0: } michael@0: michael@0: try { michael@0: Services.appinfo.annotateCrashReport("Add-ons", data); michael@0: } michael@0: catch (e) { } michael@0: michael@0: Cu.import("resource://gre/modules/TelemetryPing.jsm", {}).TelemetryPing.setAddOns(data); michael@0: }, michael@0: michael@0: /** michael@0: * Gets the add-on states for an install location. michael@0: * This function may be expensive because of the recursiveLastModifiedTime call. michael@0: * michael@0: * @param location michael@0: * The install location to retrieve the add-on states for michael@0: * @return a dictionary mapping add-on IDs to objects with a descriptor michael@0: * property which contains the add-ons dir/file descriptor and an michael@0: * mtime property which contains the add-on's last modified time as michael@0: * the number of milliseconds since the epoch. michael@0: */ michael@0: getAddonStates: function XPI_getAddonStates(aLocation) { michael@0: let addonStates = {}; michael@0: for (let file of aLocation.addonLocations) { michael@0: let scanStarted = Date.now(); michael@0: let id = aLocation.getIDForLocation(file); michael@0: let unpacked = 0; michael@0: let [modFile, modTime, items] = recursiveLastModifiedTime(file); michael@0: addonStates[id] = { michael@0: descriptor: file.persistentDescriptor, michael@0: mtime: modTime michael@0: }; michael@0: try { michael@0: // get the install.rdf update time, if any michael@0: file.append(FILE_INSTALL_MANIFEST); michael@0: let rdfTime = file.lastModifiedTime; michael@0: addonStates[id].rdfTime = rdfTime; michael@0: unpacked = 1; michael@0: } michael@0: catch (e) { } michael@0: this._mostRecentlyModifiedFile[id] = modFile; michael@0: this.setTelemetry(id, "unpacked", unpacked); michael@0: this.setTelemetry(id, "location", aLocation.name); michael@0: this.setTelemetry(id, "scan_MS", Date.now() - scanStarted); michael@0: this.setTelemetry(id, "scan_items", items); michael@0: } michael@0: michael@0: return addonStates; michael@0: }, michael@0: michael@0: /** michael@0: * Gets an array of install location states which uniquely describes all michael@0: * installed add-ons with the add-on's InstallLocation name and last modified michael@0: * time. This function may be expensive because of the getAddonStates() call. michael@0: * michael@0: * @return an array of add-on states for each install location. Each state michael@0: * is an object with a name property holding the location's name and michael@0: * an addons property holding the add-on states for the location michael@0: */ michael@0: getInstallLocationStates: function XPI_getInstallLocationStates() { michael@0: let states = []; michael@0: this.installLocations.forEach(function(aLocation) { michael@0: let addons = aLocation.addonLocations; michael@0: if (addons.length == 0) michael@0: return; michael@0: michael@0: let locationState = { michael@0: name: aLocation.name, michael@0: addons: this.getAddonStates(aLocation) michael@0: }; michael@0: michael@0: states.push(locationState); michael@0: }, this); michael@0: return states; michael@0: }, michael@0: michael@0: /** michael@0: * Check the staging directories of install locations for any add-ons to be michael@0: * installed or add-ons to be uninstalled. michael@0: * michael@0: * @param aManifests michael@0: * A dictionary to add detected install manifests to for the purpose michael@0: * of passing through updated compatibility information michael@0: * @return true if an add-on was installed or uninstalled michael@0: */ michael@0: processPendingFileChanges: function XPI_processPendingFileChanges(aManifests) { michael@0: let changed = false; michael@0: this.installLocations.forEach(function(aLocation) { michael@0: aManifests[aLocation.name] = {}; michael@0: // We can't install or uninstall anything in locked locations michael@0: if (aLocation.locked) michael@0: return; michael@0: michael@0: let stagedXPIDir = aLocation.getXPIStagingDir(); michael@0: let stagingDir = aLocation.getStagingDir(); michael@0: michael@0: if (stagedXPIDir.exists() && stagedXPIDir.isDirectory()) { michael@0: let entries = stagedXPIDir.directoryEntries michael@0: .QueryInterface(Ci.nsIDirectoryEnumerator); michael@0: while (entries.hasMoreElements()) { michael@0: let stageDirEntry = entries.nextFile; michael@0: michael@0: if (!stageDirEntry.isDirectory()) { michael@0: logger.warn("Ignoring file in XPI staging directory: " + stageDirEntry.path); michael@0: continue; michael@0: } michael@0: michael@0: // Find the last added XPI file in the directory michael@0: let stagedXPI = null; michael@0: var xpiEntries = stageDirEntry.directoryEntries michael@0: .QueryInterface(Ci.nsIDirectoryEnumerator); michael@0: while (xpiEntries.hasMoreElements()) { michael@0: let file = xpiEntries.nextFile; michael@0: if (file.isDirectory()) michael@0: continue; michael@0: michael@0: let extension = file.leafName; michael@0: extension = extension.substring(extension.length - 4); michael@0: michael@0: if (extension != ".xpi" && extension != ".jar") michael@0: continue; michael@0: michael@0: stagedXPI = file; michael@0: } michael@0: xpiEntries.close(); michael@0: michael@0: if (!stagedXPI) michael@0: continue; michael@0: michael@0: let addon = null; michael@0: try { michael@0: addon = loadManifestFromZipFile(stagedXPI); michael@0: } michael@0: catch (e) { michael@0: logger.error("Unable to read add-on manifest from " + stagedXPI.path, e); michael@0: continue; michael@0: } michael@0: michael@0: logger.debug("Migrating staged install of " + addon.id + " in " + aLocation.name); michael@0: michael@0: if (addon.unpack || Prefs.getBoolPref(PREF_XPI_UNPACK, false)) { michael@0: let targetDir = stagingDir.clone(); michael@0: targetDir.append(addon.id); michael@0: try { michael@0: targetDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); michael@0: } michael@0: catch (e) { michael@0: logger.error("Failed to create staging directory for add-on " + addon.id, e); michael@0: continue; michael@0: } michael@0: michael@0: try { michael@0: ZipUtils.extractFiles(stagedXPI, targetDir); michael@0: } michael@0: catch (e) { michael@0: logger.error("Failed to extract staged XPI for add-on " + addon.id + " in " + michael@0: aLocation.name, e); michael@0: } michael@0: } michael@0: else { michael@0: try { michael@0: stagedXPI.moveTo(stagingDir, addon.id + ".xpi"); michael@0: } michael@0: catch (e) { michael@0: logger.error("Failed to move staged XPI for add-on " + addon.id + " in " + michael@0: aLocation.name, e); michael@0: } michael@0: } michael@0: } michael@0: entries.close(); michael@0: } michael@0: michael@0: if (stagedXPIDir.exists()) { michael@0: try { michael@0: recursiveRemove(stagedXPIDir); michael@0: } michael@0: catch (e) { michael@0: // Non-critical, just saves some perf on startup if we clean this up. michael@0: logger.debug("Error removing XPI staging dir " + stagedXPIDir.path, e); michael@0: } michael@0: } michael@0: michael@0: try { michael@0: if (!stagingDir || !stagingDir.exists() || !stagingDir.isDirectory()) michael@0: return; michael@0: } michael@0: catch (e) { michael@0: logger.warn("Failed to find staging directory", e); michael@0: return; michael@0: } michael@0: michael@0: let seenFiles = []; michael@0: // Use a snapshot of the directory contents to avoid possible issues with michael@0: // iterating over a directory while removing files from it (the YAFFS2 michael@0: // embedded filesystem has this issue, see bug 772238), and to remove michael@0: // normal files before their resource forks on OSX (see bug 733436). michael@0: let stagingDirEntries = getDirectoryEntries(stagingDir, true); michael@0: for (let stageDirEntry of stagingDirEntries) { michael@0: let id = stageDirEntry.leafName; michael@0: michael@0: let isDir; michael@0: try { michael@0: isDir = stageDirEntry.isDirectory(); michael@0: } michael@0: catch (e if e.result == Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) { michael@0: // If the file has already gone away then don't worry about it, this michael@0: // can happen on OSX where the resource fork is automatically moved michael@0: // with the data fork for the file. See bug 733436. michael@0: continue; michael@0: } michael@0: michael@0: if (!isDir) { michael@0: if (id.substring(id.length - 4).toLowerCase() == ".xpi") { michael@0: id = id.substring(0, id.length - 4); michael@0: } michael@0: else { michael@0: if (id.substring(id.length - 5).toLowerCase() != ".json") { michael@0: logger.warn("Ignoring file: " + stageDirEntry.path); michael@0: seenFiles.push(stageDirEntry.leafName); michael@0: } michael@0: continue; michael@0: } michael@0: } michael@0: michael@0: // Check that the directory's name is a valid ID. michael@0: if (!gIDTest.test(id)) { michael@0: logger.warn("Ignoring directory whose name is not a valid add-on ID: " + michael@0: stageDirEntry.path); michael@0: seenFiles.push(stageDirEntry.leafName); michael@0: continue; michael@0: } michael@0: michael@0: changed = true; michael@0: michael@0: if (isDir) { michael@0: // Check if the directory contains an install manifest. michael@0: let manifest = stageDirEntry.clone(); michael@0: manifest.append(FILE_INSTALL_MANIFEST); michael@0: michael@0: // If the install manifest doesn't exist uninstall this add-on in this michael@0: // install location. michael@0: if (!manifest.exists()) { michael@0: logger.debug("Processing uninstall of " + id + " in " + aLocation.name); michael@0: try { michael@0: aLocation.uninstallAddon(id); michael@0: seenFiles.push(stageDirEntry.leafName); michael@0: } michael@0: catch (e) { michael@0: logger.error("Failed to uninstall add-on " + id + " in " + aLocation.name, e); michael@0: } michael@0: // The file check later will spot the removal and cleanup the database michael@0: continue; michael@0: } michael@0: } michael@0: michael@0: aManifests[aLocation.name][id] = null; michael@0: let existingAddonID = id; michael@0: michael@0: let jsonfile = stagingDir.clone(); michael@0: jsonfile.append(id + ".json"); michael@0: michael@0: try { michael@0: aManifests[aLocation.name][id] = loadManifestFromFile(stageDirEntry); michael@0: } michael@0: catch (e) { michael@0: logger.error("Unable to read add-on manifest from " + stageDirEntry.path, e); michael@0: // This add-on can't be installed so just remove it now michael@0: seenFiles.push(stageDirEntry.leafName); michael@0: seenFiles.push(jsonfile.leafName); michael@0: continue; michael@0: } michael@0: michael@0: // Check for a cached metadata for this add-on, it may contain updated michael@0: // compatibility information michael@0: if (jsonfile.exists()) { michael@0: logger.debug("Found updated metadata for " + id + " in " + aLocation.name); michael@0: let fis = Cc["@mozilla.org/network/file-input-stream;1"]. michael@0: createInstance(Ci.nsIFileInputStream); michael@0: let json = Cc["@mozilla.org/dom/json;1"]. michael@0: createInstance(Ci.nsIJSON); michael@0: michael@0: try { michael@0: fis.init(jsonfile, -1, 0, 0); michael@0: let metadata = json.decodeFromStream(fis, jsonfile.fileSize); michael@0: aManifests[aLocation.name][id].importMetadata(metadata); michael@0: } michael@0: catch (e) { michael@0: // If some data can't be recovered from the cached metadata then it michael@0: // is unlikely to be a problem big enough to justify throwing away michael@0: // the install, just log and error and continue michael@0: logger.error("Unable to read metadata from " + jsonfile.path, e); michael@0: } michael@0: finally { michael@0: fis.close(); michael@0: } michael@0: } michael@0: seenFiles.push(jsonfile.leafName); michael@0: michael@0: existingAddonID = aManifests[aLocation.name][id].existingAddonID || id; michael@0: michael@0: var oldBootstrap = null; michael@0: logger.debug("Processing install of " + id + " in " + aLocation.name); michael@0: if (existingAddonID in this.bootstrappedAddons) { michael@0: try { michael@0: var existingAddon = aLocation.getLocationForID(existingAddonID); michael@0: if (this.bootstrappedAddons[existingAddonID].descriptor == michael@0: existingAddon.persistentDescriptor) { michael@0: oldBootstrap = this.bootstrappedAddons[existingAddonID]; michael@0: michael@0: // We'll be replacing a currently active bootstrapped add-on so michael@0: // call its uninstall method michael@0: let newVersion = aManifests[aLocation.name][id].version; michael@0: let oldVersion = oldBootstrap.version; michael@0: let uninstallReason = Services.vc.compare(oldVersion, newVersion) < 0 ? michael@0: BOOTSTRAP_REASONS.ADDON_UPGRADE : michael@0: BOOTSTRAP_REASONS.ADDON_DOWNGRADE; michael@0: michael@0: this.callBootstrapMethod(existingAddonID, oldBootstrap.version, michael@0: oldBootstrap.type, existingAddon, "uninstall", uninstallReason, michael@0: { newVersion: newVersion }); michael@0: this.unloadBootstrapScope(existingAddonID); michael@0: flushStartupCache(); michael@0: } michael@0: } michael@0: catch (e) { michael@0: } michael@0: } michael@0: michael@0: try { michael@0: var addonInstallLocation = aLocation.installAddon(id, stageDirEntry, michael@0: existingAddonID); michael@0: if (aManifests[aLocation.name][id]) michael@0: aManifests[aLocation.name][id]._sourceBundle = addonInstallLocation; michael@0: } michael@0: catch (e) { michael@0: logger.error("Failed to install staged add-on " + id + " in " + aLocation.name, michael@0: e); michael@0: // Re-create the staged install michael@0: AddonInstall.createStagedInstall(aLocation, stageDirEntry, michael@0: aManifests[aLocation.name][id]); michael@0: // Make sure not to delete the cached manifest json file michael@0: seenFiles.pop(); michael@0: michael@0: delete aManifests[aLocation.name][id]; michael@0: michael@0: if (oldBootstrap) { michael@0: // Re-install the old add-on michael@0: this.callBootstrapMethod(existingAddonID, oldBootstrap.version, michael@0: oldBootstrap.type, existingAddon, "install", michael@0: BOOTSTRAP_REASONS.ADDON_INSTALL); michael@0: } michael@0: continue; michael@0: } michael@0: } michael@0: michael@0: try { michael@0: aLocation.cleanStagingDir(seenFiles); michael@0: } michael@0: catch (e) { michael@0: // Non-critical, just saves some perf on startup if we clean this up. michael@0: logger.debug("Error cleaning staging dir " + stagingDir.path, e); michael@0: } michael@0: }, this); michael@0: return changed; michael@0: }, michael@0: michael@0: /** michael@0: * Installs any add-ons located in the extensions directory of the michael@0: * application's distribution specific directory into the profile unless a michael@0: * newer version already exists or the user has previously uninstalled the michael@0: * distributed add-on. michael@0: * michael@0: * @param aManifests michael@0: * A dictionary to add new install manifests to to save having to michael@0: * reload them later michael@0: * @return true if any new add-ons were installed michael@0: */ michael@0: installDistributionAddons: function XPI_installDistributionAddons(aManifests) { michael@0: let distroDir; michael@0: try { michael@0: distroDir = FileUtils.getDir(KEY_APP_DISTRIBUTION, [DIR_EXTENSIONS]); michael@0: } michael@0: catch (e) { michael@0: return false; michael@0: } michael@0: michael@0: if (!distroDir.exists()) michael@0: return false; michael@0: michael@0: if (!distroDir.isDirectory()) michael@0: return false; michael@0: michael@0: let changed = false; michael@0: let profileLocation = this.installLocationsByName[KEY_APP_PROFILE]; michael@0: michael@0: let entries = distroDir.directoryEntries michael@0: .QueryInterface(Ci.nsIDirectoryEnumerator); michael@0: let entry; michael@0: while ((entry = entries.nextFile)) { michael@0: michael@0: let id = entry.leafName; michael@0: michael@0: if (entry.isFile()) { michael@0: if (id.substring(id.length - 4).toLowerCase() == ".xpi") { michael@0: id = id.substring(0, id.length - 4); michael@0: } michael@0: else { michael@0: logger.debug("Ignoring distribution add-on that isn't an XPI: " + entry.path); michael@0: continue; michael@0: } michael@0: } michael@0: else if (!entry.isDirectory()) { michael@0: logger.debug("Ignoring distribution add-on that isn't a file or directory: " + michael@0: entry.path); michael@0: continue; michael@0: } michael@0: michael@0: if (!gIDTest.test(id)) { michael@0: logger.debug("Ignoring distribution add-on whose name is not a valid add-on ID: " + michael@0: entry.path); michael@0: continue; michael@0: } michael@0: michael@0: let addon; michael@0: try { michael@0: addon = loadManifestFromFile(entry); michael@0: } michael@0: catch (e) { michael@0: logger.warn("File entry " + entry.path + " contains an invalid add-on", e); michael@0: continue; michael@0: } michael@0: michael@0: if (addon.id != id) { michael@0: logger.warn("File entry " + entry.path + " contains an add-on with an " + michael@0: "incorrect ID") michael@0: continue; michael@0: } michael@0: michael@0: let existingEntry = null; michael@0: try { michael@0: existingEntry = profileLocation.getLocationForID(id); michael@0: } michael@0: catch (e) { michael@0: } michael@0: michael@0: if (existingEntry) { michael@0: let existingAddon; michael@0: try { michael@0: existingAddon = loadManifestFromFile(existingEntry); michael@0: michael@0: if (Services.vc.compare(addon.version, existingAddon.version) <= 0) michael@0: continue; michael@0: } michael@0: catch (e) { michael@0: // Bad add-on in the profile so just proceed and install over the top michael@0: logger.warn("Profile contains an add-on with a bad or missing install " + michael@0: "manifest at " + existingEntry.path + ", overwriting", e); michael@0: } michael@0: } michael@0: else if (Prefs.getBoolPref(PREF_BRANCH_INSTALLED_ADDON + id, false)) { michael@0: continue; michael@0: } michael@0: michael@0: // Install the add-on michael@0: try { michael@0: profileLocation.installAddon(id, entry, null, true); michael@0: logger.debug("Installed distribution add-on " + id); michael@0: michael@0: Services.prefs.setBoolPref(PREF_BRANCH_INSTALLED_ADDON + id, true) michael@0: michael@0: // aManifests may contain a copy of a newly installed add-on's manifest michael@0: // and we'll have overwritten that so instead cache our install manifest michael@0: // which will later be put into the database in processFileChanges michael@0: if (!(KEY_APP_PROFILE in aManifests)) michael@0: aManifests[KEY_APP_PROFILE] = {}; michael@0: aManifests[KEY_APP_PROFILE][id] = addon; michael@0: changed = true; michael@0: } michael@0: catch (e) { michael@0: logger.error("Failed to install distribution add-on " + entry.path, e); michael@0: } michael@0: } michael@0: michael@0: entries.close(); michael@0: michael@0: return changed; michael@0: }, michael@0: michael@0: /** michael@0: * Compares the add-ons that are currently installed to those that were michael@0: * known to be installed when the application last ran and applies any michael@0: * changes found to the database. Also sends "startupcache-invalidate" signal to michael@0: * observerservice if it detects that data may have changed. michael@0: * michael@0: * @param aState michael@0: * The array of current install location states michael@0: * @param aManifests michael@0: * A dictionary of cached AddonInstalls for add-ons that have been michael@0: * installed michael@0: * @param aUpdateCompatibility michael@0: * true to update add-ons appDisabled property when the application michael@0: * version has changed michael@0: * @param aOldAppVersion michael@0: * The version of the application last run with this profile or null michael@0: * if it is a new profile or the version is unknown michael@0: * @param aOldPlatformVersion michael@0: * The version of the platform last run with this profile or null michael@0: * if it is a new profile or the version is unknown michael@0: * @return a boolean indicating if a change requiring flushing the caches was michael@0: * detected michael@0: */ michael@0: processFileChanges: function XPI_processFileChanges(aState, aManifests, michael@0: aUpdateCompatibility, michael@0: aOldAppVersion, michael@0: aOldPlatformVersion) { michael@0: let visibleAddons = {}; michael@0: let oldBootstrappedAddons = this.bootstrappedAddons; michael@0: this.bootstrappedAddons = {}; michael@0: michael@0: /** michael@0: * Updates an add-on's metadata and determines if a restart of the michael@0: * application is necessary. This is called when either the add-on's michael@0: * install directory path or last modified time has changed. michael@0: * michael@0: * @param aInstallLocation michael@0: * The install location containing the add-on michael@0: * @param aOldAddon michael@0: * The AddonInternal as it appeared the last time the application michael@0: * ran michael@0: * @param aAddonState michael@0: * The new state of the add-on michael@0: * @return a boolean indicating if flushing caches is required to complete michael@0: * changing this add-on michael@0: */ michael@0: function updateMetadata(aInstallLocation, aOldAddon, aAddonState) { michael@0: logger.debug("Add-on " + aOldAddon.id + " modified in " + aInstallLocation.name); michael@0: michael@0: // Check if there is an updated install manifest for this add-on michael@0: let newAddon = aManifests[aInstallLocation.name][aOldAddon.id]; michael@0: michael@0: try { michael@0: // If not load it michael@0: if (!newAddon) { michael@0: let file = aInstallLocation.getLocationForID(aOldAddon.id); michael@0: newAddon = loadManifestFromFile(file); michael@0: applyBlocklistChanges(aOldAddon, newAddon); michael@0: michael@0: // Carry over any pendingUninstall state to add-ons modified directly michael@0: // in the profile. This is important when the attempt to remove the michael@0: // add-on in processPendingFileChanges failed and caused an mtime michael@0: // change to the add-ons files. michael@0: newAddon.pendingUninstall = aOldAddon.pendingUninstall; michael@0: } michael@0: michael@0: // The ID in the manifest that was loaded must match the ID of the old michael@0: // add-on. michael@0: if (newAddon.id != aOldAddon.id) michael@0: throw new Error("Incorrect id in install manifest"); michael@0: } michael@0: catch (e) { michael@0: logger.warn("Add-on is invalid", e); michael@0: XPIDatabase.removeAddonMetadata(aOldAddon); michael@0: if (!aInstallLocation.locked) michael@0: aInstallLocation.uninstallAddon(aOldAddon.id); michael@0: else michael@0: logger.warn("Could not uninstall invalid item from locked install location"); michael@0: // If this was an active add-on then we must force a restart michael@0: if (aOldAddon.active) michael@0: return true; michael@0: michael@0: return false; michael@0: } michael@0: michael@0: // Set the additional properties on the new AddonInternal michael@0: newAddon._installLocation = aInstallLocation; michael@0: newAddon.updateDate = aAddonState.mtime; michael@0: newAddon.visible = !(newAddon.id in visibleAddons); michael@0: michael@0: // Update the database michael@0: let newDBAddon = XPIDatabase.updateAddonMetadata(aOldAddon, newAddon, michael@0: aAddonState.descriptor); michael@0: if (newDBAddon.visible) { michael@0: visibleAddons[newDBAddon.id] = newDBAddon; michael@0: // Remember add-ons that were changed during startup michael@0: AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED, michael@0: newDBAddon.id); michael@0: michael@0: // If this was the active theme and it is now disabled then enable the michael@0: // default theme michael@0: if (aOldAddon.active && isAddonDisabled(newDBAddon)) michael@0: XPIProvider.enableDefaultTheme(); michael@0: michael@0: // If the new add-on is bootstrapped and active then call its install method michael@0: if (newDBAddon.active && newDBAddon.bootstrap) { michael@0: // Startup cache must be flushed before calling the bootstrap script michael@0: flushStartupCache(); michael@0: michael@0: let installReason = Services.vc.compare(aOldAddon.version, newDBAddon.version) < 0 ? michael@0: BOOTSTRAP_REASONS.ADDON_UPGRADE : michael@0: BOOTSTRAP_REASONS.ADDON_DOWNGRADE; michael@0: michael@0: let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); michael@0: file.persistentDescriptor = aAddonState.descriptor; michael@0: XPIProvider.callBootstrapMethod(newDBAddon.id, newDBAddon.version, michael@0: newDBAddon.type, file, "install", michael@0: installReason, { oldVersion: aOldAddon.version }); michael@0: return false; michael@0: } michael@0: michael@0: return true; michael@0: } michael@0: michael@0: return false; michael@0: } michael@0: michael@0: /** michael@0: * Updates an add-on's descriptor for when the add-on has moved in the michael@0: * filesystem but hasn't changed in any other way. michael@0: * michael@0: * @param aInstallLocation michael@0: * The install location containing the add-on michael@0: * @param aOldAddon michael@0: * The AddonInternal as it appeared the last time the application michael@0: * ran michael@0: * @param aAddonState michael@0: * The new state of the add-on michael@0: * @return a boolean indicating if flushing caches is required to complete michael@0: * changing this add-on michael@0: */ michael@0: function updateDescriptor(aInstallLocation, aOldAddon, aAddonState) { michael@0: logger.debug("Add-on " + aOldAddon.id + " moved to " + aAddonState.descriptor); michael@0: michael@0: aOldAddon.descriptor = aAddonState.descriptor; michael@0: aOldAddon.visible = !(aOldAddon.id in visibleAddons); michael@0: XPIDatabase.saveChanges(); michael@0: michael@0: if (aOldAddon.visible) { michael@0: visibleAddons[aOldAddon.id] = aOldAddon; michael@0: michael@0: if (aOldAddon.bootstrap && aOldAddon.active) { michael@0: let bootstrap = oldBootstrappedAddons[aOldAddon.id]; michael@0: bootstrap.descriptor = aAddonState.descriptor; michael@0: XPIProvider.bootstrappedAddons[aOldAddon.id] = bootstrap; michael@0: } michael@0: michael@0: return true; michael@0: } michael@0: michael@0: return false; michael@0: } michael@0: michael@0: /** michael@0: * Called when no change has been detected for an add-on's metadata. The michael@0: * add-on may have become visible due to other add-ons being removed or michael@0: * the add-on may need to be updated when the application version has michael@0: * changed. michael@0: * michael@0: * @param aInstallLocation michael@0: * The install location containing the add-on michael@0: * @param aOldAddon michael@0: * The AddonInternal as it appeared the last time the application michael@0: * ran michael@0: * @param aAddonState michael@0: * The new state of the add-on michael@0: * @return a boolean indicating if flushing caches is required to complete michael@0: * changing this add-on michael@0: */ michael@0: function updateVisibilityAndCompatibility(aInstallLocation, aOldAddon, michael@0: aAddonState) { michael@0: let changed = false; michael@0: michael@0: // This add-ons metadata has not changed but it may have become visible michael@0: if (!(aOldAddon.id in visibleAddons)) { michael@0: visibleAddons[aOldAddon.id] = aOldAddon; michael@0: michael@0: if (!aOldAddon.visible) { michael@0: // Remember add-ons that were changed during startup. michael@0: AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED, michael@0: aOldAddon.id); michael@0: XPIDatabase.makeAddonVisible(aOldAddon); michael@0: michael@0: if (aOldAddon.bootstrap) { michael@0: // The add-on is bootstrappable so call its install script michael@0: let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); michael@0: file.persistentDescriptor = aAddonState.descriptor; michael@0: XPIProvider.callBootstrapMethod(aOldAddon.id, aOldAddon.version, aOldAddon.type, file, michael@0: "install", michael@0: BOOTSTRAP_REASONS.ADDON_INSTALL); michael@0: michael@0: // If it should be active then mark it as active otherwise unload michael@0: // its scope michael@0: if (!isAddonDisabled(aOldAddon)) { michael@0: XPIDatabase.updateAddonActive(aOldAddon, true); michael@0: } michael@0: else { michael@0: XPIProvider.unloadBootstrapScope(newAddon.id); michael@0: } michael@0: } michael@0: else { michael@0: // Otherwise a restart is necessary michael@0: changed = true; michael@0: } michael@0: } michael@0: } michael@0: michael@0: // App version changed, we may need to update the appDisabled property. michael@0: if (aUpdateCompatibility) { michael@0: let wasDisabled = isAddonDisabled(aOldAddon); michael@0: let wasAppDisabled = aOldAddon.appDisabled; michael@0: let wasUserDisabled = aOldAddon.userDisabled; michael@0: let wasSoftDisabled = aOldAddon.softDisabled; michael@0: michael@0: // This updates the addon's JSON cached data in place michael@0: applyBlocklistChanges(aOldAddon, aOldAddon, aOldAppVersion, michael@0: aOldPlatformVersion); michael@0: aOldAddon.appDisabled = !isUsableAddon(aOldAddon); michael@0: michael@0: let isDisabled = isAddonDisabled(aOldAddon); michael@0: michael@0: // If either property has changed update the database. michael@0: if (wasAppDisabled != aOldAddon.appDisabled || michael@0: wasUserDisabled != aOldAddon.userDisabled || michael@0: wasSoftDisabled != aOldAddon.softDisabled) { michael@0: logger.debug("Add-on " + aOldAddon.id + " changed appDisabled state to " + michael@0: aOldAddon.appDisabled + ", userDisabled state to " + michael@0: aOldAddon.userDisabled + " and softDisabled state to " + michael@0: aOldAddon.softDisabled); michael@0: XPIDatabase.saveChanges(); michael@0: } michael@0: michael@0: // If this is a visible add-on and it has changed disabled state then we michael@0: // may need a restart or to update the bootstrap list. michael@0: if (aOldAddon.visible && wasDisabled != isDisabled) { michael@0: // Remember add-ons that became disabled or enabled by the application michael@0: // change michael@0: let change = isDisabled ? AddonManager.STARTUP_CHANGE_DISABLED michael@0: : AddonManager.STARTUP_CHANGE_ENABLED; michael@0: AddonManagerPrivate.addStartupChange(change, aOldAddon.id); michael@0: if (aOldAddon.bootstrap) { michael@0: // Update the add-ons active state michael@0: XPIDatabase.updateAddonActive(aOldAddon, !isDisabled); michael@0: } michael@0: else { michael@0: changed = true; michael@0: } michael@0: } michael@0: } michael@0: michael@0: if (aOldAddon.visible && aOldAddon.active && aOldAddon.bootstrap) { michael@0: XPIProvider.bootstrappedAddons[aOldAddon.id] = { michael@0: version: aOldAddon.version, michael@0: type: aOldAddon.type, michael@0: descriptor: aAddonState.descriptor michael@0: }; michael@0: } michael@0: michael@0: return changed; michael@0: } michael@0: michael@0: /** michael@0: * Called when an add-on has been removed. michael@0: * michael@0: * @param aOldAddon michael@0: * The AddonInternal as it appeared the last time the application michael@0: * ran michael@0: * @return a boolean indicating if flushing caches is required to complete michael@0: * changing this add-on michael@0: */ michael@0: function removeMetadata(aOldAddon) { michael@0: // This add-on has disappeared michael@0: logger.debug("Add-on " + aOldAddon.id + " removed from " + aOldAddon.location); michael@0: XPIDatabase.removeAddonMetadata(aOldAddon); michael@0: michael@0: // Remember add-ons that were uninstalled during startup michael@0: if (aOldAddon.visible) { michael@0: AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_UNINSTALLED, michael@0: aOldAddon.id); michael@0: } michael@0: else if (AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_INSTALLED) michael@0: .indexOf(aOldAddon.id) != -1) { michael@0: AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED, michael@0: aOldAddon.id); michael@0: } michael@0: michael@0: if (aOldAddon.active) { michael@0: // Enable the default theme if the previously active theme has been michael@0: // removed michael@0: if (aOldAddon.type == "theme") michael@0: XPIProvider.enableDefaultTheme(); michael@0: michael@0: return true; michael@0: } michael@0: michael@0: return false; michael@0: } michael@0: michael@0: /** michael@0: * Called to add the metadata for an add-on in one of the install locations michael@0: * to the database. This can be called in three different cases. Either an michael@0: * add-on has been dropped into the location from outside of Firefox, or michael@0: * an add-on has been installed through the application, or the database michael@0: * has been upgraded or become corrupt and add-on data has to be reloaded michael@0: * into it. michael@0: * michael@0: * @param aInstallLocation michael@0: * The install location containing the add-on michael@0: * @param aId michael@0: * The ID of the add-on michael@0: * @param aAddonState michael@0: * The new state of the add-on michael@0: * @param aMigrateData michael@0: * If during startup the database had to be upgraded this will michael@0: * contain data that used to be held about this add-on michael@0: * @return a boolean indicating if flushing caches is required to complete michael@0: * changing this add-on michael@0: */ michael@0: function addMetadata(aInstallLocation, aId, aAddonState, aMigrateData) { michael@0: logger.debug("New add-on " + aId + " installed in " + aInstallLocation.name); michael@0: michael@0: let newAddon = null; michael@0: let sameVersion = false; michael@0: // Check the updated manifests lists for the install location, If there michael@0: // is no manifest for the add-on ID then newAddon will be undefined michael@0: if (aInstallLocation.name in aManifests) michael@0: newAddon = aManifests[aInstallLocation.name][aId]; michael@0: michael@0: // If we had staged data for this add-on or we aren't recovering from a michael@0: // corrupt database and we don't have migration data for this add-on then michael@0: // this must be a new install. michael@0: let isNewInstall = (!!newAddon) || (!XPIDatabase.activeBundles && !aMigrateData); michael@0: michael@0: // If it's a new install and we haven't yet loaded the manifest then it michael@0: // must be something dropped directly into the install location michael@0: let isDetectedInstall = isNewInstall && !newAddon; michael@0: michael@0: // Load the manifest if necessary and sanity check the add-on ID michael@0: try { michael@0: if (!newAddon) { michael@0: // Load the manifest from the add-on. michael@0: let file = aInstallLocation.getLocationForID(aId); michael@0: newAddon = loadManifestFromFile(file); michael@0: } michael@0: // The add-on in the manifest should match the add-on ID. michael@0: if (newAddon.id != aId) { michael@0: throw new Error("Invalid addon ID: expected addon ID " + aId + michael@0: ", found " + newAddon.id + " in manifest"); michael@0: } michael@0: } michael@0: catch (e) { michael@0: logger.warn("Add-on is invalid", e); michael@0: michael@0: // Remove the invalid add-on from the install location if the install michael@0: // location isn't locked, no restart will be necessary michael@0: if (!aInstallLocation.locked) michael@0: aInstallLocation.uninstallAddon(aId); michael@0: else michael@0: logger.warn("Could not uninstall invalid item from locked install location"); michael@0: return false; michael@0: } michael@0: michael@0: // Update the AddonInternal properties. michael@0: newAddon._installLocation = aInstallLocation; michael@0: newAddon.visible = !(newAddon.id in visibleAddons); michael@0: newAddon.installDate = aAddonState.mtime; michael@0: newAddon.updateDate = aAddonState.mtime; michael@0: newAddon.foreignInstall = isDetectedInstall; michael@0: michael@0: if (aMigrateData) { michael@0: // If there is migration data then apply it. michael@0: logger.debug("Migrating data from old database"); michael@0: michael@0: DB_MIGRATE_METADATA.forEach(function(aProp) { michael@0: // A theme's disabled state is determined by the selected theme michael@0: // preference which is read in loadManifestFromRDF michael@0: if (aProp == "userDisabled" && newAddon.type == "theme") michael@0: return; michael@0: michael@0: if (aProp in aMigrateData) michael@0: newAddon[aProp] = aMigrateData[aProp]; michael@0: }); michael@0: michael@0: // Force all non-profile add-ons to be foreignInstalls since they can't michael@0: // have been installed through the API michael@0: newAddon.foreignInstall |= aInstallLocation.name != KEY_APP_PROFILE; michael@0: michael@0: // Some properties should only be migrated if the add-on hasn't changed. michael@0: // The version property isn't a perfect check for this but covers the michael@0: // vast majority of cases. michael@0: if (aMigrateData.version == newAddon.version) { michael@0: logger.debug("Migrating compatibility info"); michael@0: sameVersion = true; michael@0: if ("targetApplications" in aMigrateData) michael@0: newAddon.applyCompatibilityUpdate(aMigrateData, true); michael@0: } michael@0: michael@0: // Since the DB schema has changed make sure softDisabled is correct michael@0: applyBlocklistChanges(newAddon, newAddon, aOldAppVersion, michael@0: aOldPlatformVersion); michael@0: } michael@0: michael@0: // The default theme is never a foreign install michael@0: if (newAddon.type == "theme" && newAddon.internalName == XPIProvider.defaultSkin) michael@0: newAddon.foreignInstall = false; michael@0: michael@0: if (isDetectedInstall && newAddon.foreignInstall) { michael@0: // If the add-on is a foreign install and is in a scope where add-ons michael@0: // that were dropped in should default to disabled then disable it michael@0: let disablingScopes = Prefs.getIntPref(PREF_EM_AUTO_DISABLED_SCOPES, 0); michael@0: if (aInstallLocation.scope & disablingScopes) michael@0: newAddon.userDisabled = true; michael@0: } michael@0: michael@0: // If we have a list of what add-ons should be marked as active then use michael@0: // it to guess at migration data. michael@0: if (!isNewInstall && XPIDatabase.activeBundles) { michael@0: // For themes we know which is active by the current skin setting michael@0: if (newAddon.type == "theme") michael@0: newAddon.active = newAddon.internalName == XPIProvider.currentSkin; michael@0: else michael@0: newAddon.active = XPIDatabase.activeBundles.indexOf(aAddonState.descriptor) != -1; michael@0: michael@0: // If the add-on wasn't active and it isn't already disabled in some way michael@0: // then it was probably either softDisabled or userDisabled michael@0: if (!newAddon.active && newAddon.visible && !isAddonDisabled(newAddon)) { michael@0: // If the add-on is softblocked then assume it is softDisabled michael@0: if (newAddon.blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED) michael@0: newAddon.softDisabled = true; michael@0: else michael@0: newAddon.userDisabled = true; michael@0: } michael@0: } michael@0: else { michael@0: newAddon.active = (newAddon.visible && !isAddonDisabled(newAddon)) michael@0: } michael@0: michael@0: let newDBAddon = XPIDatabase.addAddonMetadata(newAddon, aAddonState.descriptor); michael@0: michael@0: if (newDBAddon.visible) { michael@0: // Remember add-ons that were first detected during startup. michael@0: if (isDetectedInstall) { michael@0: // If a copy from a higher priority location was removed then this michael@0: // add-on has changed michael@0: if (AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_UNINSTALLED) michael@0: .indexOf(newDBAddon.id) != -1) { michael@0: AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED, michael@0: newDBAddon.id); michael@0: } michael@0: else { michael@0: AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_INSTALLED, michael@0: newDBAddon.id); michael@0: } michael@0: } michael@0: michael@0: // Note if any visible add-on is not in the application install location michael@0: if (newDBAddon._installLocation.name != KEY_APP_GLOBAL) michael@0: XPIProvider.allAppGlobal = false; michael@0: michael@0: visibleAddons[newDBAddon.id] = newDBAddon; michael@0: michael@0: let installReason = BOOTSTRAP_REASONS.ADDON_INSTALL; michael@0: let extraParams = {}; michael@0: michael@0: // If we're hiding a bootstrapped add-on then call its uninstall method michael@0: if (newDBAddon.id in oldBootstrappedAddons) { michael@0: let oldBootstrap = oldBootstrappedAddons[newDBAddon.id]; michael@0: extraParams.oldVersion = oldBootstrap.version; michael@0: XPIProvider.bootstrappedAddons[newDBAddon.id] = oldBootstrap; michael@0: michael@0: // If the old version is the same as the new version, or we're michael@0: // recovering from a corrupt DB, don't call uninstall and install michael@0: // methods. michael@0: if (sameVersion || !isNewInstall) michael@0: return false; michael@0: michael@0: installReason = Services.vc.compare(oldBootstrap.version, newDBAddon.version) < 0 ? michael@0: BOOTSTRAP_REASONS.ADDON_UPGRADE : michael@0: BOOTSTRAP_REASONS.ADDON_DOWNGRADE; michael@0: michael@0: let oldAddonFile = Cc["@mozilla.org/file/local;1"]. michael@0: createInstance(Ci.nsIFile); michael@0: oldAddonFile.persistentDescriptor = oldBootstrap.descriptor; michael@0: michael@0: XPIProvider.callBootstrapMethod(newDBAddon.id, oldBootstrap.version, michael@0: oldBootstrap.type, oldAddonFile, "uninstall", michael@0: installReason, { newVersion: newDBAddon.version }); michael@0: XPIProvider.unloadBootstrapScope(newDBAddon.id); michael@0: michael@0: // If the new add-on is bootstrapped then we must flush the caches michael@0: // before calling the new bootstrap script michael@0: if (newDBAddon.bootstrap) michael@0: flushStartupCache(); michael@0: } michael@0: michael@0: if (!newDBAddon.bootstrap) michael@0: return true; michael@0: michael@0: // Visible bootstrapped add-ons need to have their install method called michael@0: let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); michael@0: file.persistentDescriptor = aAddonState.descriptor; michael@0: XPIProvider.callBootstrapMethod(newDBAddon.id, newDBAddon.version, newDBAddon.type, file, michael@0: "install", installReason, extraParams); michael@0: if (!newDBAddon.active) michael@0: XPIProvider.unloadBootstrapScope(newDBAddon.id); michael@0: } michael@0: michael@0: return false; michael@0: } michael@0: michael@0: let changed = false; michael@0: let knownLocations = XPIDatabase.getInstallLocations(); michael@0: michael@0: // The install locations are iterated in reverse order of priority so when michael@0: // there are multiple add-ons installed with the same ID the one that michael@0: // should be visible is the first one encountered. michael@0: for (let aSt of aState.reverse()) { michael@0: michael@0: // We can't include the install location directly in the state as it has michael@0: // to be cached as JSON. michael@0: let installLocation = this.installLocationsByName[aSt.name]; michael@0: let addonStates = aSt.addons; michael@0: michael@0: // Check if the database knows about any add-ons in this install location. michael@0: if (knownLocations.has(installLocation.name)) { michael@0: knownLocations.delete(installLocation.name); michael@0: let addons = XPIDatabase.getAddonsInLocation(installLocation.name); michael@0: // Iterate through the add-ons installed the last time the application michael@0: // ran michael@0: for (let aOldAddon of addons) { michael@0: // If a version of this add-on has been installed in an higher michael@0: // priority install location then count it as changed michael@0: if (AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_INSTALLED) michael@0: .indexOf(aOldAddon.id) != -1) { michael@0: AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED, michael@0: aOldAddon.id); michael@0: } michael@0: michael@0: // Check if the add-on is still installed michael@0: if (aOldAddon.id in addonStates) { michael@0: let addonState = addonStates[aOldAddon.id]; michael@0: delete addonStates[aOldAddon.id]; michael@0: michael@0: // Remember add-ons that were inactive during startup michael@0: if (aOldAddon.visible && !aOldAddon.active) michael@0: XPIProvider.inactiveAddonIDs.push(aOldAddon.id); michael@0: michael@0: // record a bit more per-addon telemetry michael@0: let loc = aOldAddon.defaultLocale; michael@0: if (loc) { michael@0: XPIProvider.setTelemetry(aOldAddon.id, "name", loc.name); michael@0: XPIProvider.setTelemetry(aOldAddon.id, "creator", loc.creator); michael@0: } michael@0: michael@0: // Check if the add-on has been changed outside the XPI provider michael@0: if (aOldAddon.updateDate != addonState.mtime) { michael@0: // Did time change in the wrong direction? michael@0: if (addonState.mtime < aOldAddon.updateDate) { michael@0: this.setTelemetry(aOldAddon.id, "olderFile", { michael@0: name: this._mostRecentlyModifiedFile[aOldAddon.id], michael@0: mtime: addonState.mtime, michael@0: oldtime: aOldAddon.updateDate michael@0: }); michael@0: } michael@0: // Is the add-on unpacked? michael@0: else if (addonState.rdfTime) { michael@0: // Was the addon manifest "install.rdf" modified, or some other file? michael@0: if (addonState.rdfTime > aOldAddon.updateDate) { michael@0: this.setTelemetry(aOldAddon.id, "modifiedInstallRDF", 1); michael@0: } michael@0: else { michael@0: this.setTelemetry(aOldAddon.id, "modifiedFile", michael@0: this._mostRecentlyModifiedFile[aOldAddon.id]); michael@0: } michael@0: } michael@0: else { michael@0: this.setTelemetry(aOldAddon.id, "modifiedXPI", 1); michael@0: } michael@0: } michael@0: michael@0: // The add-on has changed if the modification time has changed, or michael@0: // we have an updated manifest for it. Also reload the metadata for michael@0: // add-ons in the application directory when the application version michael@0: // has changed michael@0: if (aOldAddon.id in aManifests[installLocation.name] || michael@0: aOldAddon.updateDate != addonState.mtime || michael@0: (aUpdateCompatibility && installLocation.name == KEY_APP_GLOBAL)) { michael@0: changed = updateMetadata(installLocation, aOldAddon, addonState) || michael@0: changed; michael@0: } michael@0: else if (aOldAddon.descriptor != addonState.descriptor) { michael@0: changed = updateDescriptor(installLocation, aOldAddon, addonState) || michael@0: changed; michael@0: } michael@0: else { michael@0: changed = updateVisibilityAndCompatibility(installLocation, michael@0: aOldAddon, addonState) || michael@0: changed; michael@0: } michael@0: if (aOldAddon.visible && aOldAddon._installLocation.name != KEY_APP_GLOBAL) michael@0: XPIProvider.allAppGlobal = false; michael@0: } michael@0: else { michael@0: changed = removeMetadata(aOldAddon) || changed; michael@0: } michael@0: } michael@0: } michael@0: michael@0: // All the remaining add-ons in this install location must be new. michael@0: michael@0: // Get the migration data for this install location. michael@0: let locMigrateData = {}; michael@0: if (XPIDatabase.migrateData && installLocation.name in XPIDatabase.migrateData) michael@0: locMigrateData = XPIDatabase.migrateData[installLocation.name]; michael@0: for (let id in addonStates) { michael@0: changed = addMetadata(installLocation, id, addonStates[id], michael@0: (locMigrateData[id] || null)) || changed; michael@0: } michael@0: } michael@0: michael@0: // The remaining locations that had add-ons installed in them no longer michael@0: // have any add-ons installed in them, or the locations no longer exist. michael@0: // The metadata for the add-ons that were in them must be removed from the michael@0: // database. michael@0: for (let location of knownLocations) { michael@0: let addons = XPIDatabase.getAddonsInLocation(location); michael@0: for (let aOldAddon of addons) { michael@0: changed = removeMetadata(aOldAddon) || changed; michael@0: } michael@0: } michael@0: michael@0: // Cache the new install location states michael@0: this.installStates = this.getInstallLocationStates(); michael@0: let cache = JSON.stringify(this.installStates); michael@0: Services.prefs.setCharPref(PREF_INSTALL_CACHE, cache); michael@0: this.persistBootstrappedAddons(); michael@0: michael@0: // Clear out any cached migration data. michael@0: XPIDatabase.migrateData = null; michael@0: michael@0: return changed; michael@0: }, michael@0: michael@0: /** michael@0: * Imports the xpinstall permissions from preferences into the permissions michael@0: * manager for the user to change later. michael@0: */ michael@0: importPermissions: function XPI_importPermissions() { michael@0: PermissionsUtils.importFromPrefs(PREF_XPI_PERMISSIONS_BRANCH, michael@0: XPI_PERMISSION); michael@0: }, michael@0: michael@0: /** michael@0: * Checks for any changes that have occurred since the last time the michael@0: * application was launched. michael@0: * michael@0: * @param aAppChanged michael@0: * A tri-state value. Undefined means the current profile was created michael@0: * for this session, true means the profile already existed but was michael@0: * last used with an application with a different version number, michael@0: * false means that the profile was last used by this version of the michael@0: * application. michael@0: * @param aOldAppVersion michael@0: * The version of the application last run with this profile or null michael@0: * if it is a new profile or the version is unknown michael@0: * @param aOldPlatformVersion michael@0: * The version of the platform last run with this profile or null michael@0: * if it is a new profile or the version is unknown michael@0: * @return true if a change requiring a restart was detected michael@0: */ michael@0: checkForChanges: function XPI_checkForChanges(aAppChanged, aOldAppVersion, michael@0: aOldPlatformVersion) { michael@0: logger.debug("checkForChanges"); michael@0: michael@0: // Keep track of whether and why we need to open and update the database at michael@0: // startup time. michael@0: let updateReasons = []; michael@0: if (aAppChanged) { michael@0: updateReasons.push("appChanged"); michael@0: } michael@0: michael@0: // Load the list of bootstrapped add-ons first so processFileChanges can michael@0: // modify it michael@0: try { michael@0: this.bootstrappedAddons = JSON.parse(Prefs.getCharPref(PREF_BOOTSTRAP_ADDONS, michael@0: "{}")); michael@0: } catch (e) { michael@0: logger.warn("Error parsing enabled bootstrapped extensions cache", e); michael@0: } michael@0: michael@0: // First install any new add-ons into the locations, if there are any michael@0: // changes then we must update the database with the information in the michael@0: // install locations michael@0: let manifests = {}; michael@0: let updated = this.processPendingFileChanges(manifests); michael@0: if (updated) { michael@0: updateReasons.push("pendingFileChanges"); michael@0: } michael@0: michael@0: // This will be true if the previous session made changes that affect the michael@0: // active state of add-ons but didn't commit them properly (normally due michael@0: // to the application crashing) michael@0: let hasPendingChanges = Prefs.getBoolPref(PREF_PENDING_OPERATIONS); michael@0: if (hasPendingChanges) { michael@0: updateReasons.push("hasPendingChanges"); michael@0: } michael@0: michael@0: // If the application has changed then check for new distribution add-ons michael@0: if (aAppChanged !== false && michael@0: Prefs.getBoolPref(PREF_INSTALL_DISTRO_ADDONS, true)) michael@0: { michael@0: updated = this.installDistributionAddons(manifests); michael@0: if (updated) { michael@0: updateReasons.push("installDistributionAddons"); michael@0: } michael@0: } michael@0: michael@0: // Telemetry probe added around getInstallLocationStates() to check perf michael@0: let telemetryCaptureTime = Date.now(); michael@0: this.installStates = this.getInstallLocationStates(); michael@0: let telemetry = Services.telemetry; michael@0: telemetry.getHistogramById("CHECK_ADDONS_MODIFIED_MS").add(Date.now() - telemetryCaptureTime); michael@0: michael@0: // If the install directory state has changed then we must update the database michael@0: let cache = Prefs.getCharPref(PREF_INSTALL_CACHE, "[]"); michael@0: // For a little while, gather telemetry on whether the deep comparison michael@0: // makes a difference michael@0: let newState = JSON.stringify(this.installStates); michael@0: if (cache != newState) { michael@0: logger.debug("Directory state JSON differs: cache " + cache + " state " + newState); michael@0: if (directoryStateDiffers(this.installStates, cache)) { michael@0: updateReasons.push("directoryState"); michael@0: } michael@0: else { michael@0: AddonManagerPrivate.recordSimpleMeasure("XPIDB_startup_state_badCompare", 1); michael@0: } michael@0: } michael@0: michael@0: // If the schema appears to have changed then we should update the database michael@0: if (DB_SCHEMA != Prefs.getIntPref(PREF_DB_SCHEMA, 0)) { michael@0: // If we don't have any add-ons, just update the pref, since we don't need to michael@0: // write the database michael@0: if (this.installStates.length == 0) { michael@0: logger.debug("Empty XPI database, setting schema version preference to " + DB_SCHEMA); michael@0: Services.prefs.setIntPref(PREF_DB_SCHEMA, DB_SCHEMA); michael@0: } michael@0: else { michael@0: updateReasons.push("schemaChanged"); michael@0: } michael@0: } michael@0: michael@0: // If the database doesn't exist and there are add-ons installed then we michael@0: // must update the database however if there are no add-ons then there is michael@0: // no need to update the database. michael@0: let dbFile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_DATABASE], true); michael@0: if (!dbFile.exists() && this.installStates.length > 0) { michael@0: updateReasons.push("needNewDatabase"); michael@0: } michael@0: michael@0: if (updateReasons.length == 0) { michael@0: let bootstrapDescriptors = [this.bootstrappedAddons[b].descriptor michael@0: for (b in this.bootstrappedAddons)]; michael@0: michael@0: this.installStates.forEach(function(aInstallLocationState) { michael@0: for (let id in aInstallLocationState.addons) { michael@0: let pos = bootstrapDescriptors.indexOf(aInstallLocationState.addons[id].descriptor); michael@0: if (pos != -1) michael@0: bootstrapDescriptors.splice(pos, 1); michael@0: } michael@0: }); michael@0: michael@0: if (bootstrapDescriptors.length > 0) { michael@0: logger.warn("Bootstrap state is invalid (missing add-ons: " + bootstrapDescriptors.toSource() + ")"); michael@0: updateReasons.push("missingBootstrapAddon"); michael@0: } michael@0: } michael@0: michael@0: // Catch and log any errors during the main startup michael@0: try { michael@0: let extensionListChanged = false; michael@0: // If the database needs to be updated then open it and then update it michael@0: // from the filesystem michael@0: if (updateReasons.length > 0) { michael@0: AddonManagerPrivate.recordSimpleMeasure("XPIDB_startup_load_reasons", updateReasons); michael@0: XPIDatabase.syncLoadDB(false); michael@0: try { michael@0: extensionListChanged = this.processFileChanges(this.installStates, manifests, michael@0: aAppChanged, michael@0: aOldAppVersion, michael@0: aOldPlatformVersion); michael@0: } michael@0: catch (e) { michael@0: logger.error("Failed to process extension changes at startup", e); michael@0: } michael@0: } michael@0: michael@0: if (aAppChanged) { michael@0: // When upgrading the app and using a custom skin make sure it is still michael@0: // compatible otherwise switch back the default michael@0: if (this.currentSkin != this.defaultSkin) { michael@0: let oldSkin = XPIDatabase.getVisibleAddonForInternalName(this.currentSkin); michael@0: if (!oldSkin || isAddonDisabled(oldSkin)) michael@0: this.enableDefaultTheme(); michael@0: } michael@0: michael@0: // When upgrading remove the old extensions cache to force older michael@0: // versions to rescan the entire list of extensions michael@0: try { michael@0: let oldCache = FileUtils.getFile(KEY_PROFILEDIR, [FILE_OLD_CACHE], true); michael@0: if (oldCache.exists()) michael@0: oldCache.remove(true); michael@0: } michael@0: catch (e) { michael@0: logger.warn("Unable to remove old extension cache " + oldCache.path, e); michael@0: } michael@0: } michael@0: michael@0: // If the application crashed before completing any pending operations then michael@0: // we should perform them now. michael@0: if (extensionListChanged || hasPendingChanges) { michael@0: logger.debug("Updating database with changes to installed add-ons"); michael@0: XPIDatabase.updateActiveAddons(); michael@0: Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, michael@0: !XPIDatabase.writeAddonsList()); michael@0: Services.prefs.setCharPref(PREF_BOOTSTRAP_ADDONS, michael@0: JSON.stringify(this.bootstrappedAddons)); michael@0: return true; michael@0: } michael@0: michael@0: logger.debug("No changes found"); michael@0: } michael@0: catch (e) { michael@0: logger.error("Error during startup file checks", e); michael@0: } michael@0: michael@0: // Check that the add-ons list still exists michael@0: let addonsList = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST], michael@0: true); michael@0: if (addonsList.exists() == (this.installStates.length == 0)) { michael@0: logger.debug("Add-ons list is invalid, rebuilding"); michael@0: XPIDatabase.writeAddonsList(); michael@0: } michael@0: michael@0: return false; michael@0: }, michael@0: michael@0: /** michael@0: * Called to test whether this provider supports installing a particular michael@0: * mimetype. michael@0: * michael@0: * @param aMimetype michael@0: * The mimetype to check for michael@0: * @return true if the mimetype is application/x-xpinstall michael@0: */ michael@0: supportsMimetype: function XPI_supportsMimetype(aMimetype) { michael@0: return aMimetype == "application/x-xpinstall"; michael@0: }, michael@0: michael@0: /** michael@0: * Called to test whether installing XPI add-ons is enabled. michael@0: * michael@0: * @return true if installing is enabled michael@0: */ michael@0: isInstallEnabled: function XPI_isInstallEnabled() { michael@0: // Default to enabled if the preference does not exist michael@0: return Prefs.getBoolPref(PREF_XPI_ENABLED, true); michael@0: }, michael@0: michael@0: /** michael@0: * Called to test whether installing XPI add-ons by direct URL requests is michael@0: * whitelisted. michael@0: * michael@0: * @return true if installing by direct requests is whitelisted michael@0: */ michael@0: isDirectRequestWhitelisted: function XPI_isDirectRequestWhitelisted() { michael@0: // Default to whitelisted if the preference does not exist. michael@0: return Prefs.getBoolPref(PREF_XPI_DIRECT_WHITELISTED, true); michael@0: }, michael@0: michael@0: /** michael@0: * Called to test whether installing XPI add-ons from file referrers is michael@0: * whitelisted. michael@0: * michael@0: * @return true if installing from file referrers is whitelisted michael@0: */ michael@0: isFileRequestWhitelisted: function XPI_isFileRequestWhitelisted() { michael@0: // Default to whitelisted if the preference does not exist. michael@0: return Prefs.getBoolPref(PREF_XPI_FILE_WHITELISTED, true); michael@0: }, michael@0: michael@0: /** michael@0: * Called to test whether installing XPI add-ons from a URI is allowed. michael@0: * michael@0: * @param aUri michael@0: * The URI being installed from michael@0: * @return true if installing is allowed michael@0: */ michael@0: isInstallAllowed: function XPI_isInstallAllowed(aUri) { michael@0: if (!this.isInstallEnabled()) michael@0: return false; michael@0: michael@0: // Direct requests without a referrer are either whitelisted or blocked. michael@0: if (!aUri) michael@0: return this.isDirectRequestWhitelisted(); michael@0: michael@0: // Local referrers can be whitelisted. michael@0: if (this.isFileRequestWhitelisted() && michael@0: (aUri.schemeIs("chrome") || aUri.schemeIs("file"))) michael@0: return true; michael@0: michael@0: this.importPermissions(); michael@0: michael@0: let permission = Services.perms.testPermission(aUri, XPI_PERMISSION); michael@0: if (permission == Ci.nsIPermissionManager.DENY_ACTION) michael@0: return false; michael@0: michael@0: let requireWhitelist = Prefs.getBoolPref(PREF_XPI_WHITELIST_REQUIRED, true); michael@0: if (requireWhitelist && (permission != Ci.nsIPermissionManager.ALLOW_ACTION)) michael@0: return false; michael@0: michael@0: return true; michael@0: }, michael@0: michael@0: /** michael@0: * Called to get an AddonInstall to download and install an add-on from a URL. michael@0: * michael@0: * @param aUrl michael@0: * The URL to be installed michael@0: * @param aHash michael@0: * A hash for the install michael@0: * @param aName michael@0: * A name for the install michael@0: * @param aIcons michael@0: * Icon URLs for the install michael@0: * @param aVersion michael@0: * A version for the install michael@0: * @param aLoadGroup michael@0: * An nsILoadGroup to associate requests with michael@0: * @param aCallback michael@0: * A callback to pass the AddonInstall to michael@0: */ michael@0: getInstallForURL: function XPI_getInstallForURL(aUrl, aHash, aName, aIcons, michael@0: aVersion, aLoadGroup, aCallback) { michael@0: AddonInstall.createDownload(function getInstallForURL_createDownload(aInstall) { michael@0: aCallback(aInstall.wrapper); michael@0: }, aUrl, aHash, aName, aIcons, aVersion, aLoadGroup); michael@0: }, michael@0: michael@0: /** michael@0: * Called to get an AddonInstall to install an add-on from a local file. michael@0: * michael@0: * @param aFile michael@0: * The file to be installed michael@0: * @param aCallback michael@0: * A callback to pass the AddonInstall to michael@0: */ michael@0: getInstallForFile: function XPI_getInstallForFile(aFile, aCallback) { michael@0: AddonInstall.createInstall(function getInstallForFile_createInstall(aInstall) { michael@0: if (aInstall) michael@0: aCallback(aInstall.wrapper); michael@0: else michael@0: aCallback(null); michael@0: }, aFile); michael@0: }, michael@0: michael@0: /** michael@0: * Removes an AddonInstall from the list of active installs. michael@0: * michael@0: * @param install michael@0: * The AddonInstall to remove michael@0: */ michael@0: removeActiveInstall: function XPI_removeActiveInstall(aInstall) { michael@0: this.installs = this.installs.filter(function installFilter(i) i != aInstall); michael@0: }, michael@0: michael@0: /** michael@0: * Called to get an Addon with a particular ID. michael@0: * michael@0: * @param aId michael@0: * The ID of the add-on to retrieve michael@0: * @param aCallback michael@0: * A callback to pass the Addon to michael@0: */ michael@0: getAddonByID: function XPI_getAddonByID(aId, aCallback) { michael@0: XPIDatabase.getVisibleAddonForID (aId, function getAddonByID_getVisibleAddonForID(aAddon) { michael@0: aCallback(createWrapper(aAddon)); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Called to get Addons of a particular type. michael@0: * michael@0: * @param aTypes michael@0: * An array of types to fetch. Can be null to get all types. michael@0: * @param aCallback michael@0: * A callback to pass an array of Addons to michael@0: */ michael@0: getAddonsByTypes: function XPI_getAddonsByTypes(aTypes, aCallback) { michael@0: XPIDatabase.getVisibleAddons(aTypes, function getAddonsByTypes_getVisibleAddons(aAddons) { michael@0: aCallback([createWrapper(a) for each (a in aAddons)]); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Obtain an Addon having the specified Sync GUID. michael@0: * michael@0: * @param aGUID michael@0: * String GUID of add-on to retrieve michael@0: * @param aCallback michael@0: * A callback to pass the Addon to. Receives null if not found. michael@0: */ michael@0: getAddonBySyncGUID: function XPI_getAddonBySyncGUID(aGUID, aCallback) { michael@0: XPIDatabase.getAddonBySyncGUID(aGUID, function getAddonBySyncGUID_getAddonBySyncGUID(aAddon) { michael@0: aCallback(createWrapper(aAddon)); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Called to get Addons that have pending operations. michael@0: * michael@0: * @param aTypes michael@0: * An array of types to fetch. Can be null to get all types michael@0: * @param aCallback michael@0: * A callback to pass an array of Addons to michael@0: */ michael@0: getAddonsWithOperationsByTypes: michael@0: function XPI_getAddonsWithOperationsByTypes(aTypes, aCallback) { michael@0: XPIDatabase.getVisibleAddonsWithPendingOperations(aTypes, michael@0: function getAddonsWithOpsByTypes_getVisibleAddonsWithPendingOps(aAddons) { michael@0: let results = [createWrapper(a) for each (a in aAddons)]; michael@0: XPIProvider.installs.forEach(function(aInstall) { michael@0: if (aInstall.state == AddonManager.STATE_INSTALLED && michael@0: !(aInstall.addon.inDatabase)) michael@0: results.push(createWrapper(aInstall.addon)); michael@0: }); michael@0: aCallback(results); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Called to get the current AddonInstalls, optionally limiting to a list of michael@0: * types. michael@0: * michael@0: * @param aTypes michael@0: * An array of types or null to get all types michael@0: * @param aCallback michael@0: * A callback to pass the array of AddonInstalls to michael@0: */ michael@0: getInstallsByTypes: function XPI_getInstallsByTypes(aTypes, aCallback) { michael@0: let results = []; michael@0: this.installs.forEach(function(aInstall) { michael@0: if (!aTypes || aTypes.indexOf(aInstall.type) >= 0) michael@0: results.push(aInstall.wrapper); michael@0: }); michael@0: aCallback(results); michael@0: }, michael@0: michael@0: /** michael@0: * Synchronously map a URI to the corresponding Addon ID. michael@0: * michael@0: * Mappable URIs are limited to in-application resources belonging to the michael@0: * add-on, such as Javascript compartments, XUL windows, XBL bindings, etc. michael@0: * but do not include URIs from meta data, such as the add-on homepage. michael@0: * michael@0: * @param aURI michael@0: * nsIURI to map or null michael@0: * @return string containing the Addon ID michael@0: * @see AddonManager.mapURIToAddonID michael@0: * @see amIAddonManager.mapURIToAddonID michael@0: */ michael@0: mapURIToAddonID: function XPI_mapURIToAddonID(aURI) { michael@0: this._ensureURIMappings(); michael@0: let resolved = this._resolveURIToFile(aURI); michael@0: if (!resolved) { michael@0: return null; michael@0: } michael@0: resolved = resolved.spec; michael@0: for (let [id, spec] in Iterator(this._uriMappings)) { michael@0: if (resolved.startsWith(spec)) { michael@0: return id; michael@0: } michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: /** michael@0: * Called when a new add-on has been enabled when only one add-on of that type michael@0: * can be enabled. michael@0: * michael@0: * @param aId michael@0: * The ID of the newly enabled add-on michael@0: * @param aType michael@0: * The type of the newly enabled add-on michael@0: * @param aPendingRestart michael@0: * true if the newly enabled add-on will only become enabled after a michael@0: * restart michael@0: */ michael@0: addonChanged: function XPI_addonChanged(aId, aType, aPendingRestart) { michael@0: // We only care about themes in this provider michael@0: if (aType != "theme") michael@0: return; michael@0: michael@0: if (!aId) { michael@0: // Fallback to the default theme when no theme was enabled michael@0: this.enableDefaultTheme(); michael@0: return; michael@0: } michael@0: michael@0: // Look for the previously enabled theme and find the internalName of the michael@0: // currently selected theme michael@0: let previousTheme = null; michael@0: let newSkin = this.defaultSkin; michael@0: let addons = XPIDatabase.getAddonsByType("theme"); michael@0: addons.forEach(function(aTheme) { michael@0: if (!aTheme.visible) michael@0: return; michael@0: if (aTheme.id == aId) michael@0: newSkin = aTheme.internalName; michael@0: else if (aTheme.userDisabled == false && !aTheme.pendingUninstall) michael@0: previousTheme = aTheme; michael@0: }, this); michael@0: michael@0: if (aPendingRestart) { michael@0: Services.prefs.setBoolPref(PREF_DSS_SWITCHPENDING, true); michael@0: Services.prefs.setCharPref(PREF_DSS_SKIN_TO_SELECT, newSkin); michael@0: } michael@0: else if (newSkin == this.currentSkin) { michael@0: try { michael@0: Services.prefs.clearUserPref(PREF_DSS_SWITCHPENDING); michael@0: } michael@0: catch (e) { } michael@0: try { michael@0: Services.prefs.clearUserPref(PREF_DSS_SKIN_TO_SELECT); michael@0: } michael@0: catch (e) { } michael@0: } michael@0: else { michael@0: Services.prefs.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN, newSkin); michael@0: this.currentSkin = newSkin; michael@0: } michael@0: this.selectedSkin = newSkin; michael@0: michael@0: // Flush the preferences to disk so they don't get out of sync with the michael@0: // database michael@0: Services.prefs.savePrefFile(null); michael@0: michael@0: // Mark the previous theme as disabled. This won't cause recursion since michael@0: // only enabled calls notifyAddonChanged. michael@0: if (previousTheme) michael@0: this.updateAddonDisabledState(previousTheme, true); michael@0: }, michael@0: michael@0: /** michael@0: * Update the appDisabled property for all add-ons. michael@0: */ michael@0: updateAddonAppDisabledStates: function XPI_updateAddonAppDisabledStates() { michael@0: let addons = XPIDatabase.getAddons(); michael@0: addons.forEach(function(aAddon) { michael@0: this.updateAddonDisabledState(aAddon); michael@0: }, this); michael@0: }, michael@0: michael@0: /** michael@0: * Update the repositoryAddon property for all add-ons. michael@0: * michael@0: * @param aCallback michael@0: * Function to call when operation is complete. michael@0: */ michael@0: updateAddonRepositoryData: function XPI_updateAddonRepositoryData(aCallback) { michael@0: let self = this; michael@0: XPIDatabase.getVisibleAddons(null, function UARD_getVisibleAddonsCallback(aAddons) { michael@0: let pending = aAddons.length; michael@0: logger.debug("updateAddonRepositoryData found " + pending + " visible add-ons"); michael@0: if (pending == 0) { michael@0: aCallback(); michael@0: return; michael@0: } michael@0: michael@0: function notifyComplete() { michael@0: if (--pending == 0) michael@0: aCallback(); michael@0: } michael@0: michael@0: for (let addon of aAddons) { michael@0: AddonRepository.getCachedAddonByID(addon.id, michael@0: function UARD_getCachedAddonCallback(aRepoAddon) { michael@0: if (aRepoAddon) { michael@0: logger.debug("updateAddonRepositoryData got info for " + addon.id); michael@0: addon._repositoryAddon = aRepoAddon; michael@0: addon.compatibilityOverrides = aRepoAddon.compatibilityOverrides; michael@0: self.updateAddonDisabledState(addon); michael@0: } michael@0: michael@0: notifyComplete(); michael@0: }); michael@0: }; michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * When the previously selected theme is removed this method will be called michael@0: * to enable the default theme. michael@0: */ michael@0: enableDefaultTheme: function XPI_enableDefaultTheme() { michael@0: logger.debug("Activating default theme"); michael@0: let addon = XPIDatabase.getVisibleAddonForInternalName(this.defaultSkin); michael@0: if (addon) { michael@0: if (addon.userDisabled) { michael@0: this.updateAddonDisabledState(addon, false); michael@0: } michael@0: else if (!this.extensionsActive) { michael@0: // During startup we may end up trying to enable the default theme when michael@0: // the database thinks it is already enabled (see f.e. bug 638847). In michael@0: // this case just force the theme preferences to be correct michael@0: Services.prefs.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN, michael@0: addon.internalName); michael@0: this.currentSkin = this.selectedSkin = addon.internalName; michael@0: Prefs.clearUserPref(PREF_DSS_SKIN_TO_SELECT); michael@0: Prefs.clearUserPref(PREF_DSS_SWITCHPENDING); michael@0: } michael@0: else { michael@0: logger.warn("Attempting to activate an already active default theme"); michael@0: } michael@0: } michael@0: else { michael@0: logger.warn("Unable to activate the default theme"); michael@0: } michael@0: }, michael@0: michael@0: onDebugConnectionChange: function(aEvent, aWhat, aConnection) { michael@0: if (aWhat != "opened") michael@0: return; michael@0: michael@0: for (let id of Object.keys(this.bootstrapScopes)) { michael@0: aConnection.setAddonOptions(id, { global: this.bootstrapScopes[id] }); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Notified when a preference we're interested in has changed. michael@0: * michael@0: * @see nsIObserver michael@0: */ michael@0: observe: function XPI_observe(aSubject, aTopic, aData) { michael@0: if (aTopic == NOTIFICATION_FLUSH_PERMISSIONS) { michael@0: if (!aData || aData == XPI_PERMISSION) { michael@0: this.importPermissions(); michael@0: } michael@0: return; michael@0: } michael@0: michael@0: if (aTopic == "nsPref:changed") { michael@0: switch (aData) { michael@0: case PREF_EM_MIN_COMPAT_APP_VERSION: michael@0: case PREF_EM_MIN_COMPAT_PLATFORM_VERSION: michael@0: this.minCompatibleAppVersion = Prefs.getCharPref(PREF_EM_MIN_COMPAT_APP_VERSION, michael@0: null); michael@0: this.minCompatiblePlatformVersion = Prefs.getCharPref(PREF_EM_MIN_COMPAT_PLATFORM_VERSION, michael@0: null); michael@0: this.updateAddonAppDisabledStates(); michael@0: break; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Tests whether enabling an add-on will require a restart. michael@0: * michael@0: * @param aAddon michael@0: * The add-on to test michael@0: * @return true if the operation requires a restart michael@0: */ michael@0: enableRequiresRestart: function XPI_enableRequiresRestart(aAddon) { michael@0: // If the platform couldn't have activated extensions then we can make michael@0: // changes without any restart. michael@0: if (!this.extensionsActive) michael@0: return false; michael@0: michael@0: // If the application is in safe mode then any change can be made without michael@0: // restarting michael@0: if (Services.appinfo.inSafeMode) michael@0: return false; michael@0: michael@0: // Anything that is active is already enabled michael@0: if (aAddon.active) michael@0: return false; michael@0: michael@0: if (aAddon.type == "theme") { michael@0: // If dynamic theme switching is enabled then switching themes does not michael@0: // require a restart michael@0: if (Prefs.getBoolPref(PREF_EM_DSS_ENABLED)) michael@0: return false; michael@0: michael@0: // If the theme is already the theme in use then no restart is necessary. michael@0: // This covers the case where the default theme is in use but a michael@0: // lightweight theme is considered active. michael@0: return aAddon.internalName != this.currentSkin; michael@0: } michael@0: michael@0: return !aAddon.bootstrap; michael@0: }, michael@0: michael@0: /** michael@0: * Tests whether disabling an add-on will require a restart. michael@0: * michael@0: * @param aAddon michael@0: * The add-on to test michael@0: * @return true if the operation requires a restart michael@0: */ michael@0: disableRequiresRestart: function XPI_disableRequiresRestart(aAddon) { michael@0: // If the platform couldn't have activated up extensions then we can make michael@0: // changes without any restart. michael@0: if (!this.extensionsActive) michael@0: return false; michael@0: michael@0: // If the application is in safe mode then any change can be made without michael@0: // restarting michael@0: if (Services.appinfo.inSafeMode) michael@0: return false; michael@0: michael@0: // Anything that isn't active is already disabled michael@0: if (!aAddon.active) michael@0: return false; michael@0: michael@0: if (aAddon.type == "theme") { michael@0: // If dynamic theme switching is enabled then switching themes does not michael@0: // require a restart michael@0: if (Prefs.getBoolPref(PREF_EM_DSS_ENABLED)) michael@0: return false; michael@0: michael@0: // Non-default themes always require a restart to disable since it will michael@0: // be switching from one theme to another or to the default theme and a michael@0: // lightweight theme. michael@0: if (aAddon.internalName != this.defaultSkin) michael@0: return true; michael@0: michael@0: // The default theme requires a restart to disable if we are in the michael@0: // process of switching to a different theme. Note that this makes the michael@0: // disabled flag of operationsRequiringRestart incorrect for the default michael@0: // theme (it will be false most of the time). Bug 520124 would be required michael@0: // to fix it. For the UI this isn't a problem since we never try to michael@0: // disable or uninstall the default theme. michael@0: return this.selectedSkin != this.currentSkin; michael@0: } michael@0: michael@0: return !aAddon.bootstrap; michael@0: }, michael@0: michael@0: /** michael@0: * Tests whether installing an add-on will require a restart. michael@0: * michael@0: * @param aAddon michael@0: * The add-on to test michael@0: * @return true if the operation requires a restart michael@0: */ michael@0: installRequiresRestart: function XPI_installRequiresRestart(aAddon) { michael@0: // If the platform couldn't have activated up extensions then we can make michael@0: // changes without any restart. michael@0: if (!this.extensionsActive) michael@0: return false; michael@0: michael@0: // If the application is in safe mode then any change can be made without michael@0: // restarting michael@0: if (Services.appinfo.inSafeMode) michael@0: return false; michael@0: michael@0: // Add-ons that are already installed don't require a restart to install. michael@0: // This wouldn't normally be called for an already installed add-on (except michael@0: // for forming the operationsRequiringRestart flags) so is really here as michael@0: // a safety measure. michael@0: if (aAddon.inDatabase) michael@0: return false; michael@0: michael@0: // If we have an AddonInstall for this add-on then we can see if there is michael@0: // an existing installed add-on with the same ID michael@0: if ("_install" in aAddon && aAddon._install) { michael@0: // If there is an existing installed add-on and uninstalling it would michael@0: // require a restart then installing the update will also require a michael@0: // restart michael@0: let existingAddon = aAddon._install.existingAddon; michael@0: if (existingAddon && this.uninstallRequiresRestart(existingAddon)) michael@0: return true; michael@0: } michael@0: michael@0: // If the add-on is not going to be active after installation then it michael@0: // doesn't require a restart to install. michael@0: if (isAddonDisabled(aAddon)) michael@0: return false; michael@0: michael@0: // Themes will require a restart (even if dynamic switching is enabled due michael@0: // to some caching issues) and non-bootstrapped add-ons will require a michael@0: // restart michael@0: return aAddon.type == "theme" || !aAddon.bootstrap; michael@0: }, michael@0: michael@0: /** michael@0: * Tests whether uninstalling an add-on will require a restart. michael@0: * michael@0: * @param aAddon michael@0: * The add-on to test michael@0: * @return true if the operation requires a restart michael@0: */ michael@0: uninstallRequiresRestart: function XPI_uninstallRequiresRestart(aAddon) { michael@0: // If the platform couldn't have activated up extensions then we can make michael@0: // changes without any restart. michael@0: if (!this.extensionsActive) michael@0: return false; michael@0: michael@0: // If the application is in safe mode then any change can be made without michael@0: // restarting michael@0: if (Services.appinfo.inSafeMode) michael@0: return false; michael@0: michael@0: // If the add-on can be disabled without a restart then it can also be michael@0: // uninstalled without a restart michael@0: return this.disableRequiresRestart(aAddon); michael@0: }, michael@0: michael@0: /** michael@0: * Loads a bootstrapped add-on's bootstrap.js into a sandbox and the reason michael@0: * values as constants in the scope. This will also add information about the michael@0: * add-on to the bootstrappedAddons dictionary and notify the crash reporter michael@0: * that new add-ons have been loaded. michael@0: * michael@0: * @param aId michael@0: * The add-on's ID michael@0: * @param aFile michael@0: * The nsIFile for the add-on michael@0: * @param aVersion michael@0: * The add-on's version michael@0: * @param aType michael@0: * The type for the add-on michael@0: * @return a JavaScript scope michael@0: */ michael@0: loadBootstrapScope: function XPI_loadBootstrapScope(aId, aFile, aVersion, aType) { michael@0: // Mark the add-on as active for the crash reporter before loading michael@0: this.bootstrappedAddons[aId] = { michael@0: version: aVersion, michael@0: type: aType, michael@0: descriptor: aFile.persistentDescriptor michael@0: }; michael@0: this.persistBootstrappedAddons(); michael@0: this.addAddonsToCrashReporter(); michael@0: michael@0: // Locales only contain chrome and can't have bootstrap scripts michael@0: if (aType == "locale") { michael@0: this.bootstrapScopes[aId] = null; michael@0: return; michael@0: } michael@0: michael@0: logger.debug("Loading bootstrap scope from " + aFile.path); michael@0: michael@0: let principal = Cc["@mozilla.org/systemprincipal;1"]. michael@0: createInstance(Ci.nsIPrincipal); michael@0: michael@0: if (!aFile.exists()) { michael@0: this.bootstrapScopes[aId] = michael@0: new Cu.Sandbox(principal, { sandboxName: aFile.path, michael@0: wantGlobalProperties: ["indexedDB"], michael@0: metadata: { addonID: aId } }); michael@0: logger.error("Attempted to load bootstrap scope from missing directory " + aFile.path); michael@0: return; michael@0: } michael@0: michael@0: let uri = getURIForResourceInFile(aFile, "bootstrap.js").spec; michael@0: if (aType == "dictionary") michael@0: uri = "resource://gre/modules/addons/SpellCheckDictionaryBootstrap.js" michael@0: michael@0: this.bootstrapScopes[aId] = michael@0: new Cu.Sandbox(principal, { sandboxName: uri, michael@0: wantGlobalProperties: ["indexedDB"], michael@0: metadata: { addonID: aId, URI: uri } }); michael@0: michael@0: let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]. michael@0: createInstance(Ci.mozIJSSubScriptLoader); michael@0: michael@0: // Add a mapping for XPIProvider.mapURIToAddonID michael@0: this._addURIMapping(aId, aFile); michael@0: michael@0: try { michael@0: // Copy the reason values from the global object into the bootstrap scope. michael@0: for (let name in BOOTSTRAP_REASONS) michael@0: this.bootstrapScopes[aId][name] = BOOTSTRAP_REASONS[name]; michael@0: michael@0: // Add other stuff that extensions want. michael@0: const features = [ "Worker", "ChromeWorker" ]; michael@0: michael@0: for (let feature of features) michael@0: this.bootstrapScopes[aId][feature] = gGlobalScope[feature]; michael@0: michael@0: // As we don't want our caller to control the JS version used for the michael@0: // bootstrap file, we run loadSubScript within the context of the michael@0: // sandbox with the latest JS version set explicitly. michael@0: this.bootstrapScopes[aId].__SCRIPT_URI_SPEC__ = uri; michael@0: Components.utils.evalInSandbox( michael@0: "Components.classes['@mozilla.org/moz/jssubscript-loader;1'] \ michael@0: .createInstance(Components.interfaces.mozIJSSubScriptLoader) \ michael@0: .loadSubScript(__SCRIPT_URI_SPEC__);", this.bootstrapScopes[aId], "ECMAv5"); michael@0: } michael@0: catch (e) { michael@0: logger.warn("Error loading bootstrap.js for " + aId, e); michael@0: } michael@0: michael@0: try { michael@0: BrowserToolboxProcess.setAddonOptions(aId, { global: this.bootstrapScopes[aId] }); michael@0: } michael@0: catch (e) { michael@0: // BrowserToolboxProcess is not available in all applications michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Unloads a bootstrap scope by dropping all references to it and then michael@0: * updating the list of active add-ons with the crash reporter. michael@0: * michael@0: * @param aId michael@0: * The add-on's ID michael@0: */ michael@0: unloadBootstrapScope: function XPI_unloadBootstrapScope(aId) { michael@0: delete this.bootstrapScopes[aId]; michael@0: delete this.bootstrappedAddons[aId]; michael@0: this.persistBootstrappedAddons(); michael@0: this.addAddonsToCrashReporter(); michael@0: michael@0: try { michael@0: BrowserToolboxProcess.setAddonOptions(aId, { global: null }); michael@0: } michael@0: catch (e) { michael@0: // BrowserToolboxProcess is not available in all applications michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Calls a bootstrap method for an add-on. michael@0: * michael@0: * @param aId michael@0: * The ID of the add-on michael@0: * @param aVersion michael@0: * The version of the add-on michael@0: * @param aType michael@0: * The type for the add-on michael@0: * @param aFile michael@0: * The nsIFile for the add-on michael@0: * @param aMethod michael@0: * The name of the bootstrap method to call michael@0: * @param aReason michael@0: * The reason flag to pass to the bootstrap's startup method michael@0: * @param aExtraParams michael@0: * An object of additional key/value pairs to pass to the method in michael@0: * the params argument michael@0: */ michael@0: callBootstrapMethod: function XPI_callBootstrapMethod(aId, aVersion, aType, aFile, michael@0: aMethod, aReason, aExtraParams) { michael@0: // Never call any bootstrap methods in safe mode michael@0: if (Services.appinfo.inSafeMode) michael@0: return; michael@0: michael@0: let timeStart = new Date(); michael@0: if (aMethod == "startup") { michael@0: logger.debug("Registering manifest for " + aFile.path); michael@0: Components.manager.addBootstrappedManifestLocation(aFile); michael@0: } michael@0: michael@0: try { michael@0: // Load the scope if it hasn't already been loaded michael@0: if (!(aId in this.bootstrapScopes)) michael@0: this.loadBootstrapScope(aId, aFile, aVersion, aType); michael@0: michael@0: // Nothing to call for locales michael@0: if (aType == "locale") michael@0: return; michael@0: michael@0: if (!(aMethod in this.bootstrapScopes[aId])) { michael@0: logger.warn("Add-on " + aId + " is missing bootstrap method " + aMethod); michael@0: return; michael@0: } michael@0: michael@0: let params = { michael@0: id: aId, michael@0: version: aVersion, michael@0: installPath: aFile.clone(), michael@0: resourceURI: getURIForResourceInFile(aFile, "") michael@0: }; michael@0: michael@0: if (aExtraParams) { michael@0: for (let key in aExtraParams) { michael@0: params[key] = aExtraParams[key]; michael@0: } michael@0: } michael@0: michael@0: logger.debug("Calling bootstrap method " + aMethod + " on " + aId + " version " + michael@0: aVersion); michael@0: try { michael@0: this.bootstrapScopes[aId][aMethod](params, aReason); michael@0: } michael@0: catch (e) { michael@0: logger.warn("Exception running bootstrap method " + aMethod + " on " + aId, e); michael@0: } michael@0: } michael@0: finally { michael@0: if (aMethod == "shutdown" && aReason != BOOTSTRAP_REASONS.APP_SHUTDOWN) { michael@0: logger.debug("Removing manifest for " + aFile.path); michael@0: Components.manager.removeBootstrappedManifestLocation(aFile); michael@0: } michael@0: this.setTelemetry(aId, aMethod + "_MS", new Date() - timeStart); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Updates the disabled state for an add-on. Its appDisabled property will be michael@0: * calculated and if the add-on is changed the database will be saved and michael@0: * appropriate notifications will be sent out to the registered AddonListeners. michael@0: * michael@0: * @param aAddon michael@0: * The DBAddonInternal to update michael@0: * @param aUserDisabled michael@0: * Value for the userDisabled property. If undefined the value will michael@0: * not change michael@0: * @param aSoftDisabled michael@0: * Value for the softDisabled property. If undefined the value will michael@0: * not change. If true this will force userDisabled to be true michael@0: * @throws if addon is not a DBAddonInternal michael@0: */ michael@0: updateAddonDisabledState: function XPI_updateAddonDisabledState(aAddon, michael@0: aUserDisabled, michael@0: aSoftDisabled) { michael@0: if (!(aAddon.inDatabase)) michael@0: throw new Error("Can only update addon states for installed addons."); michael@0: if (aUserDisabled !== undefined && aSoftDisabled !== undefined) { michael@0: throw new Error("Cannot change userDisabled and softDisabled at the " + michael@0: "same time"); michael@0: } michael@0: michael@0: if (aUserDisabled === undefined) { michael@0: aUserDisabled = aAddon.userDisabled; michael@0: } michael@0: else if (!aUserDisabled) { michael@0: // If enabling the add-on then remove softDisabled michael@0: aSoftDisabled = false; michael@0: } michael@0: michael@0: // If not changing softDisabled or the add-on is already userDisabled then michael@0: // use the existing value for softDisabled michael@0: if (aSoftDisabled === undefined || aUserDisabled) michael@0: aSoftDisabled = aAddon.softDisabled; michael@0: michael@0: let appDisabled = !isUsableAddon(aAddon); michael@0: // No change means nothing to do here michael@0: if (aAddon.userDisabled == aUserDisabled && michael@0: aAddon.appDisabled == appDisabled && michael@0: aAddon.softDisabled == aSoftDisabled) michael@0: return; michael@0: michael@0: let wasDisabled = isAddonDisabled(aAddon); michael@0: let isDisabled = aUserDisabled || aSoftDisabled || appDisabled; michael@0: michael@0: // If appDisabled changes but the result of isAddonDisabled() doesn't, michael@0: // no onDisabling/onEnabling is sent - so send a onPropertyChanged. michael@0: let appDisabledChanged = aAddon.appDisabled != appDisabled; michael@0: michael@0: // Update the properties in the database. michael@0: // We never persist this for experiments because the disabled flags michael@0: // are controlled by the Experiments Manager. michael@0: if (aAddon.type != "experiment") { michael@0: XPIDatabase.setAddonProperties(aAddon, { michael@0: userDisabled: aUserDisabled, michael@0: appDisabled: appDisabled, michael@0: softDisabled: aSoftDisabled michael@0: }); michael@0: } michael@0: michael@0: if (appDisabledChanged) { michael@0: AddonManagerPrivate.callAddonListeners("onPropertyChanged", michael@0: aAddon, michael@0: ["appDisabled"]); michael@0: } michael@0: michael@0: // If the add-on is not visible or the add-on is not changing state then michael@0: // there is no need to do anything else michael@0: if (!aAddon.visible || (wasDisabled == isDisabled)) michael@0: return; michael@0: michael@0: // Flag that active states in the database need to be updated on shutdown michael@0: Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true); michael@0: michael@0: let wrapper = createWrapper(aAddon); michael@0: // Have we just gone back to the current state? michael@0: if (isDisabled != aAddon.active) { michael@0: AddonManagerPrivate.callAddonListeners("onOperationCancelled", wrapper); michael@0: } michael@0: else { michael@0: if (isDisabled) { michael@0: var needsRestart = this.disableRequiresRestart(aAddon); michael@0: AddonManagerPrivate.callAddonListeners("onDisabling", wrapper, michael@0: needsRestart); michael@0: } michael@0: else { michael@0: needsRestart = this.enableRequiresRestart(aAddon); michael@0: AddonManagerPrivate.callAddonListeners("onEnabling", wrapper, michael@0: needsRestart); michael@0: } michael@0: michael@0: if (!needsRestart) { michael@0: XPIDatabase.updateAddonActive(aAddon, !isDisabled); michael@0: if (isDisabled) { michael@0: if (aAddon.bootstrap) { michael@0: let file = aAddon._installLocation.getLocationForID(aAddon.id); michael@0: this.callBootstrapMethod(aAddon.id, aAddon.version, aAddon.type, file, "shutdown", michael@0: BOOTSTRAP_REASONS.ADDON_DISABLE); michael@0: this.unloadBootstrapScope(aAddon.id); michael@0: } michael@0: AddonManagerPrivate.callAddonListeners("onDisabled", wrapper); michael@0: } michael@0: else { michael@0: if (aAddon.bootstrap) { michael@0: let file = aAddon._installLocation.getLocationForID(aAddon.id); michael@0: this.callBootstrapMethod(aAddon.id, aAddon.version, aAddon.type, file, "startup", michael@0: BOOTSTRAP_REASONS.ADDON_ENABLE); michael@0: } michael@0: AddonManagerPrivate.callAddonListeners("onEnabled", wrapper); michael@0: } michael@0: } michael@0: } michael@0: michael@0: // Notify any other providers that a new theme has been enabled michael@0: if (aAddon.type == "theme" && !isDisabled) michael@0: AddonManagerPrivate.notifyAddonChanged(aAddon.id, aAddon.type, needsRestart); michael@0: }, michael@0: michael@0: /** michael@0: * Uninstalls an add-on, immediately if possible or marks it as pending michael@0: * uninstall if not. michael@0: * michael@0: * @param aAddon michael@0: * The DBAddonInternal to uninstall michael@0: * @throws if the addon cannot be uninstalled because it is in an install michael@0: * location that does not allow it michael@0: */ michael@0: uninstallAddon: function XPI_uninstallAddon(aAddon) { michael@0: if (!(aAddon.inDatabase)) michael@0: throw new Error("Can only uninstall installed addons."); michael@0: michael@0: if (aAddon._installLocation.locked) michael@0: throw new Error("Cannot uninstall addons from locked install locations"); michael@0: michael@0: if ("_hasResourceCache" in aAddon) michael@0: aAddon._hasResourceCache = new Map(); michael@0: michael@0: if (aAddon._updateCheck) { michael@0: logger.debug("Cancel in-progress update check for " + aAddon.id); michael@0: aAddon._updateCheck.cancel(); michael@0: } michael@0: michael@0: // Inactive add-ons don't require a restart to uninstall michael@0: let requiresRestart = this.uninstallRequiresRestart(aAddon); michael@0: michael@0: if (requiresRestart) { michael@0: // We create an empty directory in the staging directory to indicate that michael@0: // an uninstall is necessary on next startup. michael@0: let stage = aAddon._installLocation.getStagingDir(); michael@0: stage.append(aAddon.id); michael@0: if (!stage.exists()) michael@0: stage.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); michael@0: michael@0: XPIDatabase.setAddonProperties(aAddon, { michael@0: pendingUninstall: true michael@0: }); michael@0: Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true); michael@0: } michael@0: michael@0: // If the add-on is not visible then there is no need to notify listeners. michael@0: if (!aAddon.visible) michael@0: return; michael@0: michael@0: let wrapper = createWrapper(aAddon); michael@0: AddonManagerPrivate.callAddonListeners("onUninstalling", wrapper, michael@0: requiresRestart); michael@0: michael@0: // Reveal the highest priority add-on with the same ID michael@0: function revealAddon(aAddon) { michael@0: XPIDatabase.makeAddonVisible(aAddon); michael@0: michael@0: let wrappedAddon = createWrapper(aAddon); michael@0: AddonManagerPrivate.callAddonListeners("onInstalling", wrappedAddon, false); michael@0: michael@0: if (!isAddonDisabled(aAddon) && !XPIProvider.enableRequiresRestart(aAddon)) { michael@0: XPIDatabase.updateAddonActive(aAddon, true); michael@0: } michael@0: michael@0: if (aAddon.bootstrap) { michael@0: let file = aAddon._installLocation.getLocationForID(aAddon.id); michael@0: XPIProvider.callBootstrapMethod(aAddon.id, aAddon.version, aAddon.type, file, michael@0: "install", BOOTSTRAP_REASONS.ADDON_INSTALL); michael@0: michael@0: if (aAddon.active) { michael@0: XPIProvider.callBootstrapMethod(aAddon.id, aAddon.version, aAddon.type, file, michael@0: "startup", BOOTSTRAP_REASONS.ADDON_INSTALL); michael@0: } michael@0: else { michael@0: XPIProvider.unloadBootstrapScope(aAddon.id); michael@0: } michael@0: } michael@0: michael@0: // We always send onInstalled even if a restart is required to enable michael@0: // the revealed add-on michael@0: AddonManagerPrivate.callAddonListeners("onInstalled", wrappedAddon); michael@0: } michael@0: michael@0: function checkInstallLocation(aPos) { michael@0: if (aPos < 0) michael@0: return; michael@0: michael@0: let location = XPIProvider.installLocations[aPos]; michael@0: XPIDatabase.getAddonInLocation(aAddon.id, location.name, michael@0: function checkInstallLocation_getAddonInLocation(aNewAddon) { michael@0: if (aNewAddon) michael@0: revealAddon(aNewAddon); michael@0: else michael@0: checkInstallLocation(aPos - 1); michael@0: }) michael@0: } michael@0: michael@0: if (!requiresRestart) { michael@0: if (aAddon.bootstrap) { michael@0: let file = aAddon._installLocation.getLocationForID(aAddon.id); michael@0: if (aAddon.active) { michael@0: this.callBootstrapMethod(aAddon.id, aAddon.version, aAddon.type, file, michael@0: "shutdown", michael@0: BOOTSTRAP_REASONS.ADDON_UNINSTALL); michael@0: } michael@0: michael@0: this.callBootstrapMethod(aAddon.id, aAddon.version, aAddon.type, file, michael@0: "uninstall", michael@0: BOOTSTRAP_REASONS.ADDON_UNINSTALL); michael@0: this.unloadBootstrapScope(aAddon.id); michael@0: flushStartupCache(); michael@0: } michael@0: aAddon._installLocation.uninstallAddon(aAddon.id); michael@0: XPIDatabase.removeAddonMetadata(aAddon); michael@0: AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper); michael@0: michael@0: checkInstallLocation(this.installLocations.length - 1); michael@0: } michael@0: michael@0: // Notify any other providers that a new theme has been enabled michael@0: if (aAddon.type == "theme" && aAddon.active) michael@0: AddonManagerPrivate.notifyAddonChanged(null, aAddon.type, requiresRestart); michael@0: }, michael@0: michael@0: /** michael@0: * Cancels the pending uninstall of an add-on. michael@0: * michael@0: * @param aAddon michael@0: * The DBAddonInternal to cancel uninstall for michael@0: */ michael@0: cancelUninstallAddon: function XPI_cancelUninstallAddon(aAddon) { michael@0: if (!(aAddon.inDatabase)) michael@0: throw new Error("Can only cancel uninstall for installed addons."); michael@0: michael@0: aAddon._installLocation.cleanStagingDir([aAddon.id]); michael@0: michael@0: XPIDatabase.setAddonProperties(aAddon, { michael@0: pendingUninstall: false michael@0: }); michael@0: michael@0: if (!aAddon.visible) michael@0: return; michael@0: michael@0: Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true); michael@0: michael@0: // TODO hide hidden add-ons (bug 557710) michael@0: let wrapper = createWrapper(aAddon); michael@0: AddonManagerPrivate.callAddonListeners("onOperationCancelled", wrapper); michael@0: michael@0: // Notify any other providers that this theme is now enabled again. michael@0: if (aAddon.type == "theme" && aAddon.active) michael@0: AddonManagerPrivate.notifyAddonChanged(aAddon.id, aAddon.type, false); michael@0: } michael@0: }; michael@0: michael@0: function getHashStringForCrypto(aCrypto) { michael@0: // return the two-digit hexadecimal code for a byte michael@0: function toHexString(charCode) michael@0: ("0" + charCode.toString(16)).slice(-2); michael@0: michael@0: // convert the binary hash data to a hex string. michael@0: let binary = aCrypto.finish(false); michael@0: return [toHexString(binary.charCodeAt(i)) for (i in binary)].join("").toLowerCase() michael@0: } michael@0: michael@0: /** michael@0: * Instantiates an AddonInstall. michael@0: * michael@0: * @param aInstallLocation michael@0: * The install location the add-on will be installed into michael@0: * @param aUrl michael@0: * The nsIURL to get the add-on from. If this is an nsIFileURL then michael@0: * the add-on will not need to be downloaded michael@0: * @param aHash michael@0: * An optional hash for the add-on michael@0: * @param aReleaseNotesURI michael@0: * An optional nsIURI of release notes for the add-on michael@0: * @param aExistingAddon michael@0: * The add-on this install will update if known michael@0: * @param aLoadGroup michael@0: * The nsILoadGroup to associate any requests with michael@0: * @throws if the url is the url of a local file and the hash does not match michael@0: * or the add-on does not contain an valid install manifest michael@0: */ michael@0: function AddonInstall(aInstallLocation, aUrl, aHash, aReleaseNotesURI, michael@0: aExistingAddon, aLoadGroup) { michael@0: this.wrapper = new AddonInstallWrapper(this); michael@0: this.installLocation = aInstallLocation; michael@0: this.sourceURI = aUrl; michael@0: this.releaseNotesURI = aReleaseNotesURI; michael@0: if (aHash) { michael@0: let hashSplit = aHash.toLowerCase().split(":"); michael@0: this.originalHash = { michael@0: algorithm: hashSplit[0], michael@0: data: hashSplit[1] michael@0: }; michael@0: } michael@0: this.hash = this.originalHash; michael@0: this.loadGroup = aLoadGroup; michael@0: this.listeners = []; michael@0: this.icons = {}; michael@0: this.existingAddon = aExistingAddon; michael@0: this.error = 0; michael@0: if (aLoadGroup) michael@0: this.window = aLoadGroup.notificationCallbacks michael@0: .getInterface(Ci.nsIDOMWindow); michael@0: else michael@0: this.window = null; michael@0: michael@0: // Giving each instance of AddonInstall a reference to the logger. michael@0: this.logger = logger; michael@0: } michael@0: michael@0: AddonInstall.prototype = { michael@0: installLocation: null, michael@0: wrapper: null, michael@0: stream: null, michael@0: crypto: null, michael@0: originalHash: null, michael@0: hash: null, michael@0: loadGroup: null, michael@0: badCertHandler: null, michael@0: listeners: null, michael@0: restartDownload: false, michael@0: michael@0: name: null, michael@0: type: null, michael@0: version: null, michael@0: icons: null, michael@0: releaseNotesURI: null, michael@0: sourceURI: null, michael@0: file: null, michael@0: ownsTempFile: false, michael@0: certificate: null, michael@0: certName: null, michael@0: michael@0: linkedInstalls: null, michael@0: existingAddon: null, michael@0: addon: null, michael@0: michael@0: state: null, michael@0: error: null, michael@0: progress: null, michael@0: maxProgress: null, michael@0: michael@0: /** michael@0: * Initialises this install to be a staged install waiting to be applied michael@0: * michael@0: * @param aManifest michael@0: * The cached manifest for the staged install michael@0: */ michael@0: initStagedInstall: function AI_initStagedInstall(aManifest) { michael@0: this.name = aManifest.name; michael@0: this.type = aManifest.type; michael@0: this.version = aManifest.version; michael@0: this.icons = aManifest.icons; michael@0: this.releaseNotesURI = aManifest.releaseNotesURI ? michael@0: NetUtil.newURI(aManifest.releaseNotesURI) : michael@0: null michael@0: this.sourceURI = aManifest.sourceURI ? michael@0: NetUtil.newURI(aManifest.sourceURI) : michael@0: null; michael@0: this.file = null; michael@0: this.addon = aManifest; michael@0: michael@0: this.state = AddonManager.STATE_INSTALLED; michael@0: michael@0: XPIProvider.installs.push(this); michael@0: }, michael@0: michael@0: /** michael@0: * Initialises this install to be an install from a local file. michael@0: * michael@0: * @param aCallback michael@0: * The callback to pass the initialised AddonInstall to michael@0: */ michael@0: initLocalInstall: function AI_initLocalInstall(aCallback) { michael@0: aCallback = makeSafe(aCallback); michael@0: this.file = this.sourceURI.QueryInterface(Ci.nsIFileURL).file; michael@0: michael@0: if (!this.file.exists()) { michael@0: logger.warn("XPI file " + this.file.path + " does not exist"); michael@0: this.state = AddonManager.STATE_DOWNLOAD_FAILED; michael@0: this.error = AddonManager.ERROR_NETWORK_FAILURE; michael@0: aCallback(this); michael@0: return; michael@0: } michael@0: michael@0: this.state = AddonManager.STATE_DOWNLOADED; michael@0: this.progress = this.file.fileSize; michael@0: this.maxProgress = this.file.fileSize; michael@0: michael@0: if (this.hash) { michael@0: let crypto = Cc["@mozilla.org/security/hash;1"]. michael@0: createInstance(Ci.nsICryptoHash); michael@0: try { michael@0: crypto.initWithString(this.hash.algorithm); michael@0: } michael@0: catch (e) { michael@0: logger.warn("Unknown hash algorithm '" + this.hash.algorithm + "' for addon " + this.sourceURI.spec, e); michael@0: this.state = AddonManager.STATE_DOWNLOAD_FAILED; michael@0: this.error = AddonManager.ERROR_INCORRECT_HASH; michael@0: aCallback(this); michael@0: return; michael@0: } michael@0: michael@0: let fis = Cc["@mozilla.org/network/file-input-stream;1"]. michael@0: createInstance(Ci.nsIFileInputStream); michael@0: fis.init(this.file, -1, -1, false); michael@0: crypto.updateFromStream(fis, this.file.fileSize); michael@0: let calculatedHash = getHashStringForCrypto(crypto); michael@0: if (calculatedHash != this.hash.data) { michael@0: logger.warn("File hash (" + calculatedHash + ") did not match provided hash (" + michael@0: this.hash.data + ")"); michael@0: this.state = AddonManager.STATE_DOWNLOAD_FAILED; michael@0: this.error = AddonManager.ERROR_INCORRECT_HASH; michael@0: aCallback(this); michael@0: return; michael@0: } michael@0: } michael@0: michael@0: try { michael@0: let self = this; michael@0: this.loadManifest(function initLocalInstall_loadManifest() { michael@0: XPIDatabase.getVisibleAddonForID(self.addon.id, function initLocalInstall_getVisibleAddon(aAddon) { michael@0: self.existingAddon = aAddon; michael@0: if (aAddon) michael@0: applyBlocklistChanges(aAddon, self.addon); michael@0: self.addon.updateDate = Date.now(); michael@0: self.addon.installDate = aAddon ? aAddon.installDate : self.addon.updateDate; michael@0: michael@0: if (!self.addon.isCompatible) { michael@0: // TODO Should we send some event here? michael@0: self.state = AddonManager.STATE_CHECKING; michael@0: new UpdateChecker(self.addon, { michael@0: onUpdateFinished: function updateChecker_onUpdateFinished(aAddon) { michael@0: self.state = AddonManager.STATE_DOWNLOADED; michael@0: XPIProvider.installs.push(self); michael@0: AddonManagerPrivate.callInstallListeners("onNewInstall", michael@0: self.listeners, michael@0: self.wrapper); michael@0: michael@0: aCallback(self); michael@0: } michael@0: }, AddonManager.UPDATE_WHEN_ADDON_INSTALLED); michael@0: } michael@0: else { michael@0: XPIProvider.installs.push(self); michael@0: AddonManagerPrivate.callInstallListeners("onNewInstall", michael@0: self.listeners, michael@0: self.wrapper); michael@0: michael@0: aCallback(self); michael@0: } michael@0: }); michael@0: }); michael@0: } michael@0: catch (e) { michael@0: logger.warn("Invalid XPI", e); michael@0: this.state = AddonManager.STATE_DOWNLOAD_FAILED; michael@0: this.error = AddonManager.ERROR_CORRUPT_FILE; michael@0: aCallback(this); michael@0: return; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Initialises this install to be a download from a remote url. michael@0: * michael@0: * @param aCallback michael@0: * The callback to pass the initialised AddonInstall to michael@0: * @param aName michael@0: * An optional name for the add-on michael@0: * @param aType michael@0: * An optional type for the add-on michael@0: * @param aIcons michael@0: * Optional icons for the add-on michael@0: * @param aVersion michael@0: * An optional version for the add-on michael@0: */ michael@0: initAvailableDownload: function AI_initAvailableDownload(aName, aType, aIcons, aVersion, aCallback) { michael@0: this.state = AddonManager.STATE_AVAILABLE; michael@0: this.name = aName; michael@0: this.type = aType; michael@0: this.version = aVersion; michael@0: this.icons = aIcons; michael@0: this.progress = 0; michael@0: this.maxProgress = -1; michael@0: michael@0: XPIProvider.installs.push(this); michael@0: AddonManagerPrivate.callInstallListeners("onNewInstall", this.listeners, michael@0: this.wrapper); michael@0: michael@0: makeSafe(aCallback)(this); michael@0: }, michael@0: michael@0: /** michael@0: * Starts installation of this add-on from whatever state it is currently at michael@0: * if possible. michael@0: * michael@0: * @throws if installation cannot proceed from the current state michael@0: */ michael@0: install: function AI_install() { michael@0: switch (this.state) { michael@0: case AddonManager.STATE_AVAILABLE: michael@0: this.startDownload(); michael@0: break; michael@0: case AddonManager.STATE_DOWNLOADED: michael@0: this.startInstall(); michael@0: break; michael@0: case AddonManager.STATE_DOWNLOAD_FAILED: michael@0: case AddonManager.STATE_INSTALL_FAILED: michael@0: case AddonManager.STATE_CANCELLED: michael@0: this.removeTemporaryFile(); michael@0: this.state = AddonManager.STATE_AVAILABLE; michael@0: this.error = 0; michael@0: this.progress = 0; michael@0: this.maxProgress = -1; michael@0: this.hash = this.originalHash; michael@0: XPIProvider.installs.push(this); michael@0: this.startDownload(); michael@0: break; michael@0: case AddonManager.STATE_DOWNLOADING: michael@0: case AddonManager.STATE_CHECKING: michael@0: case AddonManager.STATE_INSTALLING: michael@0: // Installation is already running michael@0: return; michael@0: default: michael@0: throw new Error("Cannot start installing from this state"); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Cancels installation of this add-on. michael@0: * michael@0: * @throws if installation cannot be cancelled from the current state michael@0: */ michael@0: cancel: function AI_cancel() { michael@0: switch (this.state) { michael@0: case AddonManager.STATE_DOWNLOADING: michael@0: if (this.channel) michael@0: this.channel.cancel(Cr.NS_BINDING_ABORTED); michael@0: case AddonManager.STATE_AVAILABLE: michael@0: case AddonManager.STATE_DOWNLOADED: michael@0: logger.debug("Cancelling download of " + this.sourceURI.spec); michael@0: this.state = AddonManager.STATE_CANCELLED; michael@0: XPIProvider.removeActiveInstall(this); michael@0: AddonManagerPrivate.callInstallListeners("onDownloadCancelled", michael@0: this.listeners, this.wrapper); michael@0: this.removeTemporaryFile(); michael@0: break; michael@0: case AddonManager.STATE_INSTALLED: michael@0: logger.debug("Cancelling install of " + this.addon.id); michael@0: let xpi = this.installLocation.getStagingDir(); michael@0: xpi.append(this.addon.id + ".xpi"); michael@0: flushJarCache(xpi); michael@0: this.installLocation.cleanStagingDir([this.addon.id, this.addon.id + ".xpi", michael@0: this.addon.id + ".json"]); michael@0: this.state = AddonManager.STATE_CANCELLED; michael@0: XPIProvider.removeActiveInstall(this); michael@0: michael@0: if (this.existingAddon) { michael@0: delete this.existingAddon.pendingUpgrade; michael@0: this.existingAddon.pendingUpgrade = null; michael@0: } michael@0: michael@0: AddonManagerPrivate.callAddonListeners("onOperationCancelled", createWrapper(this.addon)); michael@0: michael@0: AddonManagerPrivate.callInstallListeners("onInstallCancelled", michael@0: this.listeners, this.wrapper); michael@0: break; michael@0: default: michael@0: throw new Error("Cannot cancel install of " + this.sourceURI.spec + michael@0: " from this state (" + this.state + ")"); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Adds an InstallListener for this instance if the listener is not already michael@0: * registered. michael@0: * michael@0: * @param aListener michael@0: * The InstallListener to add michael@0: */ michael@0: addListener: function AI_addListener(aListener) { michael@0: if (!this.listeners.some(function addListener_matchListener(i) { return i == aListener; })) michael@0: this.listeners.push(aListener); michael@0: }, michael@0: michael@0: /** michael@0: * Removes an InstallListener for this instance if it is registered. michael@0: * michael@0: * @param aListener michael@0: * The InstallListener to remove michael@0: */ michael@0: removeListener: function AI_removeListener(aListener) { michael@0: this.listeners = this.listeners.filter(function removeListener_filterListener(i) { michael@0: return i != aListener; michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Removes the temporary file owned by this AddonInstall if there is one. michael@0: */ michael@0: removeTemporaryFile: function AI_removeTemporaryFile() { michael@0: // Only proceed if this AddonInstall owns its XPI file michael@0: if (!this.ownsTempFile) { michael@0: this.logger.debug("removeTemporaryFile: " + this.sourceURI.spec + " does not own temp file"); michael@0: return; michael@0: } michael@0: michael@0: try { michael@0: this.logger.debug("removeTemporaryFile: " + this.sourceURI.spec + " removing temp file " + michael@0: this.file.path); michael@0: this.file.remove(true); michael@0: this.ownsTempFile = false; michael@0: } michael@0: catch (e) { michael@0: this.logger.warn("Failed to remove temporary file " + this.file.path + " for addon " + michael@0: this.sourceURI.spec, michael@0: e); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Updates the sourceURI and releaseNotesURI values on the Addon being michael@0: * installed by this AddonInstall instance. michael@0: */ michael@0: updateAddonURIs: function AI_updateAddonURIs() { michael@0: this.addon.sourceURI = this.sourceURI.spec; michael@0: if (this.releaseNotesURI) michael@0: this.addon.releaseNotesURI = this.releaseNotesURI.spec; michael@0: }, michael@0: michael@0: /** michael@0: * Loads add-on manifests from a multi-package XPI file. Each of the michael@0: * XPI and JAR files contained in the XPI will be extracted. Any that michael@0: * do not contain valid add-ons will be ignored. The first valid add-on will michael@0: * be installed by this AddonInstall instance, the rest will have new michael@0: * AddonInstall instances created for them. michael@0: * michael@0: * @param aZipReader michael@0: * An open nsIZipReader for the multi-package XPI's files. This will michael@0: * be closed before this method returns. michael@0: * @param aCallback michael@0: * A function to call when all of the add-on manifests have been michael@0: * loaded. Because this loadMultipackageManifests is an internal API michael@0: * we don't exception-wrap this callback michael@0: */ michael@0: _loadMultipackageManifests: function AI_loadMultipackageManifests(aZipReader, michael@0: aCallback) { michael@0: let files = []; michael@0: let entries = aZipReader.findEntries("(*.[Xx][Pp][Ii]|*.[Jj][Aa][Rr])"); michael@0: while (entries.hasMore()) { michael@0: let entryName = entries.getNext(); michael@0: var target = getTemporaryFile(); michael@0: try { michael@0: aZipReader.extract(entryName, target); michael@0: files.push(target); michael@0: } michael@0: catch (e) { michael@0: logger.warn("Failed to extract " + entryName + " from multi-package " + michael@0: "XPI", e); michael@0: target.remove(false); michael@0: } michael@0: } michael@0: michael@0: aZipReader.close(); michael@0: michael@0: if (files.length == 0) { michael@0: throw new Error("Multi-package XPI does not contain any packages " + michael@0: "to install"); michael@0: } michael@0: michael@0: let addon = null; michael@0: michael@0: // Find the first file that has a valid install manifest and use it for michael@0: // the add-on that this AddonInstall instance will install. michael@0: while (files.length > 0) { michael@0: this.removeTemporaryFile(); michael@0: this.file = files.shift(); michael@0: this.ownsTempFile = true; michael@0: try { michael@0: addon = loadManifestFromZipFile(this.file); michael@0: break; michael@0: } michael@0: catch (e) { michael@0: logger.warn(this.file.leafName + " cannot be installed from multi-package " + michael@0: "XPI", e); michael@0: } michael@0: } michael@0: michael@0: if (!addon) { michael@0: // No valid add-on was found michael@0: aCallback(); michael@0: return; michael@0: } michael@0: michael@0: this.addon = addon; michael@0: michael@0: this.updateAddonURIs(); michael@0: michael@0: this.addon._install = this; michael@0: this.name = this.addon.selectedLocale.name; michael@0: this.type = this.addon.type; michael@0: this.version = this.addon.version; michael@0: michael@0: // Setting the iconURL to something inside the XPI locks the XPI and michael@0: // makes it impossible to delete on Windows. michael@0: //let newIcon = createWrapper(this.addon).iconURL; michael@0: //if (newIcon) michael@0: // this.iconURL = newIcon; michael@0: michael@0: // Create new AddonInstall instances for every remaining file michael@0: if (files.length > 0) { michael@0: this.linkedInstalls = []; michael@0: let count = 0; michael@0: let self = this; michael@0: files.forEach(function(file) { michael@0: AddonInstall.createInstall(function loadMultipackageManifests_createInstall(aInstall) { michael@0: // Ignore bad add-ons (createInstall will have logged the error) michael@0: if (aInstall.state == AddonManager.STATE_DOWNLOAD_FAILED) { michael@0: // Manually remove the temporary file michael@0: file.remove(true); michael@0: } michael@0: else { michael@0: // Make the new install own its temporary file michael@0: aInstall.ownsTempFile = true; michael@0: michael@0: self.linkedInstalls.push(aInstall) michael@0: michael@0: aInstall.sourceURI = self.sourceURI; michael@0: aInstall.releaseNotesURI = self.releaseNotesURI; michael@0: aInstall.updateAddonURIs(); michael@0: } michael@0: michael@0: count++; michael@0: if (count == files.length) michael@0: aCallback(); michael@0: }, file); michael@0: }, this); michael@0: } michael@0: else { michael@0: aCallback(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Called after the add-on is a local file and the signature and install michael@0: * manifest can be read. michael@0: * michael@0: * @param aCallback michael@0: * A function to call when the manifest has been loaded michael@0: * @throws if the add-on does not contain a valid install manifest or the michael@0: * XPI is incorrectly signed michael@0: */ michael@0: loadManifest: function AI_loadManifest(aCallback) { michael@0: aCallback = makeSafe(aCallback); michael@0: let self = this; michael@0: function addRepositoryData(aAddon) { michael@0: // Try to load from the existing cache first michael@0: AddonRepository.getCachedAddonByID(aAddon.id, function loadManifest_getCachedAddonByID(aRepoAddon) { michael@0: if (aRepoAddon) { michael@0: aAddon._repositoryAddon = aRepoAddon; michael@0: self.name = self.name || aAddon._repositoryAddon.name; michael@0: aAddon.compatibilityOverrides = aRepoAddon.compatibilityOverrides; michael@0: aAddon.appDisabled = !isUsableAddon(aAddon); michael@0: aCallback(); michael@0: return; michael@0: } michael@0: michael@0: // It wasn't there so try to re-download it michael@0: AddonRepository.cacheAddons([aAddon.id], function loadManifest_cacheAddons() { michael@0: AddonRepository.getCachedAddonByID(aAddon.id, function loadManifest_getCachedAddonByID(aRepoAddon) { michael@0: aAddon._repositoryAddon = aRepoAddon; michael@0: self.name = self.name || aAddon._repositoryAddon.name; michael@0: aAddon.compatibilityOverrides = aRepoAddon ? michael@0: aRepoAddon.compatibilityOverrides : michael@0: null; michael@0: aAddon.appDisabled = !isUsableAddon(aAddon); michael@0: aCallback(); michael@0: }); michael@0: }); michael@0: }); michael@0: } michael@0: michael@0: let zipreader = Cc["@mozilla.org/libjar/zip-reader;1"]. michael@0: createInstance(Ci.nsIZipReader); michael@0: try { michael@0: zipreader.open(this.file); michael@0: } michael@0: catch (e) { michael@0: zipreader.close(); michael@0: throw e; michael@0: } michael@0: michael@0: let principal = zipreader.getCertificatePrincipal(null); michael@0: if (principal && principal.hasCertificate) { michael@0: logger.debug("Verifying XPI signature"); michael@0: if (verifyZipSigning(zipreader, principal)) { michael@0: let x509 = principal.certificate; michael@0: if (x509 instanceof Ci.nsIX509Cert) michael@0: this.certificate = x509; michael@0: if (this.certificate && this.certificate.commonName.length > 0) michael@0: this.certName = this.certificate.commonName; michael@0: else michael@0: this.certName = principal.prettyName; michael@0: } michael@0: else { michael@0: zipreader.close(); michael@0: throw new Error("XPI is incorrectly signed"); michael@0: } michael@0: } michael@0: michael@0: try { michael@0: this.addon = loadManifestFromZipReader(zipreader); michael@0: } michael@0: catch (e) { michael@0: zipreader.close(); michael@0: throw e; michael@0: } michael@0: michael@0: if (this.addon.type == "multipackage") { michael@0: this._loadMultipackageManifests(zipreader, function loadManifest_loadMultipackageManifests() { michael@0: addRepositoryData(self.addon); michael@0: }); michael@0: return; michael@0: } michael@0: michael@0: zipreader.close(); michael@0: michael@0: this.updateAddonURIs(); michael@0: michael@0: this.addon._install = this; michael@0: this.name = this.addon.selectedLocale.name; michael@0: this.type = this.addon.type; michael@0: this.version = this.addon.version; michael@0: michael@0: // Setting the iconURL to something inside the XPI locks the XPI and michael@0: // makes it impossible to delete on Windows. michael@0: //let newIcon = createWrapper(this.addon).iconURL; michael@0: //if (newIcon) michael@0: // this.iconURL = newIcon; michael@0: michael@0: addRepositoryData(this.addon); michael@0: }, michael@0: michael@0: observe: function AI_observe(aSubject, aTopic, aData) { michael@0: // Network is going offline michael@0: this.cancel(); michael@0: }, michael@0: michael@0: /** michael@0: * Starts downloading the add-on's XPI file. michael@0: */ michael@0: startDownload: function AI_startDownload() { michael@0: this.state = AddonManager.STATE_DOWNLOADING; michael@0: if (!AddonManagerPrivate.callInstallListeners("onDownloadStarted", michael@0: this.listeners, this.wrapper)) { michael@0: logger.debug("onDownloadStarted listeners cancelled installation of addon " + this.sourceURI.spec); michael@0: this.state = AddonManager.STATE_CANCELLED; michael@0: XPIProvider.removeActiveInstall(this); michael@0: AddonManagerPrivate.callInstallListeners("onDownloadCancelled", michael@0: this.listeners, this.wrapper) michael@0: return; michael@0: } michael@0: michael@0: // If a listener changed our state then do not proceed with the download michael@0: if (this.state != AddonManager.STATE_DOWNLOADING) michael@0: return; michael@0: michael@0: if (this.channel) { michael@0: // A previous download attempt hasn't finished cleaning up yet, signal michael@0: // that it should restart when complete michael@0: logger.debug("Waiting for previous download to complete"); michael@0: this.restartDownload = true; michael@0: return; michael@0: } michael@0: michael@0: this.openChannel(); michael@0: }, michael@0: michael@0: openChannel: function AI_openChannel() { michael@0: this.restartDownload = false; michael@0: michael@0: try { michael@0: this.file = getTemporaryFile(); michael@0: this.ownsTempFile = true; michael@0: this.stream = Cc["@mozilla.org/network/file-output-stream;1"]. michael@0: createInstance(Ci.nsIFileOutputStream); michael@0: this.stream.init(this.file, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | michael@0: FileUtils.MODE_TRUNCATE, FileUtils.PERMS_FILE, 0); michael@0: } michael@0: catch (e) { michael@0: logger.warn("Failed to start download for addon " + this.sourceURI.spec, e); michael@0: this.state = AddonManager.STATE_DOWNLOAD_FAILED; michael@0: this.error = AddonManager.ERROR_FILE_ACCESS; michael@0: XPIProvider.removeActiveInstall(this); michael@0: AddonManagerPrivate.callInstallListeners("onDownloadFailed", michael@0: this.listeners, this.wrapper); michael@0: return; michael@0: } michael@0: michael@0: let listener = Cc["@mozilla.org/network/stream-listener-tee;1"]. michael@0: createInstance(Ci.nsIStreamListenerTee); michael@0: listener.init(this, this.stream); michael@0: try { michael@0: Components.utils.import("resource://gre/modules/CertUtils.jsm"); michael@0: let requireBuiltIn = Prefs.getBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, true); michael@0: this.badCertHandler = new BadCertHandler(!requireBuiltIn); michael@0: michael@0: this.channel = NetUtil.newChannel(this.sourceURI); michael@0: this.channel.notificationCallbacks = this; michael@0: if (this.channel instanceof Ci.nsIHttpChannelInternal) michael@0: this.channel.forceAllowThirdPartyCookie = true; michael@0: this.channel.asyncOpen(listener, null); michael@0: michael@0: Services.obs.addObserver(this, "network:offline-about-to-go-offline", false); michael@0: } michael@0: catch (e) { michael@0: logger.warn("Failed to start download for addon " + this.sourceURI.spec, e); michael@0: this.state = AddonManager.STATE_DOWNLOAD_FAILED; michael@0: this.error = AddonManager.ERROR_NETWORK_FAILURE; michael@0: XPIProvider.removeActiveInstall(this); michael@0: AddonManagerPrivate.callInstallListeners("onDownloadFailed", michael@0: this.listeners, this.wrapper); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Update the crypto hasher with the new data and call the progress listeners. michael@0: * michael@0: * @see nsIStreamListener michael@0: */ michael@0: onDataAvailable: function AI_onDataAvailable(aRequest, aContext, aInputstream, michael@0: aOffset, aCount) { michael@0: this.crypto.updateFromStream(aInputstream, aCount); michael@0: this.progress += aCount; michael@0: if (!AddonManagerPrivate.callInstallListeners("onDownloadProgress", michael@0: this.listeners, this.wrapper)) { michael@0: // TODO cancel the download and make it available again (bug 553024) michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Check the redirect response for a hash of the target XPI and verify that michael@0: * we don't end up on an insecure channel. michael@0: * michael@0: * @see nsIChannelEventSink michael@0: */ michael@0: asyncOnChannelRedirect: function AI_asyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, aCallback) { michael@0: if (!this.hash && aOldChannel.originalURI.schemeIs("https") && michael@0: aOldChannel instanceof Ci.nsIHttpChannel) { michael@0: try { michael@0: let hashStr = aOldChannel.getResponseHeader("X-Target-Digest"); michael@0: let hashSplit = hashStr.toLowerCase().split(":"); michael@0: this.hash = { michael@0: algorithm: hashSplit[0], michael@0: data: hashSplit[1] michael@0: }; michael@0: } michael@0: catch (e) { michael@0: } michael@0: } michael@0: michael@0: // Verify that we don't end up on an insecure channel if we haven't got a michael@0: // hash to verify with (see bug 537761 for discussion) michael@0: if (!this.hash) michael@0: this.badCertHandler.asyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, aCallback); michael@0: else michael@0: aCallback.onRedirectVerifyCallback(Cr.NS_OK); michael@0: michael@0: this.channel = aNewChannel; michael@0: }, michael@0: michael@0: /** michael@0: * This is the first chance to get at real headers on the channel. michael@0: * michael@0: * @see nsIStreamListener michael@0: */ michael@0: onStartRequest: function AI_onStartRequest(aRequest, aContext) { michael@0: this.crypto = Cc["@mozilla.org/security/hash;1"]. michael@0: createInstance(Ci.nsICryptoHash); michael@0: if (this.hash) { michael@0: try { michael@0: this.crypto.initWithString(this.hash.algorithm); michael@0: } michael@0: catch (e) { michael@0: logger.warn("Unknown hash algorithm '" + this.hash.algorithm + "' for addon " + this.sourceURI.spec, e); michael@0: this.state = AddonManager.STATE_DOWNLOAD_FAILED; michael@0: this.error = AddonManager.ERROR_INCORRECT_HASH; michael@0: XPIProvider.removeActiveInstall(this); michael@0: AddonManagerPrivate.callInstallListeners("onDownloadFailed", michael@0: this.listeners, this.wrapper); michael@0: aRequest.cancel(Cr.NS_BINDING_ABORTED); michael@0: return; michael@0: } michael@0: } michael@0: else { michael@0: // We always need something to consume data from the inputstream passed michael@0: // to onDataAvailable so just create a dummy cryptohasher to do that. michael@0: this.crypto.initWithString("sha1"); michael@0: } michael@0: michael@0: this.progress = 0; michael@0: if (aRequest instanceof Ci.nsIChannel) { michael@0: try { michael@0: this.maxProgress = aRequest.contentLength; michael@0: } michael@0: catch (e) { michael@0: } michael@0: logger.debug("Download started for " + this.sourceURI.spec + " to file " + michael@0: this.file.path); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * The download is complete. michael@0: * michael@0: * @see nsIStreamListener michael@0: */ michael@0: onStopRequest: function AI_onStopRequest(aRequest, aContext, aStatus) { michael@0: this.stream.close(); michael@0: this.channel = null; michael@0: this.badCerthandler = null; michael@0: Services.obs.removeObserver(this, "network:offline-about-to-go-offline"); michael@0: michael@0: // If the download was cancelled then all events will have already been sent michael@0: if (aStatus == Cr.NS_BINDING_ABORTED) { michael@0: this.removeTemporaryFile(); michael@0: if (this.restartDownload) michael@0: this.openChannel(); michael@0: return; michael@0: } michael@0: michael@0: logger.debug("Download of " + this.sourceURI.spec + " completed."); michael@0: michael@0: if (Components.isSuccessCode(aStatus)) { michael@0: if (!(aRequest instanceof Ci.nsIHttpChannel) || aRequest.requestSucceeded) { michael@0: if (!this.hash && (aRequest instanceof Ci.nsIChannel)) { michael@0: try { michael@0: checkCert(aRequest, michael@0: !Prefs.getBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, true)); michael@0: } michael@0: catch (e) { michael@0: this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, e); michael@0: return; michael@0: } michael@0: } michael@0: michael@0: // convert the binary hash data to a hex string. michael@0: let calculatedHash = getHashStringForCrypto(this.crypto); michael@0: this.crypto = null; michael@0: if (this.hash && calculatedHash != this.hash.data) { michael@0: this.downloadFailed(AddonManager.ERROR_INCORRECT_HASH, michael@0: "Downloaded file hash (" + calculatedHash + michael@0: ") did not match provided hash (" + this.hash.data + ")"); michael@0: return; michael@0: } michael@0: try { michael@0: let self = this; michael@0: this.loadManifest(function onStopRequest_loadManifest() { michael@0: if (self.addon.isCompatible) { michael@0: self.downloadCompleted(); michael@0: } michael@0: else { michael@0: // TODO Should we send some event here (bug 557716)? michael@0: self.state = AddonManager.STATE_CHECKING; michael@0: new UpdateChecker(self.addon, { michael@0: onUpdateFinished: function onStopRequest_onUpdateFinished(aAddon) { michael@0: self.downloadCompleted(); michael@0: } michael@0: }, AddonManager.UPDATE_WHEN_ADDON_INSTALLED); michael@0: } michael@0: }); michael@0: } michael@0: catch (e) { michael@0: this.downloadFailed(AddonManager.ERROR_CORRUPT_FILE, e); michael@0: } michael@0: } michael@0: else { michael@0: if (aRequest instanceof Ci.nsIHttpChannel) michael@0: this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, michael@0: aRequest.responseStatus + " " + michael@0: aRequest.responseStatusText); michael@0: else michael@0: this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, aStatus); michael@0: } michael@0: } michael@0: else { michael@0: this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, aStatus); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Notify listeners that the download failed. michael@0: * michael@0: * @param aReason michael@0: * Something to log about the failure michael@0: * @param error michael@0: * The error code to pass to the listeners michael@0: */ michael@0: downloadFailed: function AI_downloadFailed(aReason, aError) { michael@0: logger.warn("Download of " + this.sourceURI.spec + " failed", aError); michael@0: this.state = AddonManager.STATE_DOWNLOAD_FAILED; michael@0: this.error = aReason; michael@0: XPIProvider.removeActiveInstall(this); michael@0: AddonManagerPrivate.callInstallListeners("onDownloadFailed", this.listeners, michael@0: this.wrapper); michael@0: michael@0: // If the listener hasn't restarted the download then remove any temporary michael@0: // file michael@0: if (this.state == AddonManager.STATE_DOWNLOAD_FAILED) { michael@0: logger.debug("downloadFailed: removing temp file for " + this.sourceURI.spec); michael@0: this.removeTemporaryFile(); michael@0: } michael@0: else michael@0: logger.debug("downloadFailed: listener changed AddonInstall state for " + michael@0: this.sourceURI.spec + " to " + this.state); michael@0: }, michael@0: michael@0: /** michael@0: * Notify listeners that the download completed. michael@0: */ michael@0: downloadCompleted: function AI_downloadCompleted() { michael@0: let self = this; michael@0: XPIDatabase.getVisibleAddonForID(this.addon.id, function downloadCompleted_getVisibleAddonForID(aAddon) { michael@0: if (aAddon) michael@0: self.existingAddon = aAddon; michael@0: michael@0: self.state = AddonManager.STATE_DOWNLOADED; michael@0: self.addon.updateDate = Date.now(); michael@0: michael@0: if (self.existingAddon) { michael@0: self.addon.existingAddonID = self.existingAddon.id; michael@0: self.addon.installDate = self.existingAddon.installDate; michael@0: applyBlocklistChanges(self.existingAddon, self.addon); michael@0: } michael@0: else { michael@0: self.addon.installDate = self.addon.updateDate; michael@0: } michael@0: michael@0: if (AddonManagerPrivate.callInstallListeners("onDownloadEnded", michael@0: self.listeners, michael@0: self.wrapper)) { michael@0: // If a listener changed our state then do not proceed with the install michael@0: if (self.state != AddonManager.STATE_DOWNLOADED) michael@0: return; michael@0: michael@0: self.install(); michael@0: michael@0: if (self.linkedInstalls) { michael@0: self.linkedInstalls.forEach(function(aInstall) { michael@0: aInstall.install(); michael@0: }); michael@0: } michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: // TODO This relies on the assumption that we are always installing into the michael@0: // highest priority install location so the resulting add-on will be visible michael@0: // overriding any existing copy in another install location (bug 557710). michael@0: /** michael@0: * Installs the add-on into the install location. michael@0: */ michael@0: startInstall: function AI_startInstall() { michael@0: this.state = AddonManager.STATE_INSTALLING; michael@0: if (!AddonManagerPrivate.callInstallListeners("onInstallStarted", michael@0: this.listeners, this.wrapper)) { michael@0: this.state = AddonManager.STATE_DOWNLOADED; michael@0: XPIProvider.removeActiveInstall(this); michael@0: AddonManagerPrivate.callInstallListeners("onInstallCancelled", michael@0: this.listeners, this.wrapper) michael@0: return; michael@0: } michael@0: michael@0: // Find and cancel any pending installs for the same add-on in the same michael@0: // install location michael@0: for (let aInstall of XPIProvider.installs) { michael@0: if (aInstall.state == AddonManager.STATE_INSTALLED && michael@0: aInstall.installLocation == this.installLocation && michael@0: aInstall.addon.id == this.addon.id) { michael@0: logger.debug("Cancelling previous pending install of " + aInstall.addon.id); michael@0: aInstall.cancel(); michael@0: } michael@0: } michael@0: michael@0: let isUpgrade = this.existingAddon && michael@0: this.existingAddon._installLocation == this.installLocation; michael@0: let requiresRestart = XPIProvider.installRequiresRestart(this.addon); michael@0: michael@0: logger.debug("Starting install of " + this.addon.id + " from " + this.sourceURI.spec); michael@0: AddonManagerPrivate.callAddonListeners("onInstalling", michael@0: createWrapper(this.addon), michael@0: requiresRestart); michael@0: michael@0: let stagingDir = this.installLocation.getStagingDir(); michael@0: let stagedAddon = stagingDir.clone(); michael@0: michael@0: Task.spawn((function() { michael@0: let installedUnpacked = 0; michael@0: yield this.installLocation.requestStagingDir(); michael@0: michael@0: // First stage the file regardless of whether restarting is necessary michael@0: if (this.addon.unpack || Prefs.getBoolPref(PREF_XPI_UNPACK, false)) { michael@0: logger.debug("Addon " + this.addon.id + " will be installed as " + michael@0: "an unpacked directory"); michael@0: stagedAddon.append(this.addon.id); michael@0: yield removeAsync(stagedAddon); michael@0: yield OS.File.makeDir(stagedAddon.path); michael@0: yield ZipUtils.extractFilesAsync(this.file, stagedAddon); michael@0: installedUnpacked = 1; michael@0: } michael@0: else { michael@0: logger.debug("Addon " + this.addon.id + " will be installed as " + michael@0: "a packed xpi"); michael@0: stagedAddon.append(this.addon.id + ".xpi"); michael@0: yield removeAsync(stagedAddon); michael@0: yield OS.File.copy(this.file.path, stagedAddon.path); michael@0: } michael@0: michael@0: if (requiresRestart) { michael@0: // Point the add-on to its extracted files as the xpi may get deleted michael@0: this.addon._sourceBundle = stagedAddon; michael@0: michael@0: // Cache the AddonInternal as it may have updated compatibility info michael@0: let stagedJSON = stagedAddon.clone(); michael@0: stagedJSON.leafName = this.addon.id + ".json"; michael@0: if (stagedJSON.exists()) michael@0: stagedJSON.remove(true); michael@0: let stream = Cc["@mozilla.org/network/file-output-stream;1"]. michael@0: createInstance(Ci.nsIFileOutputStream); michael@0: let converter = Cc["@mozilla.org/intl/converter-output-stream;1"]. michael@0: createInstance(Ci.nsIConverterOutputStream); michael@0: michael@0: try { michael@0: stream.init(stagedJSON, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | michael@0: FileUtils.MODE_TRUNCATE, FileUtils.PERMS_FILE, michael@0: 0); michael@0: converter.init(stream, "UTF-8", 0, 0x0000); michael@0: converter.writeString(JSON.stringify(this.addon)); michael@0: } michael@0: finally { michael@0: converter.close(); michael@0: stream.close(); michael@0: } michael@0: michael@0: logger.debug("Staged install of " + this.addon.id + " from " + this.sourceURI.spec + " ready; waiting for restart."); michael@0: this.state = AddonManager.STATE_INSTALLED; michael@0: if (isUpgrade) { michael@0: delete this.existingAddon.pendingUpgrade; michael@0: this.existingAddon.pendingUpgrade = this.addon; michael@0: } michael@0: AddonManagerPrivate.callInstallListeners("onInstallEnded", michael@0: this.listeners, this.wrapper, michael@0: createWrapper(this.addon)); michael@0: } michael@0: else { michael@0: // The install is completed so it should be removed from the active list michael@0: XPIProvider.removeActiveInstall(this); michael@0: michael@0: // TODO We can probably reduce the number of DB operations going on here michael@0: // We probably also want to support rolling back failed upgrades etc. michael@0: // See bug 553015. michael@0: michael@0: // Deactivate and remove the old add-on as necessary michael@0: let reason = BOOTSTRAP_REASONS.ADDON_INSTALL; michael@0: if (this.existingAddon) { michael@0: if (Services.vc.compare(this.existingAddon.version, this.addon.version) < 0) michael@0: reason = BOOTSTRAP_REASONS.ADDON_UPGRADE; michael@0: else michael@0: reason = BOOTSTRAP_REASONS.ADDON_DOWNGRADE; michael@0: michael@0: if (this.existingAddon.bootstrap) { michael@0: let file = this.existingAddon._installLocation michael@0: .getLocationForID(this.existingAddon.id); michael@0: if (this.existingAddon.active) { michael@0: XPIProvider.callBootstrapMethod(this.existingAddon.id, michael@0: this.existingAddon.version, michael@0: this.existingAddon.type, file, michael@0: "shutdown", reason, michael@0: { newVersion: this.addon.version }); michael@0: } michael@0: michael@0: XPIProvider.callBootstrapMethod(this.existingAddon.id, michael@0: this.existingAddon.version, michael@0: this.existingAddon.type, file, michael@0: "uninstall", reason, michael@0: { newVersion: this.addon.version }); michael@0: XPIProvider.unloadBootstrapScope(this.existingAddon.id); michael@0: flushStartupCache(); michael@0: } michael@0: michael@0: if (!isUpgrade && this.existingAddon.active) { michael@0: XPIDatabase.updateAddonActive(this.existingAddon, false); michael@0: } michael@0: } michael@0: michael@0: // Install the new add-on into its final location michael@0: let existingAddonID = this.existingAddon ? this.existingAddon.id : null; michael@0: let file = this.installLocation.installAddon(this.addon.id, stagedAddon, michael@0: existingAddonID); michael@0: michael@0: // Update the metadata in the database michael@0: this.addon._sourceBundle = file; michael@0: this.addon._installLocation = this.installLocation; michael@0: let scanStarted = Date.now(); michael@0: let [, mTime, scanItems] = recursiveLastModifiedTime(file); michael@0: let scanTime = Date.now() - scanStarted; michael@0: this.addon.updateDate = mTime; michael@0: this.addon.visible = true; michael@0: if (isUpgrade) { michael@0: this.addon = XPIDatabase.updateAddonMetadata(this.existingAddon, this.addon, michael@0: file.persistentDescriptor); michael@0: } michael@0: else { michael@0: this.addon.installDate = this.addon.updateDate; michael@0: this.addon.active = (this.addon.visible && !isAddonDisabled(this.addon)) michael@0: this.addon = XPIDatabase.addAddonMetadata(this.addon, file.persistentDescriptor); michael@0: } michael@0: michael@0: let extraParams = {}; michael@0: if (this.existingAddon) { michael@0: extraParams.oldVersion = this.existingAddon.version; michael@0: } michael@0: michael@0: if (this.addon.bootstrap) { michael@0: XPIProvider.callBootstrapMethod(this.addon.id, this.addon.version, michael@0: this.addon.type, file, "install", michael@0: reason, extraParams); michael@0: } michael@0: michael@0: AddonManagerPrivate.callAddonListeners("onInstalled", michael@0: createWrapper(this.addon)); michael@0: michael@0: logger.debug("Install of " + this.sourceURI.spec + " completed."); michael@0: this.state = AddonManager.STATE_INSTALLED; michael@0: AddonManagerPrivate.callInstallListeners("onInstallEnded", michael@0: this.listeners, this.wrapper, michael@0: createWrapper(this.addon)); michael@0: michael@0: if (this.addon.bootstrap) { michael@0: if (this.addon.active) { michael@0: XPIProvider.callBootstrapMethod(this.addon.id, this.addon.version, michael@0: this.addon.type, file, "startup", michael@0: reason, extraParams); michael@0: } michael@0: else { michael@0: // XXX this makes it dangerous to do some things in onInstallEnded michael@0: // listeners because important cleanup hasn't been done yet michael@0: XPIProvider.unloadBootstrapScope(this.addon.id); michael@0: } michael@0: } michael@0: XPIProvider.setTelemetry(this.addon.id, "unpacked", installedUnpacked); michael@0: XPIProvider.setTelemetry(this.addon.id, "location", this.installLocation.name); michael@0: XPIProvider.setTelemetry(this.addon.id, "scan_MS", scanTime); michael@0: XPIProvider.setTelemetry(this.addon.id, "scan_items", scanItems); michael@0: let loc = this.addon.defaultLocale; michael@0: if (loc) { michael@0: XPIProvider.setTelemetry(this.addon.id, "name", loc.name); michael@0: XPIProvider.setTelemetry(this.addon.id, "creator", loc.creator); michael@0: } michael@0: } michael@0: }).bind(this)).then(null, (e) => { michael@0: logger.warn("Failed to install " + this.file.path + " from " + this.sourceURI.spec, e); michael@0: if (stagedAddon.exists()) michael@0: recursiveRemove(stagedAddon); michael@0: this.state = AddonManager.STATE_INSTALL_FAILED; michael@0: this.error = AddonManager.ERROR_FILE_ACCESS; michael@0: XPIProvider.removeActiveInstall(this); michael@0: AddonManagerPrivate.callAddonListeners("onOperationCancelled", michael@0: createWrapper(this.addon)); michael@0: AddonManagerPrivate.callInstallListeners("onInstallFailed", michael@0: this.listeners, michael@0: this.wrapper); michael@0: }).then(() => { michael@0: this.removeTemporaryFile(); michael@0: return this.installLocation.releaseStagingDir(); michael@0: }); michael@0: }, michael@0: michael@0: getInterface: function AI_getInterface(iid) { michael@0: if (iid.equals(Ci.nsIAuthPrompt2)) { michael@0: var factory = Cc["@mozilla.org/prompter;1"]. michael@0: getService(Ci.nsIPromptFactory); michael@0: return factory.getPrompt(this.window, Ci.nsIAuthPrompt); michael@0: } michael@0: else if (iid.equals(Ci.nsIChannelEventSink)) { michael@0: return this; michael@0: } michael@0: michael@0: return this.badCertHandler.getInterface(iid); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Creates a new AddonInstall for an already staged install. Used when michael@0: * installing the staged install failed for some reason. michael@0: * michael@0: * @param aDir michael@0: * The directory holding the staged install michael@0: * @param aManifest michael@0: * The cached manifest for the install michael@0: */ michael@0: AddonInstall.createStagedInstall = function AI_createStagedInstall(aInstallLocation, aDir, aManifest) { michael@0: let url = Services.io.newFileURI(aDir); michael@0: michael@0: let install = new AddonInstall(aInstallLocation, aDir); michael@0: install.initStagedInstall(aManifest); michael@0: }; michael@0: michael@0: /** michael@0: * Creates a new AddonInstall to install an add-on from a local file. Installs michael@0: * always go into the profile install location. michael@0: * michael@0: * @param aCallback michael@0: * The callback to pass the new AddonInstall to michael@0: * @param aFile michael@0: * The file to install michael@0: */ michael@0: AddonInstall.createInstall = function AI_createInstall(aCallback, aFile) { michael@0: let location = XPIProvider.installLocationsByName[KEY_APP_PROFILE]; michael@0: let url = Services.io.newFileURI(aFile); michael@0: michael@0: try { michael@0: let install = new AddonInstall(location, url); michael@0: install.initLocalInstall(aCallback); michael@0: } michael@0: catch(e) { michael@0: logger.error("Error creating install", e); michael@0: makeSafe(aCallback)(null); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Creates a new AddonInstall to download and install a URL. michael@0: * michael@0: * @param aCallback michael@0: * The callback to pass the new AddonInstall to michael@0: * @param aUri michael@0: * The URI to download michael@0: * @param aHash michael@0: * A hash for the add-on michael@0: * @param aName michael@0: * A name for the add-on michael@0: * @param aIcons michael@0: * An icon URLs for the add-on michael@0: * @param aVersion michael@0: * A version for the add-on michael@0: * @param aLoadGroup michael@0: * An nsILoadGroup to associate the download with michael@0: */ michael@0: AddonInstall.createDownload = function AI_createDownload(aCallback, aUri, aHash, aName, aIcons, michael@0: aVersion, aLoadGroup) { michael@0: let location = XPIProvider.installLocationsByName[KEY_APP_PROFILE]; michael@0: let url = NetUtil.newURI(aUri); michael@0: michael@0: let install = new AddonInstall(location, url, aHash, null, null, aLoadGroup); michael@0: if (url instanceof Ci.nsIFileURL) michael@0: install.initLocalInstall(aCallback); michael@0: else michael@0: install.initAvailableDownload(aName, null, aIcons, aVersion, aCallback); michael@0: }; michael@0: michael@0: /** michael@0: * Creates a new AddonInstall for an update. michael@0: * michael@0: * @param aCallback michael@0: * The callback to pass the new AddonInstall to michael@0: * @param aAddon michael@0: * The add-on being updated michael@0: * @param aUpdate michael@0: * The metadata about the new version from the update manifest michael@0: */ michael@0: AddonInstall.createUpdate = function AI_createUpdate(aCallback, aAddon, aUpdate) { michael@0: let url = NetUtil.newURI(aUpdate.updateURL); michael@0: let releaseNotesURI = null; michael@0: try { michael@0: if (aUpdate.updateInfoURL) michael@0: releaseNotesURI = NetUtil.newURI(escapeAddonURI(aAddon, aUpdate.updateInfoURL)); michael@0: } michael@0: catch (e) { michael@0: // If the releaseNotesURI cannot be parsed then just ignore it. michael@0: } michael@0: michael@0: let install = new AddonInstall(aAddon._installLocation, url, michael@0: aUpdate.updateHash, releaseNotesURI, aAddon); michael@0: if (url instanceof Ci.nsIFileURL) { michael@0: install.initLocalInstall(aCallback); michael@0: } michael@0: else { michael@0: install.initAvailableDownload(aAddon.selectedLocale.name, aAddon.type, michael@0: aAddon.icons, aUpdate.version, aCallback); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Creates a wrapper for an AddonInstall that only exposes the public API michael@0: * michael@0: * @param install michael@0: * The AddonInstall to create a wrapper for michael@0: */ michael@0: function AddonInstallWrapper(aInstall) { michael@0: #ifdef MOZ_EM_DEBUG michael@0: this.__defineGetter__("__AddonInstallInternal__", function AIW_debugGetter() { michael@0: return aInstall; michael@0: }); michael@0: #endif michael@0: michael@0: ["name", "type", "version", "icons", "releaseNotesURI", "file", "state", "error", michael@0: "progress", "maxProgress", "certificate", "certName"].forEach(function(aProp) { michael@0: this.__defineGetter__(aProp, function AIW_propertyGetter() aInstall[aProp]); michael@0: }, this); michael@0: michael@0: this.__defineGetter__("iconURL", function AIW_iconURL() aInstall.icons[32]); michael@0: michael@0: this.__defineGetter__("existingAddon", function AIW_existingAddonGetter() { michael@0: return createWrapper(aInstall.existingAddon); michael@0: }); michael@0: this.__defineGetter__("addon", function AIW_addonGetter() createWrapper(aInstall.addon)); michael@0: this.__defineGetter__("sourceURI", function AIW_sourceURIGetter() aInstall.sourceURI); michael@0: michael@0: this.__defineGetter__("linkedInstalls", function AIW_linkedInstallsGetter() { michael@0: if (!aInstall.linkedInstalls) michael@0: return null; michael@0: return [i.wrapper for each (i in aInstall.linkedInstalls)]; michael@0: }); michael@0: michael@0: this.install = function AIW_install() { michael@0: aInstall.install(); michael@0: } michael@0: michael@0: this.cancel = function AIW_cancel() { michael@0: aInstall.cancel(); michael@0: } michael@0: michael@0: this.addListener = function AIW_addListener(listener) { michael@0: aInstall.addListener(listener); michael@0: } michael@0: michael@0: this.removeListener = function AIW_removeListener(listener) { michael@0: aInstall.removeListener(listener); michael@0: } michael@0: } michael@0: michael@0: AddonInstallWrapper.prototype = {}; michael@0: michael@0: /** michael@0: * Creates a new update checker. michael@0: * michael@0: * @param aAddon michael@0: * The add-on to check for updates michael@0: * @param aListener michael@0: * An UpdateListener to notify of updates michael@0: * @param aReason michael@0: * The reason for the update check michael@0: * @param aAppVersion michael@0: * An optional application version to check for updates for michael@0: * @param aPlatformVersion michael@0: * An optional platform version to check for updates for michael@0: * @throws if the aListener or aReason arguments are not valid michael@0: */ michael@0: function UpdateChecker(aAddon, aListener, aReason, aAppVersion, aPlatformVersion) { michael@0: if (!aListener || !aReason) michael@0: throw Cr.NS_ERROR_INVALID_ARG; michael@0: michael@0: Components.utils.import("resource://gre/modules/addons/AddonUpdateChecker.jsm"); michael@0: michael@0: this.addon = aAddon; michael@0: aAddon._updateCheck = this; michael@0: XPIProvider.doing(this); michael@0: this.listener = aListener; michael@0: this.appVersion = aAppVersion; michael@0: this.platformVersion = aPlatformVersion; michael@0: this.syncCompatibility = (aReason == AddonManager.UPDATE_WHEN_NEW_APP_INSTALLED); michael@0: michael@0: let updateURL = aAddon.updateURL; michael@0: if (!updateURL) { michael@0: if (aReason == AddonManager.UPDATE_WHEN_PERIODIC_UPDATE && michael@0: Services.prefs.getPrefType(PREF_EM_UPDATE_BACKGROUND_URL) == Services.prefs.PREF_STRING) { michael@0: updateURL = Services.prefs.getCharPref(PREF_EM_UPDATE_BACKGROUND_URL); michael@0: } else { michael@0: updateURL = Services.prefs.getCharPref(PREF_EM_UPDATE_URL); michael@0: } michael@0: } michael@0: michael@0: const UPDATE_TYPE_COMPATIBILITY = 32; michael@0: const UPDATE_TYPE_NEWVERSION = 64; michael@0: michael@0: aReason |= UPDATE_TYPE_COMPATIBILITY; michael@0: if ("onUpdateAvailable" in this.listener) michael@0: aReason |= UPDATE_TYPE_NEWVERSION; michael@0: michael@0: let url = escapeAddonURI(aAddon, updateURL, aReason, aAppVersion); michael@0: this._parser = AddonUpdateChecker.checkForUpdates(aAddon.id, aAddon.updateKey, michael@0: url, this); michael@0: } michael@0: michael@0: UpdateChecker.prototype = { michael@0: addon: null, michael@0: listener: null, michael@0: appVersion: null, michael@0: platformVersion: null, michael@0: syncCompatibility: null, michael@0: michael@0: /** michael@0: * Calls a method on the listener passing any number of arguments and michael@0: * consuming any exceptions. michael@0: * michael@0: * @param aMethod michael@0: * The method to call on the listener michael@0: */ michael@0: callListener: function UC_callListener(aMethod, ...aArgs) { michael@0: if (!(aMethod in this.listener)) michael@0: return; michael@0: michael@0: try { michael@0: this.listener[aMethod].apply(this.listener, aArgs); michael@0: } michael@0: catch (e) { michael@0: logger.warn("Exception calling UpdateListener method " + aMethod, e); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Called when AddonUpdateChecker completes the update check michael@0: * michael@0: * @param updates michael@0: * The list of update details for the add-on michael@0: */ michael@0: onUpdateCheckComplete: function UC_onUpdateCheckComplete(aUpdates) { michael@0: XPIProvider.done(this.addon._updateCheck); michael@0: this.addon._updateCheck = null; michael@0: let AUC = AddonUpdateChecker; michael@0: michael@0: let ignoreMaxVersion = false; michael@0: let ignoreStrictCompat = false; michael@0: if (!AddonManager.checkCompatibility) { michael@0: ignoreMaxVersion = true; michael@0: ignoreStrictCompat = true; michael@0: } else if (this.addon.type in COMPATIBLE_BY_DEFAULT_TYPES && michael@0: !AddonManager.strictCompatibility && michael@0: !this.addon.strictCompatibility && michael@0: !this.addon.hasBinaryComponents) { michael@0: ignoreMaxVersion = true; michael@0: } michael@0: michael@0: // Always apply any compatibility update for the current version michael@0: let compatUpdate = AUC.getCompatibilityUpdate(aUpdates, this.addon.version, michael@0: this.syncCompatibility, michael@0: null, null, michael@0: ignoreMaxVersion, michael@0: ignoreStrictCompat); michael@0: // Apply the compatibility update to the database michael@0: if (compatUpdate) michael@0: this.addon.applyCompatibilityUpdate(compatUpdate, this.syncCompatibility); michael@0: michael@0: // If the request is for an application or platform version that is michael@0: // different to the current application or platform version then look for a michael@0: // compatibility update for those versions. michael@0: if ((this.appVersion && michael@0: Services.vc.compare(this.appVersion, Services.appinfo.version) != 0) || michael@0: (this.platformVersion && michael@0: Services.vc.compare(this.platformVersion, Services.appinfo.platformVersion) != 0)) { michael@0: compatUpdate = AUC.getCompatibilityUpdate(aUpdates, this.addon.version, michael@0: false, this.appVersion, michael@0: this.platformVersion, michael@0: ignoreMaxVersion, michael@0: ignoreStrictCompat); michael@0: } michael@0: michael@0: if (compatUpdate) michael@0: this.callListener("onCompatibilityUpdateAvailable", createWrapper(this.addon)); michael@0: else michael@0: this.callListener("onNoCompatibilityUpdateAvailable", createWrapper(this.addon)); michael@0: michael@0: function sendUpdateAvailableMessages(aSelf, aInstall) { michael@0: if (aInstall) { michael@0: aSelf.callListener("onUpdateAvailable", createWrapper(aSelf.addon), michael@0: aInstall.wrapper); michael@0: } michael@0: else { michael@0: aSelf.callListener("onNoUpdateAvailable", createWrapper(aSelf.addon)); michael@0: } michael@0: aSelf.callListener("onUpdateFinished", createWrapper(aSelf.addon), michael@0: AddonManager.UPDATE_STATUS_NO_ERROR); michael@0: } michael@0: michael@0: let compatOverrides = AddonManager.strictCompatibility ? michael@0: null : michael@0: this.addon.compatibilityOverrides; michael@0: michael@0: let update = AUC.getNewestCompatibleUpdate(aUpdates, michael@0: this.appVersion, michael@0: this.platformVersion, michael@0: ignoreMaxVersion, michael@0: ignoreStrictCompat, michael@0: compatOverrides); michael@0: michael@0: if (update && Services.vc.compare(this.addon.version, update.version) < 0) { michael@0: for (let currentInstall of XPIProvider.installs) { michael@0: // Skip installs that don't match the available update michael@0: if (currentInstall.existingAddon != this.addon || michael@0: currentInstall.version != update.version) michael@0: continue; michael@0: michael@0: // If the existing install has not yet started downloading then send an michael@0: // available update notification. If it is already downloading then michael@0: // don't send any available update notification michael@0: if (currentInstall.state == AddonManager.STATE_AVAILABLE) { michael@0: logger.debug("Found an existing AddonInstall for " + this.addon.id); michael@0: sendUpdateAvailableMessages(this, currentInstall); michael@0: } michael@0: else michael@0: sendUpdateAvailableMessages(this, null); michael@0: return; michael@0: } michael@0: michael@0: let self = this; michael@0: AddonInstall.createUpdate(function onUpdateCheckComplete_createUpdate(aInstall) { michael@0: sendUpdateAvailableMessages(self, aInstall); michael@0: }, this.addon, update); michael@0: } michael@0: else { michael@0: sendUpdateAvailableMessages(this, null); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Called when AddonUpdateChecker fails the update check michael@0: * michael@0: * @param aError michael@0: * An error status michael@0: */ michael@0: onUpdateCheckError: function UC_onUpdateCheckError(aError) { michael@0: XPIProvider.done(this.addon._updateCheck); michael@0: this.addon._updateCheck = null; michael@0: this.callListener("onNoCompatibilityUpdateAvailable", createWrapper(this.addon)); michael@0: this.callListener("onNoUpdateAvailable", createWrapper(this.addon)); michael@0: this.callListener("onUpdateFinished", createWrapper(this.addon), aError); michael@0: }, michael@0: michael@0: /** michael@0: * Called to cancel an in-progress update check michael@0: */ michael@0: cancel: function UC_cancel() { michael@0: let parser = this._parser; michael@0: if (parser) { michael@0: this._parser = null; michael@0: // This will call back to onUpdateCheckError with a CANCELLED error michael@0: parser.cancel(); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * The AddonInternal is an internal only representation of add-ons. It may michael@0: * have come from the database (see DBAddonInternal in XPIProviderUtils.jsm) michael@0: * or an install manifest. michael@0: */ michael@0: function AddonInternal() { michael@0: } michael@0: michael@0: AddonInternal.prototype = { michael@0: _selectedLocale: null, michael@0: active: false, michael@0: visible: false, michael@0: userDisabled: false, michael@0: appDisabled: false, michael@0: softDisabled: false, michael@0: sourceURI: null, michael@0: releaseNotesURI: null, michael@0: foreignInstall: false, michael@0: michael@0: get selectedLocale() { michael@0: if (this._selectedLocale) michael@0: return this._selectedLocale; michael@0: let locale = findClosestLocale(this.locales); michael@0: this._selectedLocale = locale ? locale : this.defaultLocale; michael@0: return this._selectedLocale; michael@0: }, michael@0: michael@0: get providesUpdatesSecurely() { michael@0: return !!(this.updateKey || !this.updateURL || michael@0: this.updateURL.substring(0, 6) == "https:"); michael@0: }, michael@0: michael@0: get isCompatible() { michael@0: return this.isCompatibleWith(); michael@0: }, michael@0: michael@0: get isPlatformCompatible() { michael@0: if (this.targetPlatforms.length == 0) michael@0: return true; michael@0: michael@0: let matchedOS = false; michael@0: michael@0: // If any targetPlatform matches the OS and contains an ABI then we will michael@0: // only match a targetPlatform that contains both the current OS and ABI michael@0: let needsABI = false; michael@0: michael@0: // Some platforms do not specify an ABI, test against null in that case. michael@0: let abi = null; michael@0: try { michael@0: abi = Services.appinfo.XPCOMABI; michael@0: } michael@0: catch (e) { } michael@0: michael@0: for (let platform of this.targetPlatforms) { michael@0: if (platform.os == Services.appinfo.OS) { michael@0: if (platform.abi) { michael@0: needsABI = true; michael@0: if (platform.abi === abi) michael@0: return true; michael@0: } michael@0: else { michael@0: matchedOS = true; michael@0: } michael@0: } michael@0: } michael@0: michael@0: return matchedOS && !needsABI; michael@0: }, michael@0: michael@0: isCompatibleWith: function AddonInternal_isCompatibleWith(aAppVersion, aPlatformVersion) { michael@0: // Experiments are installed through an external mechanism that michael@0: // limits target audience to compatible clients. We trust it knows what michael@0: // it's doing and skip compatibility checks. michael@0: // michael@0: // This decision does forfeit defense in depth. If the experiments system michael@0: // is ever wrong about targeting an add-on to a specific application michael@0: // or platform, the client will likely see errors. michael@0: if (this.type == "experiment") { michael@0: return true; michael@0: } michael@0: michael@0: let app = this.matchingTargetApplication; michael@0: if (!app) michael@0: return false; michael@0: michael@0: if (!aAppVersion) michael@0: aAppVersion = Services.appinfo.version; michael@0: if (!aPlatformVersion) michael@0: aPlatformVersion = Services.appinfo.platformVersion; michael@0: michael@0: let version; michael@0: if (app.id == Services.appinfo.ID) michael@0: version = aAppVersion; michael@0: else if (app.id == TOOLKIT_ID) michael@0: version = aPlatformVersion michael@0: michael@0: // Only extensions and dictionaries can be compatible by default; themes michael@0: // and language packs always use strict compatibility checking. michael@0: if (this.type in COMPATIBLE_BY_DEFAULT_TYPES && michael@0: !AddonManager.strictCompatibility && !this.strictCompatibility && michael@0: !this.hasBinaryComponents) { michael@0: michael@0: // The repository can specify compatibility overrides. michael@0: // Note: For now, only blacklisting is supported by overrides. michael@0: if (this._repositoryAddon && michael@0: this._repositoryAddon.compatibilityOverrides) { michael@0: let overrides = this._repositoryAddon.compatibilityOverrides; michael@0: let override = AddonRepository.findMatchingCompatOverride(this.version, michael@0: overrides); michael@0: if (override && override.type == "incompatible") michael@0: return false; michael@0: } michael@0: michael@0: // Extremely old extensions should not be compatible by default. michael@0: let minCompatVersion; michael@0: if (app.id == Services.appinfo.ID) michael@0: minCompatVersion = XPIProvider.minCompatibleAppVersion; michael@0: else if (app.id == TOOLKIT_ID) michael@0: minCompatVersion = XPIProvider.minCompatiblePlatformVersion; michael@0: michael@0: if (minCompatVersion && michael@0: Services.vc.compare(minCompatVersion, app.maxVersion) > 0) michael@0: return false; michael@0: michael@0: return Services.vc.compare(version, app.minVersion) >= 0; michael@0: } michael@0: michael@0: return (Services.vc.compare(version, app.minVersion) >= 0) && michael@0: (Services.vc.compare(version, app.maxVersion) <= 0) michael@0: }, michael@0: michael@0: get matchingTargetApplication() { michael@0: let app = null; michael@0: for (let targetApp of this.targetApplications) { michael@0: if (targetApp.id == Services.appinfo.ID) michael@0: return targetApp; michael@0: if (targetApp.id == TOOLKIT_ID) michael@0: app = targetApp; michael@0: } michael@0: return app; michael@0: }, michael@0: michael@0: get blocklistState() { michael@0: let staticItem = findMatchingStaticBlocklistItem(this); michael@0: if (staticItem) michael@0: return staticItem.level; michael@0: michael@0: let bs = Cc["@mozilla.org/extensions/blocklist;1"]. michael@0: getService(Ci.nsIBlocklistService); michael@0: return bs.getAddonBlocklistState(createWrapper(this)); michael@0: }, michael@0: michael@0: get blocklistURL() { michael@0: let staticItem = findMatchingStaticBlocklistItem(this); michael@0: if (staticItem) { michael@0: let url = Services.urlFormatter.formatURLPref("extensions.blocklist.itemURL"); michael@0: return url.replace(/%blockID%/g, staticItem.blockID); michael@0: } michael@0: michael@0: let bs = Cc["@mozilla.org/extensions/blocklist;1"]. michael@0: getService(Ci.nsIBlocklistService); michael@0: return bs.getAddonBlocklistURL(createWrapper(this)); michael@0: }, michael@0: michael@0: applyCompatibilityUpdate: function AddonInternal_applyCompatibilityUpdate(aUpdate, aSyncCompatibility) { michael@0: this.targetApplications.forEach(function(aTargetApp) { michael@0: aUpdate.targetApplications.forEach(function(aUpdateTarget) { michael@0: if (aTargetApp.id == aUpdateTarget.id && (aSyncCompatibility || michael@0: Services.vc.compare(aTargetApp.maxVersion, aUpdateTarget.maxVersion) < 0)) { michael@0: aTargetApp.minVersion = aUpdateTarget.minVersion; michael@0: aTargetApp.maxVersion = aUpdateTarget.maxVersion; michael@0: } michael@0: }); michael@0: }); michael@0: this.appDisabled = !isUsableAddon(this); michael@0: }, michael@0: michael@0: /** michael@0: * toJSON is called by JSON.stringify in order to create a filtered version michael@0: * of this object to be serialized to a JSON file. A new object is returned michael@0: * with copies of all non-private properties. Functions, getters and setters michael@0: * are not copied. michael@0: * michael@0: * @param aKey michael@0: * The key that this object is being serialized as in the JSON. michael@0: * Unused here since this is always the main object serialized michael@0: * michael@0: * @return an object containing copies of the properties of this object michael@0: * ignoring private properties, functions, getters and setters michael@0: */ michael@0: toJSON: function AddonInternal_toJSON(aKey) { michael@0: let obj = {}; michael@0: for (let prop in this) { michael@0: // Ignore private properties michael@0: if (prop.substring(0, 1) == "_") michael@0: continue; michael@0: michael@0: // Ignore getters michael@0: if (this.__lookupGetter__(prop)) michael@0: continue; michael@0: michael@0: // Ignore setters michael@0: if (this.__lookupSetter__(prop)) michael@0: continue; michael@0: michael@0: // Ignore functions michael@0: if (typeof this[prop] == "function") michael@0: continue; michael@0: michael@0: obj[prop] = this[prop]; michael@0: } michael@0: michael@0: return obj; michael@0: }, michael@0: michael@0: /** michael@0: * When an add-on install is pending its metadata will be cached in a file. michael@0: * This method reads particular properties of that metadata that may be newer michael@0: * than that in the install manifest, like compatibility information. michael@0: * michael@0: * @param aObj michael@0: * A JS object containing the cached metadata michael@0: */ michael@0: importMetadata: function AddonInternal_importMetaData(aObj) { michael@0: PENDING_INSTALL_METADATA.forEach(function(aProp) { michael@0: if (!(aProp in aObj)) michael@0: return; michael@0: michael@0: this[aProp] = aObj[aProp]; michael@0: }, this); michael@0: michael@0: // Compatibility info may have changed so update appDisabled michael@0: this.appDisabled = !isUsableAddon(this); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Creates an AddonWrapper for an AddonInternal. michael@0: * michael@0: * @param addon michael@0: * The AddonInternal to wrap michael@0: * @return an AddonWrapper or null if addon was null michael@0: */ michael@0: function createWrapper(aAddon) { michael@0: if (!aAddon) michael@0: return null; michael@0: if (!aAddon._wrapper) { michael@0: aAddon._hasResourceCache = new Map(); michael@0: aAddon._wrapper = new AddonWrapper(aAddon); michael@0: } michael@0: return aAddon._wrapper; michael@0: } michael@0: michael@0: /** michael@0: * The AddonWrapper wraps an Addon to provide the data visible to consumers of michael@0: * the public API. michael@0: */ michael@0: function AddonWrapper(aAddon) { michael@0: #ifdef MOZ_EM_DEBUG michael@0: this.__defineGetter__("__AddonInternal__", function AW_debugGetter() { michael@0: return aAddon; michael@0: }); michael@0: #endif michael@0: michael@0: function chooseValue(aObj, aProp) { michael@0: let repositoryAddon = aAddon._repositoryAddon; michael@0: let objValue = aObj[aProp]; michael@0: michael@0: if (repositoryAddon && (aProp in repositoryAddon) && michael@0: (objValue === undefined || objValue === null)) { michael@0: return [repositoryAddon[aProp], true]; michael@0: } michael@0: michael@0: return [objValue, false]; michael@0: } michael@0: michael@0: ["id", "syncGUID", "version", "type", "isCompatible", "isPlatformCompatible", michael@0: "providesUpdatesSecurely", "blocklistState", "blocklistURL", "appDisabled", michael@0: "softDisabled", "skinnable", "size", "foreignInstall", "hasBinaryComponents", michael@0: "strictCompatibility", "compatibilityOverrides", "updateURL"].forEach(function(aProp) { michael@0: this.__defineGetter__(aProp, function AddonWrapper_propertyGetter() aAddon[aProp]); michael@0: }, this); michael@0: michael@0: ["fullDescription", "developerComments", "eula", "supportURL", michael@0: "contributionURL", "contributionAmount", "averageRating", "reviewCount", michael@0: "reviewURL", "totalDownloads", "weeklyDownloads", "dailyUsers", michael@0: "repositoryStatus"].forEach(function(aProp) { michael@0: this.__defineGetter__(aProp, function AddonWrapper_repoPropertyGetter() { michael@0: if (aAddon._repositoryAddon) michael@0: return aAddon._repositoryAddon[aProp]; michael@0: michael@0: return null; michael@0: }); michael@0: }, this); michael@0: michael@0: this.__defineGetter__("aboutURL", function AddonWrapper_aboutURLGetter() { michael@0: return this.isActive ? aAddon["aboutURL"] : null; michael@0: }); michael@0: michael@0: ["installDate", "updateDate"].forEach(function(aProp) { michael@0: this.__defineGetter__(aProp, function AddonWrapper_datePropertyGetter() new Date(aAddon[aProp])); michael@0: }, this); michael@0: michael@0: ["sourceURI", "releaseNotesURI"].forEach(function(aProp) { michael@0: this.__defineGetter__(aProp, function AddonWrapper_URIPropertyGetter() { michael@0: let [target, fromRepo] = chooseValue(aAddon, aProp); michael@0: if (!target) michael@0: return null; michael@0: if (fromRepo) michael@0: return target; michael@0: return NetUtil.newURI(target); michael@0: }); michael@0: }, this); michael@0: michael@0: this.__defineGetter__("optionsURL", function AddonWrapper_optionsURLGetter() { michael@0: if (this.isActive && aAddon.optionsURL) michael@0: return aAddon.optionsURL; michael@0: michael@0: if (this.isActive && this.hasResource("options.xul")) michael@0: return this.getResourceURI("options.xul").spec; michael@0: michael@0: return null; michael@0: }, this); michael@0: michael@0: this.__defineGetter__("optionsType", function AddonWrapper_optionsTypeGetter() { michael@0: if (!this.isActive) michael@0: return null; michael@0: michael@0: let hasOptionsXUL = this.hasResource("options.xul"); michael@0: let hasOptionsURL = !!this.optionsURL; michael@0: michael@0: if (aAddon.optionsType) { michael@0: switch (parseInt(aAddon.optionsType, 10)) { michael@0: case AddonManager.OPTIONS_TYPE_DIALOG: michael@0: case AddonManager.OPTIONS_TYPE_TAB: michael@0: return hasOptionsURL ? aAddon.optionsType : null; michael@0: case AddonManager.OPTIONS_TYPE_INLINE: michael@0: case AddonManager.OPTIONS_TYPE_INLINE_INFO: michael@0: return (hasOptionsXUL || hasOptionsURL) ? aAddon.optionsType : null; michael@0: } michael@0: return null; michael@0: } michael@0: michael@0: if (hasOptionsXUL) michael@0: return AddonManager.OPTIONS_TYPE_INLINE; michael@0: michael@0: if (hasOptionsURL) michael@0: return AddonManager.OPTIONS_TYPE_DIALOG; michael@0: michael@0: return null; michael@0: }, this); michael@0: michael@0: this.__defineGetter__("iconURL", function AddonWrapper_iconURLGetter() { michael@0: return this.icons[32]; michael@0: }, this); michael@0: michael@0: this.__defineGetter__("icon64URL", function AddonWrapper_icon64URLGetter() { michael@0: return this.icons[64]; michael@0: }, this); michael@0: michael@0: this.__defineGetter__("icons", function AddonWrapper_iconsGetter() { michael@0: let icons = {}; michael@0: if (aAddon._repositoryAddon) { michael@0: for (let size in aAddon._repositoryAddon.icons) { michael@0: icons[size] = aAddon._repositoryAddon.icons[size]; michael@0: } michael@0: } michael@0: if (this.isActive && aAddon.iconURL) { michael@0: icons[32] = aAddon.iconURL; michael@0: } else if (this.hasResource("icon.png")) { michael@0: icons[32] = this.getResourceURI("icon.png").spec; michael@0: } michael@0: if (this.isActive && aAddon.icon64URL) { michael@0: icons[64] = aAddon.icon64URL; michael@0: } else if (this.hasResource("icon64.png")) { michael@0: icons[64] = this.getResourceURI("icon64.png").spec; michael@0: } michael@0: Object.freeze(icons); michael@0: return icons; michael@0: }, this); michael@0: michael@0: PROP_LOCALE_SINGLE.forEach(function(aProp) { michael@0: this.__defineGetter__(aProp, function AddonWrapper_singleLocaleGetter() { michael@0: // Override XPI creator if repository creator is defined michael@0: if (aProp == "creator" && michael@0: aAddon._repositoryAddon && aAddon._repositoryAddon.creator) { michael@0: return aAddon._repositoryAddon.creator; michael@0: } michael@0: michael@0: let result = null; michael@0: michael@0: if (aAddon.active) { michael@0: try { michael@0: let pref = PREF_EM_EXTENSION_FORMAT + aAddon.id + "." + aProp; michael@0: let value = Services.prefs.getComplexValue(pref, michael@0: Ci.nsIPrefLocalizedString); michael@0: if (value.data) michael@0: result = value.data; michael@0: } michael@0: catch (e) { michael@0: } michael@0: } michael@0: michael@0: if (result == null) michael@0: [result, ] = chooseValue(aAddon.selectedLocale, aProp); michael@0: michael@0: if (aProp == "creator") michael@0: return result ? new AddonManagerPrivate.AddonAuthor(result) : null; michael@0: michael@0: return result; michael@0: }); michael@0: }, this); michael@0: michael@0: PROP_LOCALE_MULTI.forEach(function(aProp) { michael@0: this.__defineGetter__(aProp, function AddonWrapper_multiLocaleGetter() { michael@0: let results = null; michael@0: let usedRepository = false; michael@0: michael@0: if (aAddon.active) { michael@0: let pref = PREF_EM_EXTENSION_FORMAT + aAddon.id + "." + michael@0: aProp.substring(0, aProp.length - 1); michael@0: let list = Services.prefs.getChildList(pref, {}); michael@0: if (list.length > 0) { michael@0: list.sort(); michael@0: results = []; michael@0: list.forEach(function(aPref) { michael@0: let value = Services.prefs.getComplexValue(aPref, michael@0: Ci.nsIPrefLocalizedString); michael@0: if (value.data) michael@0: results.push(value.data); michael@0: }); michael@0: } michael@0: } michael@0: michael@0: if (results == null) michael@0: [results, usedRepository] = chooseValue(aAddon.selectedLocale, aProp); michael@0: michael@0: if (results && !usedRepository) { michael@0: results = results.map(function mapResult(aResult) { michael@0: return new AddonManagerPrivate.AddonAuthor(aResult); michael@0: }); michael@0: } michael@0: michael@0: return results; michael@0: }); michael@0: }, this); michael@0: michael@0: this.__defineGetter__("screenshots", function AddonWrapper_screenshotsGetter() { michael@0: let repositoryAddon = aAddon._repositoryAddon; michael@0: if (repositoryAddon && ("screenshots" in repositoryAddon)) { michael@0: let repositoryScreenshots = repositoryAddon.screenshots; michael@0: if (repositoryScreenshots && repositoryScreenshots.length > 0) michael@0: return repositoryScreenshots; michael@0: } michael@0: michael@0: if (aAddon.type == "theme" && this.hasResource("preview.png")) { michael@0: let url = this.getResourceURI("preview.png").spec; michael@0: return [new AddonManagerPrivate.AddonScreenshot(url)]; michael@0: } michael@0: michael@0: return null; michael@0: }); michael@0: michael@0: this.__defineGetter__("applyBackgroundUpdates", function AddonWrapper_applyBackgroundUpdatesGetter() { michael@0: return aAddon.applyBackgroundUpdates; michael@0: }); michael@0: this.__defineSetter__("applyBackgroundUpdates", function AddonWrapper_applyBackgroundUpdatesSetter(val) { michael@0: if (this.type == "experiment") { michael@0: logger.warn("Setting applyBackgroundUpdates on an experiment is not supported."); michael@0: return; michael@0: } michael@0: michael@0: if (val != AddonManager.AUTOUPDATE_DEFAULT && michael@0: val != AddonManager.AUTOUPDATE_DISABLE && michael@0: val != AddonManager.AUTOUPDATE_ENABLE) { michael@0: val = val ? AddonManager.AUTOUPDATE_DEFAULT : michael@0: AddonManager.AUTOUPDATE_DISABLE; michael@0: } michael@0: michael@0: if (val == aAddon.applyBackgroundUpdates) michael@0: return val; michael@0: michael@0: XPIDatabase.setAddonProperties(aAddon, { michael@0: applyBackgroundUpdates: val michael@0: }); michael@0: AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, ["applyBackgroundUpdates"]); michael@0: michael@0: return val; michael@0: }); michael@0: michael@0: this.__defineSetter__("syncGUID", function AddonWrapper_syncGUIDGetter(val) { michael@0: if (aAddon.syncGUID == val) michael@0: return val; michael@0: michael@0: if (aAddon.inDatabase) michael@0: XPIDatabase.setAddonSyncGUID(aAddon, val); michael@0: michael@0: aAddon.syncGUID = val; michael@0: michael@0: return val; michael@0: }); michael@0: michael@0: this.__defineGetter__("install", function AddonWrapper_installGetter() { michael@0: if (!("_install" in aAddon) || !aAddon._install) michael@0: return null; michael@0: return aAddon._install.wrapper; michael@0: }); michael@0: michael@0: this.__defineGetter__("pendingUpgrade", function AddonWrapper_pendingUpgradeGetter() { michael@0: return createWrapper(aAddon.pendingUpgrade); michael@0: }); michael@0: michael@0: this.__defineGetter__("scope", function AddonWrapper_scopeGetter() { michael@0: if (aAddon._installLocation) michael@0: return aAddon._installLocation.scope; michael@0: michael@0: return AddonManager.SCOPE_PROFILE; michael@0: }); michael@0: michael@0: this.__defineGetter__("pendingOperations", function AddonWrapper_pendingOperationsGetter() { michael@0: let pending = 0; michael@0: if (!(aAddon.inDatabase)) { michael@0: // Add-on is pending install if there is no associated install (shouldn't michael@0: // happen here) or if the install is in the process of or has successfully michael@0: // completed the install. If an add-on is pending install then we ignore michael@0: // any other pending operations. michael@0: if (!aAddon._install || aAddon._install.state == AddonManager.STATE_INSTALLING || michael@0: aAddon._install.state == AddonManager.STATE_INSTALLED) michael@0: return AddonManager.PENDING_INSTALL; michael@0: } michael@0: else if (aAddon.pendingUninstall) { michael@0: // If an add-on is pending uninstall then we ignore any other pending michael@0: // operations michael@0: return AddonManager.PENDING_UNINSTALL; michael@0: } michael@0: michael@0: if (aAddon.active && isAddonDisabled(aAddon)) michael@0: pending |= AddonManager.PENDING_DISABLE; michael@0: else if (!aAddon.active && !isAddonDisabled(aAddon)) michael@0: pending |= AddonManager.PENDING_ENABLE; michael@0: michael@0: if (aAddon.pendingUpgrade) michael@0: pending |= AddonManager.PENDING_UPGRADE; michael@0: michael@0: return pending; michael@0: }); michael@0: michael@0: this.__defineGetter__("operationsRequiringRestart", function AddonWrapper_operationsRequiringRestartGetter() { michael@0: let ops = 0; michael@0: if (XPIProvider.installRequiresRestart(aAddon)) michael@0: ops |= AddonManager.OP_NEEDS_RESTART_INSTALL; michael@0: if (XPIProvider.uninstallRequiresRestart(aAddon)) michael@0: ops |= AddonManager.OP_NEEDS_RESTART_UNINSTALL; michael@0: if (XPIProvider.enableRequiresRestart(aAddon)) michael@0: ops |= AddonManager.OP_NEEDS_RESTART_ENABLE; michael@0: if (XPIProvider.disableRequiresRestart(aAddon)) michael@0: ops |= AddonManager.OP_NEEDS_RESTART_DISABLE; michael@0: michael@0: return ops; michael@0: }); michael@0: michael@0: this.__defineGetter__("isDebuggable", function AddonWrapper_isDebuggable() { michael@0: return this.isActive && aAddon.bootstrap; michael@0: }); michael@0: michael@0: this.__defineGetter__("permissions", function AddonWrapper_permisionsGetter() { michael@0: let permissions = 0; michael@0: michael@0: // Add-ons that aren't installed cannot be modified in any way michael@0: if (!(aAddon.inDatabase)) michael@0: return permissions; michael@0: michael@0: // Experiments can only be uninstalled. An uninstall reflects the user michael@0: // intent of "disable this experiment." This is partially managed by the michael@0: // experiments manager. michael@0: if (aAddon.type == "experiment") { michael@0: return AddonManager.PERM_CAN_UNINSTALL; michael@0: } michael@0: michael@0: if (!aAddon.appDisabled) { michael@0: if (this.userDisabled) { michael@0: permissions |= AddonManager.PERM_CAN_ENABLE; michael@0: } michael@0: else if (aAddon.type != "theme") { michael@0: permissions |= AddonManager.PERM_CAN_DISABLE; michael@0: } michael@0: } michael@0: michael@0: // Add-ons that are in locked install locations, or are pending uninstall michael@0: // cannot be upgraded or uninstalled michael@0: if (!aAddon._installLocation.locked && !aAddon.pendingUninstall) { michael@0: // Add-ons that are installed by a file link cannot be upgraded michael@0: if (!aAddon._installLocation.isLinkedAddon(aAddon.id)) { michael@0: permissions |= AddonManager.PERM_CAN_UPGRADE; michael@0: } michael@0: michael@0: permissions |= AddonManager.PERM_CAN_UNINSTALL; michael@0: } michael@0: michael@0: return permissions; michael@0: }); michael@0: michael@0: this.__defineGetter__("isActive", function AddonWrapper_isActiveGetter() { michael@0: if (Services.appinfo.inSafeMode) michael@0: return false; michael@0: return aAddon.active; michael@0: }); michael@0: michael@0: this.__defineGetter__("userDisabled", function AddonWrapper_userDisabledGetter() { michael@0: if (XPIProvider._enabledExperiments.has(aAddon.id)) { michael@0: return false; michael@0: } michael@0: michael@0: return aAddon.softDisabled || aAddon.userDisabled; michael@0: }); michael@0: this.__defineSetter__("userDisabled", function AddonWrapper_userDisabledSetter(val) { michael@0: if (val == this.userDisabled) { michael@0: return val; michael@0: } michael@0: michael@0: if (aAddon.type == "experiment") { michael@0: if (val) { michael@0: XPIProvider._enabledExperiments.delete(aAddon.id); michael@0: } else { michael@0: XPIProvider._enabledExperiments.add(aAddon.id); michael@0: } michael@0: } michael@0: michael@0: if (aAddon.inDatabase) { michael@0: if (aAddon.type == "theme" && val) { michael@0: if (aAddon.internalName == XPIProvider.defaultSkin) michael@0: throw new Error("Cannot disable the default theme"); michael@0: XPIProvider.enableDefaultTheme(); michael@0: } michael@0: else { michael@0: XPIProvider.updateAddonDisabledState(aAddon, val); michael@0: } michael@0: } michael@0: else { michael@0: aAddon.userDisabled = val; michael@0: // When enabling remove the softDisabled flag michael@0: if (!val) michael@0: aAddon.softDisabled = false; michael@0: } michael@0: michael@0: return val; michael@0: }); michael@0: michael@0: this.__defineSetter__("softDisabled", function AddonWrapper_softDisabledSetter(val) { michael@0: if (val == aAddon.softDisabled) michael@0: return val; michael@0: michael@0: if (aAddon.inDatabase) { michael@0: // When softDisabling a theme just enable the active theme michael@0: if (aAddon.type == "theme" && val && !aAddon.userDisabled) { michael@0: if (aAddon.internalName == XPIProvider.defaultSkin) michael@0: throw new Error("Cannot disable the default theme"); michael@0: XPIProvider.enableDefaultTheme(); michael@0: } michael@0: else { michael@0: XPIProvider.updateAddonDisabledState(aAddon, undefined, val); michael@0: } michael@0: } michael@0: else { michael@0: // Only set softDisabled if not already disabled michael@0: if (!aAddon.userDisabled) michael@0: aAddon.softDisabled = val; michael@0: } michael@0: michael@0: return val; michael@0: }); michael@0: michael@0: this.isCompatibleWith = function AddonWrapper_isCompatiblewith(aAppVersion, aPlatformVersion) { michael@0: return aAddon.isCompatibleWith(aAppVersion, aPlatformVersion); michael@0: }; michael@0: michael@0: this.uninstall = function AddonWrapper_uninstall() { michael@0: if (!(aAddon.inDatabase)) michael@0: throw new Error("Cannot uninstall an add-on that isn't installed"); michael@0: if (aAddon.pendingUninstall) michael@0: throw new Error("Add-on is already marked to be uninstalled"); michael@0: XPIProvider.uninstallAddon(aAddon); michael@0: }; michael@0: michael@0: this.cancelUninstall = function AddonWrapper_cancelUninstall() { michael@0: if (!(aAddon.inDatabase)) michael@0: throw new Error("Cannot cancel uninstall for an add-on that isn't installed"); michael@0: if (!aAddon.pendingUninstall) michael@0: throw new Error("Add-on is not marked to be uninstalled"); michael@0: XPIProvider.cancelUninstallAddon(aAddon); michael@0: }; michael@0: michael@0: this.findUpdates = function AddonWrapper_findUpdates(aListener, aReason, aAppVersion, aPlatformVersion) { michael@0: // Short-circuit updates for experiments because updates are handled michael@0: // through the Experiments Manager. michael@0: if (this.type == "experiment") { michael@0: AddonManagerPrivate.callNoUpdateListeners(this, aListener, aReason, michael@0: aAppVersion, aPlatformVersion); michael@0: return; michael@0: } michael@0: michael@0: new UpdateChecker(aAddon, aListener, aReason, aAppVersion, aPlatformVersion); michael@0: }; michael@0: michael@0: // Returns true if there was an update in progress, false if there was no update to cancel michael@0: this.cancelUpdate = function AddonWrapper_cancelUpdate() { michael@0: if (aAddon._updateCheck) { michael@0: aAddon._updateCheck.cancel(); michael@0: return true; michael@0: } michael@0: return false; michael@0: }; michael@0: michael@0: this.hasResource = function AddonWrapper_hasResource(aPath) { michael@0: if (aAddon._hasResourceCache.has(aPath)) michael@0: return aAddon._hasResourceCache.get(aPath); michael@0: michael@0: let bundle = aAddon._sourceBundle.clone(); michael@0: michael@0: // Bundle may not exist any more if the addon has just been uninstalled, michael@0: // but explicitly first checking .exists() results in unneeded file I/O. michael@0: try { michael@0: var isDir = bundle.isDirectory(); michael@0: } catch (e) { michael@0: aAddon._hasResourceCache.set(aPath, false); michael@0: return false; michael@0: } michael@0: michael@0: if (isDir) { michael@0: if (aPath) { michael@0: aPath.split("/").forEach(function(aPart) { michael@0: bundle.append(aPart); michael@0: }); michael@0: } michael@0: let result = bundle.exists(); michael@0: aAddon._hasResourceCache.set(aPath, result); michael@0: return result; michael@0: } michael@0: michael@0: let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"]. michael@0: createInstance(Ci.nsIZipReader); michael@0: try { michael@0: zipReader.open(bundle); michael@0: let result = zipReader.hasEntry(aPath); michael@0: aAddon._hasResourceCache.set(aPath, result); michael@0: return result; michael@0: } michael@0: catch (e) { michael@0: aAddon._hasResourceCache.set(aPath, false); michael@0: return false; michael@0: } michael@0: finally { michael@0: zipReader.close(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Returns a URI to the selected resource or to the add-on bundle if aPath michael@0: * is null. URIs to the bundle will always be file: URIs. URIs to resources michael@0: * will be file: URIs if the add-on is unpacked or jar: URIs if the add-on is michael@0: * still an XPI file. michael@0: * michael@0: * @param aPath michael@0: * The path in the add-on to get the URI for or null to get a URI to michael@0: * the file or directory the add-on is installed as. michael@0: * @return an nsIURI michael@0: */ michael@0: this.getResourceURI = function AddonWrapper_getResourceURI(aPath) { michael@0: if (!aPath) michael@0: return NetUtil.newURI(aAddon._sourceBundle); michael@0: michael@0: return getURIForResourceInFile(aAddon._sourceBundle, aPath); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * An object which identifies a directory install location for add-ons. The michael@0: * location consists of a directory which contains the add-ons installed in the michael@0: * location. michael@0: * michael@0: * Each add-on installed in the location is either a directory containing the michael@0: * add-on's files or a text file containing an absolute path to the directory michael@0: * containing the add-ons files. The directory or text file must have the same michael@0: * name as the add-on's ID. michael@0: * michael@0: * There may also a special directory named "staged" which can contain michael@0: * directories with the same name as an add-on ID. If the directory is empty michael@0: * then it means the add-on will be uninstalled from this location during the michael@0: * next startup. If the directory contains the add-on's files then they will be michael@0: * installed during the next startup. michael@0: * michael@0: * @param aName michael@0: * The string identifier for the install location michael@0: * @param aDirectory michael@0: * The nsIFile directory for the install location michael@0: * @param aScope michael@0: * The scope of add-ons installed in this location michael@0: * @param aLocked michael@0: * true if add-ons cannot be installed, uninstalled or upgraded in the michael@0: * install location michael@0: */ michael@0: function DirectoryInstallLocation(aName, aDirectory, aScope, aLocked) { michael@0: this._name = aName; michael@0: this.locked = aLocked; michael@0: this._directory = aDirectory; michael@0: this._scope = aScope michael@0: this._IDToFileMap = {}; michael@0: this._FileToIDMap = {}; michael@0: this._linkedAddons = []; michael@0: this._stagingDirLock = 0; michael@0: michael@0: if (!aDirectory.exists()) michael@0: return; michael@0: if (!aDirectory.isDirectory()) michael@0: throw new Error("Location must be a directory."); michael@0: michael@0: this._readAddons(); michael@0: } michael@0: michael@0: DirectoryInstallLocation.prototype = { michael@0: _name : "", michael@0: _directory : null, michael@0: _IDToFileMap : null, // mapping from add-on ID to nsIFile michael@0: _FileToIDMap : null, // mapping from add-on path to add-on ID michael@0: michael@0: /** michael@0: * Reads a directory linked to in a file. michael@0: * michael@0: * @param file michael@0: * The file containing the directory path michael@0: * @return An nsIFile object representing the linked directory. michael@0: */ michael@0: _readDirectoryFromFile: function DirInstallLocation__readDirectoryFromFile(aFile) { michael@0: let fis = Cc["@mozilla.org/network/file-input-stream;1"]. michael@0: createInstance(Ci.nsIFileInputStream); michael@0: fis.init(aFile, -1, -1, false); michael@0: let line = { value: "" }; michael@0: if (fis instanceof Ci.nsILineInputStream) michael@0: fis.readLine(line); michael@0: fis.close(); michael@0: if (line.value) { michael@0: let linkedDirectory = Cc["@mozilla.org/file/local;1"]. michael@0: createInstance(Ci.nsIFile); michael@0: michael@0: try { michael@0: linkedDirectory.initWithPath(line.value); michael@0: } michael@0: catch (e) { michael@0: linkedDirectory.setRelativeDescriptor(aFile.parent, line.value); michael@0: } michael@0: michael@0: if (!linkedDirectory.exists()) { michael@0: logger.warn("File pointer " + aFile.path + " points to " + linkedDirectory.path + michael@0: " which does not exist"); michael@0: return null; michael@0: } michael@0: michael@0: if (!linkedDirectory.isDirectory()) { michael@0: logger.warn("File pointer " + aFile.path + " points to " + linkedDirectory.path + michael@0: " which is not a directory"); michael@0: return null; michael@0: } michael@0: michael@0: return linkedDirectory; michael@0: } michael@0: michael@0: logger.warn("File pointer " + aFile.path + " does not contain a path"); michael@0: return null; michael@0: }, michael@0: michael@0: /** michael@0: * Finds all the add-ons installed in this location. michael@0: */ michael@0: _readAddons: function DirInstallLocation__readAddons() { michael@0: // Use a snapshot of the directory contents to avoid possible issues with michael@0: // iterating over a directory while removing files from it (the YAFFS2 michael@0: // embedded filesystem has this issue, see bug 772238). michael@0: let entries = getDirectoryEntries(this._directory); michael@0: for (let entry of entries) { michael@0: let id = entry.leafName; michael@0: michael@0: if (id == DIR_STAGE || id == DIR_XPI_STAGE || id == DIR_TRASH) michael@0: continue; michael@0: michael@0: let directLoad = false; michael@0: if (entry.isFile() && michael@0: id.substring(id.length - 4).toLowerCase() == ".xpi") { michael@0: directLoad = true; michael@0: id = id.substring(0, id.length - 4); michael@0: } michael@0: michael@0: if (!gIDTest.test(id)) { michael@0: logger.debug("Ignoring file entry whose name is not a valid add-on ID: " + michael@0: entry.path); michael@0: continue; michael@0: } michael@0: michael@0: if (entry.isFile() && !directLoad) { michael@0: let newEntry = this._readDirectoryFromFile(entry); michael@0: if (!newEntry) { michael@0: logger.debug("Deleting stale pointer file " + entry.path); michael@0: try { michael@0: entry.remove(true); michael@0: } michael@0: catch (e) { michael@0: logger.warn("Failed to remove stale pointer file " + entry.path, e); michael@0: // Failing to remove the stale pointer file is ignorable michael@0: } michael@0: continue; michael@0: } michael@0: michael@0: entry = newEntry; michael@0: this._linkedAddons.push(id); michael@0: } michael@0: michael@0: this._IDToFileMap[id] = entry; michael@0: this._FileToIDMap[entry.path] = id; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Gets the name of this install location. michael@0: */ michael@0: get name() { michael@0: return this._name; michael@0: }, michael@0: michael@0: /** michael@0: * Gets the scope of this install location. michael@0: */ michael@0: get scope() { michael@0: return this._scope; michael@0: }, michael@0: michael@0: /** michael@0: * Gets an array of nsIFiles for add-ons installed in this location. michael@0: */ michael@0: get addonLocations() { michael@0: let locations = []; michael@0: for (let id in this._IDToFileMap) { michael@0: locations.push(this._IDToFileMap[id].clone()); michael@0: } michael@0: return locations; michael@0: }, michael@0: michael@0: /** michael@0: * Gets the staging directory to put add-ons that are pending install and michael@0: * uninstall into. michael@0: * michael@0: * @return an nsIFile michael@0: */ michael@0: getStagingDir: function DirInstallLocation_getStagingDir() { michael@0: let dir = this._directory.clone(); michael@0: dir.append(DIR_STAGE); michael@0: return dir; michael@0: }, michael@0: michael@0: requestStagingDir: function() { michael@0: this._stagingDirLock++; michael@0: michael@0: if (this._stagingDirPromise) michael@0: return this._stagingDirPromise; michael@0: michael@0: OS.File.makeDir(this._directory.path); michael@0: let stagepath = OS.Path.join(this._directory.path, DIR_STAGE); michael@0: return this._stagingDirPromise = OS.File.makeDir(stagepath).then(null, (e) => { michael@0: if (e instanceof OS.File.Error && e.becauseExists) michael@0: return; michael@0: logger.error("Failed to create staging directory", e); michael@0: throw e; michael@0: }); michael@0: }, michael@0: michael@0: releaseStagingDir: function() { michael@0: this._stagingDirLock--; michael@0: michael@0: if (this._stagingDirLock == 0) { michael@0: this._stagingDirPromise = null; michael@0: this.cleanStagingDir(); michael@0: } michael@0: michael@0: return Promise.resolve(); michael@0: }, michael@0: michael@0: /** michael@0: * Removes the specified files or directories in the staging directory and michael@0: * then if the staging directory is empty attempts to remove it. michael@0: * michael@0: * @param aLeafNames michael@0: * An array of file or directory to remove from the directory, the michael@0: * array may be empty michael@0: */ michael@0: cleanStagingDir: function(aLeafNames = []) { michael@0: let dir = this.getStagingDir(); michael@0: michael@0: for (let name of aLeafNames) { michael@0: let file = dir.clone(); michael@0: file.append(name); michael@0: recursiveRemove(file); michael@0: } michael@0: michael@0: if (this._stagingDirLock > 0) michael@0: return; michael@0: michael@0: let dirEntries = dir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator); michael@0: try { michael@0: if (dirEntries.nextFile) michael@0: return; michael@0: } michael@0: finally { michael@0: dirEntries.close(); michael@0: } michael@0: michael@0: try { michael@0: setFilePermissions(dir, FileUtils.PERMS_DIRECTORY); michael@0: dir.remove(false); michael@0: } michael@0: catch (e) { michael@0: logger.warn("Failed to remove staging dir", e); michael@0: // Failing to remove the staging directory is ignorable michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Gets the directory used by old versions for staging XPI and JAR files ready michael@0: * to be installed. michael@0: * michael@0: * @return an nsIFile michael@0: */ michael@0: getXPIStagingDir: function DirInstallLocation_getXPIStagingDir() { michael@0: let dir = this._directory.clone(); michael@0: dir.append(DIR_XPI_STAGE); michael@0: return dir; michael@0: }, michael@0: michael@0: /** michael@0: * Returns a directory that is normally on the same filesystem as the rest of michael@0: * the install location and can be used for temporarily storing files during michael@0: * safe move operations. Calling this method will delete the existing trash michael@0: * directory and its contents. michael@0: * michael@0: * @return an nsIFile michael@0: */ michael@0: getTrashDir: function DirInstallLocation_getTrashDir() { michael@0: let trashDir = this._directory.clone(); michael@0: trashDir.append(DIR_TRASH); michael@0: if (trashDir.exists()) michael@0: recursiveRemove(trashDir); michael@0: trashDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); michael@0: return trashDir; michael@0: }, michael@0: michael@0: /** michael@0: * Installs an add-on into the install location. michael@0: * michael@0: * @param aId michael@0: * The ID of the add-on to install michael@0: * @param aSource michael@0: * The source nsIFile to install from michael@0: * @param aExistingAddonID michael@0: * The ID of an existing add-on to uninstall at the same time michael@0: * @param aCopy michael@0: * If false the source files will be moved to the new location, michael@0: * otherwise they will only be copied michael@0: * @return an nsIFile indicating where the add-on was installed to michael@0: */ michael@0: installAddon: function DirInstallLocation_installAddon(aId, aSource, michael@0: aExistingAddonID, michael@0: aCopy) { michael@0: let trashDir = this.getTrashDir(); michael@0: michael@0: let transaction = new SafeInstallOperation(); michael@0: michael@0: let self = this; michael@0: function moveOldAddon(aId) { michael@0: let file = self._directory.clone(); michael@0: file.append(aId); michael@0: michael@0: if (file.exists()) michael@0: transaction.move(file, trashDir); michael@0: michael@0: file = self._directory.clone(); michael@0: file.append(aId + ".xpi"); michael@0: if (file.exists()) { michael@0: flushJarCache(file); michael@0: transaction.move(file, trashDir); michael@0: } michael@0: } michael@0: michael@0: // If any of these operations fails the finally block will clean up the michael@0: // temporary directory michael@0: try { michael@0: moveOldAddon(aId); michael@0: if (aExistingAddonID && aExistingAddonID != aId) michael@0: moveOldAddon(aExistingAddonID); michael@0: michael@0: if (aCopy) { michael@0: transaction.copy(aSource, this._directory); michael@0: } michael@0: else { michael@0: if (aSource.isFile()) michael@0: flushJarCache(aSource); michael@0: michael@0: transaction.move(aSource, this._directory); michael@0: } michael@0: } michael@0: finally { michael@0: // It isn't ideal if this cleanup fails but it isn't worth rolling back michael@0: // the install because of it. michael@0: try { michael@0: recursiveRemove(trashDir); michael@0: } michael@0: catch (e) { michael@0: logger.warn("Failed to remove trash directory when installing " + aId, e); michael@0: } michael@0: } michael@0: michael@0: let newFile = this._directory.clone(); michael@0: newFile.append(aSource.leafName); michael@0: try { michael@0: newFile.lastModifiedTime = Date.now(); michael@0: } catch (e) { michael@0: logger.warn("failed to set lastModifiedTime on " + newFile.path, e); michael@0: } michael@0: this._FileToIDMap[newFile.path] = aId; michael@0: this._IDToFileMap[aId] = newFile; michael@0: michael@0: if (aExistingAddonID && aExistingAddonID != aId && michael@0: aExistingAddonID in this._IDToFileMap) { michael@0: delete this._FileToIDMap[this._IDToFileMap[aExistingAddonID]]; michael@0: delete this._IDToFileMap[aExistingAddonID]; michael@0: } michael@0: michael@0: return newFile; michael@0: }, michael@0: michael@0: /** michael@0: * Uninstalls an add-on from this location. michael@0: * michael@0: * @param aId michael@0: * The ID of the add-on to uninstall michael@0: * @throws if the ID does not match any of the add-ons installed michael@0: */ michael@0: uninstallAddon: function DirInstallLocation_uninstallAddon(aId) { michael@0: let file = this._IDToFileMap[aId]; michael@0: if (!file) { michael@0: logger.warn("Attempted to remove " + aId + " from " + michael@0: this._name + " but it was already gone"); michael@0: return; michael@0: } michael@0: michael@0: file = this._directory.clone(); michael@0: file.append(aId); michael@0: if (!file.exists()) michael@0: file.leafName += ".xpi"; michael@0: michael@0: if (!file.exists()) { michael@0: logger.warn("Attempted to remove " + aId + " from " + michael@0: this._name + " but it was already gone"); michael@0: michael@0: delete this._FileToIDMap[file.path]; michael@0: delete this._IDToFileMap[aId]; michael@0: return; michael@0: } michael@0: michael@0: let trashDir = this.getTrashDir(); michael@0: michael@0: if (file.leafName != aId) { michael@0: logger.debug("uninstallAddon: flushing jar cache " + file.path + " for addon " + aId); michael@0: flushJarCache(file); michael@0: } michael@0: michael@0: let transaction = new SafeInstallOperation(); michael@0: michael@0: try { michael@0: transaction.move(file, trashDir); michael@0: } michael@0: finally { michael@0: // It isn't ideal if this cleanup fails, but it is probably better than michael@0: // rolling back the uninstall at this point michael@0: try { michael@0: recursiveRemove(trashDir); michael@0: } michael@0: catch (e) { michael@0: logger.warn("Failed to remove trash directory when uninstalling " + aId, e); michael@0: } michael@0: } michael@0: michael@0: delete this._FileToIDMap[file.path]; michael@0: delete this._IDToFileMap[aId]; michael@0: }, michael@0: michael@0: /** michael@0: * Gets the ID of the add-on installed in the given nsIFile. michael@0: * michael@0: * @param aFile michael@0: * The nsIFile to look in michael@0: * @return the ID michael@0: * @throws if the file does not represent an installed add-on michael@0: */ michael@0: getIDForLocation: function DirInstallLocation_getIDForLocation(aFile) { michael@0: if (aFile.path in this._FileToIDMap) michael@0: return this._FileToIDMap[aFile.path]; michael@0: throw new Error("Unknown add-on location " + aFile.path); michael@0: }, michael@0: michael@0: /** michael@0: * Gets the directory that the add-on with the given ID is installed in. michael@0: * michael@0: * @param aId michael@0: * The ID of the add-on michael@0: * @return The nsIFile michael@0: * @throws if the ID does not match any of the add-ons installed michael@0: */ michael@0: getLocationForID: function DirInstallLocation_getLocationForID(aId) { michael@0: if (aId in this._IDToFileMap) michael@0: return this._IDToFileMap[aId].clone(); michael@0: throw new Error("Unknown add-on ID " + aId); michael@0: }, michael@0: michael@0: /** michael@0: * Returns true if the given addon was installed in this location by a text michael@0: * file pointing to its real path. michael@0: * michael@0: * @param aId michael@0: * The ID of the addon michael@0: */ michael@0: isLinkedAddon: function DirInstallLocation__isLinkedAddon(aId) { michael@0: return this._linkedAddons.indexOf(aId) != -1; michael@0: } michael@0: }; michael@0: michael@0: #ifdef XP_WIN michael@0: /** michael@0: * An object that identifies a registry install location for add-ons. The location michael@0: * consists of a registry key which contains string values mapping ID to the michael@0: * path where an add-on is installed michael@0: * michael@0: * @param aName michael@0: * The string identifier of this Install Location. michael@0: * @param aRootKey michael@0: * The root key (one of the ROOT_KEY_ values from nsIWindowsRegKey). michael@0: * @param scope michael@0: * The scope of add-ons installed in this location michael@0: */ michael@0: function WinRegInstallLocation(aName, aRootKey, aScope) { michael@0: this.locked = true; michael@0: this._name = aName; michael@0: this._rootKey = aRootKey; michael@0: this._scope = aScope; michael@0: this._IDToFileMap = {}; michael@0: this._FileToIDMap = {}; michael@0: michael@0: let path = this._appKeyPath + "\\Extensions"; michael@0: let key = Cc["@mozilla.org/windows-registry-key;1"]. michael@0: createInstance(Ci.nsIWindowsRegKey); michael@0: michael@0: // Reading the registry may throw an exception, and that's ok. In error michael@0: // cases, we just leave ourselves in the empty state. michael@0: try { michael@0: key.open(this._rootKey, path, Ci.nsIWindowsRegKey.ACCESS_READ); michael@0: } michael@0: catch (e) { michael@0: return; michael@0: } michael@0: michael@0: this._readAddons(key); michael@0: key.close(); michael@0: } michael@0: michael@0: WinRegInstallLocation.prototype = { michael@0: _name : "", michael@0: _rootKey : null, michael@0: _scope : null, michael@0: _IDToFileMap : null, // mapping from ID to nsIFile michael@0: _FileToIDMap : null, // mapping from path to ID michael@0: michael@0: /** michael@0: * Retrieves the path of this Application's data key in the registry. michael@0: */ michael@0: get _appKeyPath() { michael@0: let appVendor = Services.appinfo.vendor; michael@0: let appName = Services.appinfo.name; michael@0: michael@0: #ifdef MOZ_THUNDERBIRD michael@0: // XXX Thunderbird doesn't specify a vendor string michael@0: if (appVendor == "") michael@0: appVendor = "Mozilla"; michael@0: #endif michael@0: michael@0: // XULRunner-based apps may intentionally not specify a vendor michael@0: if (appVendor != "") michael@0: appVendor += "\\"; michael@0: michael@0: return "SOFTWARE\\" + appVendor + appName; michael@0: }, michael@0: michael@0: /** michael@0: * Read the registry and build a mapping between ID and path for each michael@0: * installed add-on. michael@0: * michael@0: * @param key michael@0: * The key that contains the ID to path mapping michael@0: */ michael@0: _readAddons: function RegInstallLocation__readAddons(aKey) { michael@0: let count = aKey.valueCount; michael@0: for (let i = 0; i < count; ++i) { michael@0: let id = aKey.getValueName(i); michael@0: michael@0: let file = Cc["@mozilla.org/file/local;1"]. michael@0: createInstance(Ci.nsIFile); michael@0: file.initWithPath(aKey.readStringValue(id)); michael@0: michael@0: if (!file.exists()) { michael@0: logger.warn("Ignoring missing add-on in " + file.path); michael@0: continue; michael@0: } michael@0: michael@0: this._IDToFileMap[id] = file; michael@0: this._FileToIDMap[file.path] = id; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Gets the name of this install location. michael@0: */ michael@0: get name() { michael@0: return this._name; michael@0: }, michael@0: michael@0: /** michael@0: * Gets the scope of this install location. michael@0: */ michael@0: get scope() { michael@0: return this._scope; michael@0: }, michael@0: michael@0: /** michael@0: * Gets an array of nsIFiles for add-ons installed in this location. michael@0: */ michael@0: get addonLocations() { michael@0: let locations = []; michael@0: for (let id in this._IDToFileMap) { michael@0: locations.push(this._IDToFileMap[id].clone()); michael@0: } michael@0: return locations; michael@0: }, michael@0: michael@0: /** michael@0: * Gets the ID of the add-on installed in the given nsIFile. michael@0: * michael@0: * @param aFile michael@0: * The nsIFile to look in michael@0: * @return the ID michael@0: * @throws if the file does not represent an installed add-on michael@0: */ michael@0: getIDForLocation: function RegInstallLocation_getIDForLocation(aFile) { michael@0: if (aFile.path in this._FileToIDMap) michael@0: return this._FileToIDMap[aFile.path]; michael@0: throw new Error("Unknown add-on location"); michael@0: }, michael@0: michael@0: /** michael@0: * Gets the nsIFile that the add-on with the given ID is installed in. michael@0: * michael@0: * @param aId michael@0: * The ID of the add-on michael@0: * @return the nsIFile michael@0: */ michael@0: getLocationForID: function RegInstallLocation_getLocationForID(aId) { michael@0: if (aId in this._IDToFileMap) michael@0: return this._IDToFileMap[aId].clone(); michael@0: throw new Error("Unknown add-on ID"); michael@0: }, michael@0: michael@0: /** michael@0: * @see DirectoryInstallLocation michael@0: */ michael@0: isLinkedAddon: function RegInstallLocation_isLinkedAddon(aId) { michael@0: return true; michael@0: } michael@0: }; michael@0: #endif michael@0: michael@0: let addonTypes = [ michael@0: new AddonManagerPrivate.AddonType("extension", URI_EXTENSION_STRINGS, michael@0: STRING_TYPE_NAME, michael@0: AddonManager.VIEW_TYPE_LIST, 4000), michael@0: new AddonManagerPrivate.AddonType("theme", URI_EXTENSION_STRINGS, michael@0: STRING_TYPE_NAME, michael@0: AddonManager.VIEW_TYPE_LIST, 5000), michael@0: new AddonManagerPrivate.AddonType("dictionary", URI_EXTENSION_STRINGS, michael@0: STRING_TYPE_NAME, michael@0: AddonManager.VIEW_TYPE_LIST, 7000, michael@0: AddonManager.TYPE_UI_HIDE_EMPTY), michael@0: new AddonManagerPrivate.AddonType("locale", URI_EXTENSION_STRINGS, michael@0: STRING_TYPE_NAME, michael@0: AddonManager.VIEW_TYPE_LIST, 8000, michael@0: AddonManager.TYPE_UI_HIDE_EMPTY), michael@0: ]; michael@0: michael@0: // We only register experiments support if the application supports them. michael@0: // Ideally, we would install an observer to watch the pref. Installing michael@0: // an observer for this pref is not necessary here and may be buggy with michael@0: // regards to registering this XPIProvider twice. michael@0: if (Prefs.getBoolPref("experiments.supported", false)) { michael@0: addonTypes.push( michael@0: new AddonManagerPrivate.AddonType("experiment", michael@0: URI_EXTENSION_STRINGS, michael@0: STRING_TYPE_NAME, michael@0: AddonManager.VIEW_TYPE_LIST, 11000, michael@0: AddonManager.TYPE_UI_HIDE_EMPTY)); michael@0: } michael@0: michael@0: AddonManagerPrivate.registerProvider(XPIProvider, addonTypes);