1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,7434 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +const Cc = Components.classes; 1.11 +const Ci = Components.interfaces; 1.12 +const Cr = Components.results; 1.13 +const Cu = Components.utils; 1.14 + 1.15 +this.EXPORTED_SYMBOLS = ["XPIProvider"]; 1.16 + 1.17 +Components.utils.import("resource://gre/modules/Services.jsm"); 1.18 +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); 1.19 +Components.utils.import("resource://gre/modules/AddonManager.jsm"); 1.20 + 1.21 +XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository", 1.22 + "resource://gre/modules/addons/AddonRepository.jsm"); 1.23 +XPCOMUtils.defineLazyModuleGetter(this, "ChromeManifestParser", 1.24 + "resource://gre/modules/ChromeManifestParser.jsm"); 1.25 +XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager", 1.26 + "resource://gre/modules/LightweightThemeManager.jsm"); 1.27 +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", 1.28 + "resource://gre/modules/FileUtils.jsm"); 1.29 +XPCOMUtils.defineLazyModuleGetter(this, "ZipUtils", 1.30 + "resource://gre/modules/ZipUtils.jsm"); 1.31 +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", 1.32 + "resource://gre/modules/NetUtil.jsm"); 1.33 +XPCOMUtils.defineLazyModuleGetter(this, "PermissionsUtils", 1.34 + "resource://gre/modules/PermissionsUtils.jsm"); 1.35 +XPCOMUtils.defineLazyModuleGetter(this, "Promise", 1.36 + "resource://gre/modules/Promise.jsm"); 1.37 +XPCOMUtils.defineLazyModuleGetter(this, "Task", 1.38 + "resource://gre/modules/Task.jsm"); 1.39 +XPCOMUtils.defineLazyModuleGetter(this, "OS", 1.40 + "resource://gre/modules/osfile.jsm"); 1.41 +XPCOMUtils.defineLazyModuleGetter(this, "BrowserToolboxProcess", 1.42 + "resource:///modules/devtools/ToolboxProcess.jsm"); 1.43 + 1.44 +XPCOMUtils.defineLazyServiceGetter(this, 1.45 + "ChromeRegistry", 1.46 + "@mozilla.org/chrome/chrome-registry;1", 1.47 + "nsIChromeRegistry"); 1.48 +XPCOMUtils.defineLazyServiceGetter(this, 1.49 + "ResProtocolHandler", 1.50 + "@mozilla.org/network/protocol;1?name=resource", 1.51 + "nsIResProtocolHandler"); 1.52 + 1.53 +const nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile", 1.54 + "initWithPath"); 1.55 + 1.56 +const PREF_DB_SCHEMA = "extensions.databaseSchema"; 1.57 +const PREF_INSTALL_CACHE = "extensions.installCache"; 1.58 +const PREF_BOOTSTRAP_ADDONS = "extensions.bootstrappedAddons"; 1.59 +const PREF_PENDING_OPERATIONS = "extensions.pendingOperations"; 1.60 +const PREF_MATCH_OS_LOCALE = "intl.locale.matchOS"; 1.61 +const PREF_SELECTED_LOCALE = "general.useragent.locale"; 1.62 +const PREF_EM_DSS_ENABLED = "extensions.dss.enabled"; 1.63 +const PREF_DSS_SWITCHPENDING = "extensions.dss.switchPending"; 1.64 +const PREF_DSS_SKIN_TO_SELECT = "extensions.lastSelectedSkin"; 1.65 +const PREF_GENERAL_SKINS_SELECTEDSKIN = "general.skins.selectedSkin"; 1.66 +const PREF_EM_UPDATE_URL = "extensions.update.url"; 1.67 +const PREF_EM_UPDATE_BACKGROUND_URL = "extensions.update.background.url"; 1.68 +const PREF_EM_ENABLED_ADDONS = "extensions.enabledAddons"; 1.69 +const PREF_EM_EXTENSION_FORMAT = "extensions."; 1.70 +const PREF_EM_ENABLED_SCOPES = "extensions.enabledScopes"; 1.71 +const PREF_EM_AUTO_DISABLED_SCOPES = "extensions.autoDisableScopes"; 1.72 +const PREF_EM_SHOW_MISMATCH_UI = "extensions.showMismatchUI"; 1.73 +const PREF_XPI_ENABLED = "xpinstall.enabled"; 1.74 +const PREF_XPI_WHITELIST_REQUIRED = "xpinstall.whitelist.required"; 1.75 +const PREF_XPI_DIRECT_WHITELISTED = "xpinstall.whitelist.directRequest"; 1.76 +const PREF_XPI_FILE_WHITELISTED = "xpinstall.whitelist.fileRequest"; 1.77 +const PREF_XPI_PERMISSIONS_BRANCH = "xpinstall."; 1.78 +const PREF_XPI_UNPACK = "extensions.alwaysUnpack"; 1.79 +const PREF_INSTALL_REQUIREBUILTINCERTS = "extensions.install.requireBuiltInCerts"; 1.80 +const PREF_INSTALL_DISTRO_ADDONS = "extensions.installDistroAddons"; 1.81 +const PREF_BRANCH_INSTALLED_ADDON = "extensions.installedDistroAddon."; 1.82 +const PREF_SHOWN_SELECTION_UI = "extensions.shownSelectionUI"; 1.83 + 1.84 +const PREF_EM_MIN_COMPAT_APP_VERSION = "extensions.minCompatibleAppVersion"; 1.85 +const PREF_EM_MIN_COMPAT_PLATFORM_VERSION = "extensions.minCompatiblePlatformVersion"; 1.86 + 1.87 +const PREF_CHECKCOMAT_THEMEOVERRIDE = "extensions.checkCompatibility.temporaryThemeOverride_minAppVersion"; 1.88 + 1.89 +const URI_EXTENSION_SELECT_DIALOG = "chrome://mozapps/content/extensions/selectAddons.xul"; 1.90 +const URI_EXTENSION_UPDATE_DIALOG = "chrome://mozapps/content/extensions/update.xul"; 1.91 +const URI_EXTENSION_STRINGS = "chrome://mozapps/locale/extensions/extensions.properties"; 1.92 + 1.93 +const STRING_TYPE_NAME = "type.%ID%.name"; 1.94 + 1.95 +const DIR_EXTENSIONS = "extensions"; 1.96 +const DIR_STAGE = "staged"; 1.97 +const DIR_XPI_STAGE = "staged-xpis"; 1.98 +const DIR_TRASH = "trash"; 1.99 + 1.100 +const FILE_DATABASE = "extensions.json"; 1.101 +const FILE_OLD_CACHE = "extensions.cache"; 1.102 +const FILE_INSTALL_MANIFEST = "install.rdf"; 1.103 +const FILE_XPI_ADDONS_LIST = "extensions.ini"; 1.104 + 1.105 +const KEY_PROFILEDIR = "ProfD"; 1.106 +const KEY_APPDIR = "XCurProcD"; 1.107 +const KEY_TEMPDIR = "TmpD"; 1.108 +const KEY_APP_DISTRIBUTION = "XREAppDist"; 1.109 + 1.110 +const KEY_APP_PROFILE = "app-profile"; 1.111 +const KEY_APP_GLOBAL = "app-global"; 1.112 +const KEY_APP_SYSTEM_LOCAL = "app-system-local"; 1.113 +const KEY_APP_SYSTEM_SHARE = "app-system-share"; 1.114 +const KEY_APP_SYSTEM_USER = "app-system-user"; 1.115 + 1.116 +const NOTIFICATION_FLUSH_PERMISSIONS = "flush-pending-permissions"; 1.117 +const XPI_PERMISSION = "install"; 1.118 + 1.119 +const RDFURI_INSTALL_MANIFEST_ROOT = "urn:mozilla:install-manifest"; 1.120 +const PREFIX_NS_EM = "http://www.mozilla.org/2004/em-rdf#"; 1.121 + 1.122 +const TOOLKIT_ID = "toolkit@mozilla.org"; 1.123 + 1.124 +// The value for this is in Makefile.in 1.125 +#expand const DB_SCHEMA = __MOZ_EXTENSIONS_DB_SCHEMA__; 1.126 + 1.127 +// Properties that exist in the install manifest 1.128 +const PROP_METADATA = ["id", "version", "type", "internalName", "updateURL", 1.129 + "updateKey", "optionsURL", "optionsType", "aboutURL", 1.130 + "iconURL", "icon64URL"]; 1.131 +const PROP_LOCALE_SINGLE = ["name", "description", "creator", "homepageURL"]; 1.132 +const PROP_LOCALE_MULTI = ["developers", "translators", "contributors"]; 1.133 +const PROP_TARGETAPP = ["id", "minVersion", "maxVersion"]; 1.134 + 1.135 +// Properties that should be migrated where possible from an old database. These 1.136 +// shouldn't include properties that can be read directly from install.rdf files 1.137 +// or calculated 1.138 +const DB_MIGRATE_METADATA= ["installDate", "userDisabled", "softDisabled", 1.139 + "sourceURI", "applyBackgroundUpdates", 1.140 + "releaseNotesURI", "foreignInstall", "syncGUID"]; 1.141 +// Properties to cache and reload when an addon installation is pending 1.142 +const PENDING_INSTALL_METADATA = 1.143 + ["syncGUID", "targetApplications", "userDisabled", "softDisabled", 1.144 + "existingAddonID", "sourceURI", "releaseNotesURI", "installDate", 1.145 + "updateDate", "applyBackgroundUpdates", "compatibilityOverrides"]; 1.146 + 1.147 +// Note: When adding/changing/removing items here, remember to change the 1.148 +// DB schema version to ensure changes are picked up ASAP. 1.149 +const STATIC_BLOCKLIST_PATTERNS = [ 1.150 + { creator: "Mozilla Corp.", 1.151 + level: Ci.nsIBlocklistService.STATE_BLOCKED, 1.152 + blockID: "i162" }, 1.153 + { creator: "Mozilla.org", 1.154 + level: Ci.nsIBlocklistService.STATE_BLOCKED, 1.155 + blockID: "i162" } 1.156 +]; 1.157 + 1.158 + 1.159 +const BOOTSTRAP_REASONS = { 1.160 + APP_STARTUP : 1, 1.161 + APP_SHUTDOWN : 2, 1.162 + ADDON_ENABLE : 3, 1.163 + ADDON_DISABLE : 4, 1.164 + ADDON_INSTALL : 5, 1.165 + ADDON_UNINSTALL : 6, 1.166 + ADDON_UPGRADE : 7, 1.167 + ADDON_DOWNGRADE : 8 1.168 +}; 1.169 + 1.170 +// Map new string type identifiers to old style nsIUpdateItem types 1.171 +const TYPES = { 1.172 + extension: 2, 1.173 + theme: 4, 1.174 + locale: 8, 1.175 + multipackage: 32, 1.176 + dictionary: 64, 1.177 + experiment: 128, 1.178 +}; 1.179 + 1.180 +const RESTARTLESS_TYPES = new Set([ 1.181 + "dictionary", 1.182 + "experiment", 1.183 + "locale", 1.184 +]); 1.185 + 1.186 +// Keep track of where we are in startup for telemetry 1.187 +// event happened during XPIDatabase.startup() 1.188 +const XPI_STARTING = "XPIStarting"; 1.189 +// event happened after startup() but before the final-ui-startup event 1.190 +const XPI_BEFORE_UI_STARTUP = "BeforeFinalUIStartup"; 1.191 +// event happened after final-ui-startup 1.192 +const XPI_AFTER_UI_STARTUP = "AfterFinalUIStartup"; 1.193 + 1.194 +const COMPATIBLE_BY_DEFAULT_TYPES = { 1.195 + extension: true, 1.196 + dictionary: true 1.197 +}; 1.198 + 1.199 +const MSG_JAR_FLUSH = "AddonJarFlush"; 1.200 + 1.201 +var gGlobalScope = this; 1.202 + 1.203 +/** 1.204 + * Valid IDs fit this pattern. 1.205 + */ 1.206 +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; 1.207 + 1.208 +Cu.import("resource://gre/modules/Log.jsm"); 1.209 +const LOGGER_ID = "addons.xpi"; 1.210 + 1.211 +// Create a new logger for use by all objects in this Addons XPI Provider module 1.212 +// (Requires AddonManager.jsm) 1.213 +let logger = Log.repository.getLogger(LOGGER_ID); 1.214 + 1.215 +const LAZY_OBJECTS = ["XPIDatabase"]; 1.216 + 1.217 +var gLazyObjectsLoaded = false; 1.218 + 1.219 +function loadLazyObjects() { 1.220 + let scope = {}; 1.221 + scope.AddonInternal = AddonInternal; 1.222 + scope.XPIProvider = XPIProvider; 1.223 + Services.scriptloader.loadSubScript("resource://gre/modules/addons/XPIProviderUtils.js", 1.224 + scope); 1.225 + 1.226 + for (let name of LAZY_OBJECTS) { 1.227 + delete gGlobalScope[name]; 1.228 + gGlobalScope[name] = scope[name]; 1.229 + } 1.230 + gLazyObjectsLoaded = true; 1.231 + return scope; 1.232 +} 1.233 + 1.234 +for (let name of LAZY_OBJECTS) { 1.235 + Object.defineProperty(gGlobalScope, name, { 1.236 + get: function lazyObjectGetter() { 1.237 + let objs = loadLazyObjects(); 1.238 + return objs[name]; 1.239 + }, 1.240 + configurable: true 1.241 + }); 1.242 +} 1.243 + 1.244 + 1.245 +function findMatchingStaticBlocklistItem(aAddon) { 1.246 + for (let item of STATIC_BLOCKLIST_PATTERNS) { 1.247 + if ("creator" in item && typeof item.creator == "string") { 1.248 + if ((aAddon.defaultLocale && aAddon.defaultLocale.creator == item.creator) || 1.249 + (aAddon.selectedLocale && aAddon.selectedLocale.creator == item.creator)) { 1.250 + return item; 1.251 + } 1.252 + } 1.253 + } 1.254 + return null; 1.255 +} 1.256 + 1.257 + 1.258 +/** 1.259 + * Sets permissions on a file 1.260 + * 1.261 + * @param aFile 1.262 + * The file or directory to operate on. 1.263 + * @param aPermissions 1.264 + * The permisions to set 1.265 + */ 1.266 +function setFilePermissions(aFile, aPermissions) { 1.267 + try { 1.268 + aFile.permissions = aPermissions; 1.269 + } 1.270 + catch (e) { 1.271 + logger.warn("Failed to set permissions " + aPermissions.toString(8) + " on " + 1.272 + aFile.path, e); 1.273 + } 1.274 +} 1.275 + 1.276 +/** 1.277 + * A safe way to install a file or the contents of a directory to a new 1.278 + * directory. The file or directory is moved or copied recursively and if 1.279 + * anything fails an attempt is made to rollback the entire operation. The 1.280 + * operation may also be rolled back to its original state after it has 1.281 + * completed by calling the rollback method. 1.282 + * 1.283 + * Operations can be chained. Calling move or copy multiple times will remember 1.284 + * the whole set and if one fails all of the operations will be rolled back. 1.285 + */ 1.286 +function SafeInstallOperation() { 1.287 + this._installedFiles = []; 1.288 + this._createdDirs = []; 1.289 +} 1.290 + 1.291 +SafeInstallOperation.prototype = { 1.292 + _installedFiles: null, 1.293 + _createdDirs: null, 1.294 + 1.295 + _installFile: function SIO_installFile(aFile, aTargetDirectory, aCopy) { 1.296 + let oldFile = aCopy ? null : aFile.clone(); 1.297 + let newFile = aFile.clone(); 1.298 + try { 1.299 + if (aCopy) 1.300 + newFile.copyTo(aTargetDirectory, null); 1.301 + else 1.302 + newFile.moveTo(aTargetDirectory, null); 1.303 + } 1.304 + catch (e) { 1.305 + logger.error("Failed to " + (aCopy ? "copy" : "move") + " file " + aFile.path + 1.306 + " to " + aTargetDirectory.path, e); 1.307 + throw e; 1.308 + } 1.309 + this._installedFiles.push({ oldFile: oldFile, newFile: newFile }); 1.310 + }, 1.311 + 1.312 + _installDirectory: function SIO_installDirectory(aDirectory, aTargetDirectory, aCopy) { 1.313 + let newDir = aTargetDirectory.clone(); 1.314 + newDir.append(aDirectory.leafName); 1.315 + try { 1.316 + newDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); 1.317 + } 1.318 + catch (e) { 1.319 + logger.error("Failed to create directory " + newDir.path, e); 1.320 + throw e; 1.321 + } 1.322 + this._createdDirs.push(newDir); 1.323 + 1.324 + // Use a snapshot of the directory contents to avoid possible issues with 1.325 + // iterating over a directory while removing files from it (the YAFFS2 1.326 + // embedded filesystem has this issue, see bug 772238), and to remove 1.327 + // normal files before their resource forks on OSX (see bug 733436). 1.328 + let entries = getDirectoryEntries(aDirectory, true); 1.329 + entries.forEach(function(aEntry) { 1.330 + try { 1.331 + this._installDirEntry(aEntry, newDir, aCopy); 1.332 + } 1.333 + catch (e) { 1.334 + logger.error("Failed to " + (aCopy ? "copy" : "move") + " entry " + 1.335 + aEntry.path, e); 1.336 + throw e; 1.337 + } 1.338 + }, this); 1.339 + 1.340 + // If this is only a copy operation then there is nothing else to do 1.341 + if (aCopy) 1.342 + return; 1.343 + 1.344 + // The directory should be empty by this point. If it isn't this will throw 1.345 + // and all of the operations will be rolled back 1.346 + try { 1.347 + setFilePermissions(aDirectory, FileUtils.PERMS_DIRECTORY); 1.348 + aDirectory.remove(false); 1.349 + } 1.350 + catch (e) { 1.351 + logger.error("Failed to remove directory " + aDirectory.path, e); 1.352 + throw e; 1.353 + } 1.354 + 1.355 + // Note we put the directory move in after all the file moves so the 1.356 + // directory is recreated before all the files are moved back 1.357 + this._installedFiles.push({ oldFile: aDirectory, newFile: newDir }); 1.358 + }, 1.359 + 1.360 + _installDirEntry: function SIO_installDirEntry(aDirEntry, aTargetDirectory, aCopy) { 1.361 + let isDir = null; 1.362 + 1.363 + try { 1.364 + isDir = aDirEntry.isDirectory(); 1.365 + } 1.366 + catch (e) { 1.367 + // If the file has already gone away then don't worry about it, this can 1.368 + // happen on OSX where the resource fork is automatically moved with the 1.369 + // data fork for the file. See bug 733436. 1.370 + if (e.result == Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) 1.371 + return; 1.372 + 1.373 + logger.error("Failure " + (aCopy ? "copying" : "moving") + " " + aDirEntry.path + 1.374 + " to " + aTargetDirectory.path); 1.375 + throw e; 1.376 + } 1.377 + 1.378 + try { 1.379 + if (isDir) 1.380 + this._installDirectory(aDirEntry, aTargetDirectory, aCopy); 1.381 + else 1.382 + this._installFile(aDirEntry, aTargetDirectory, aCopy); 1.383 + } 1.384 + catch (e) { 1.385 + logger.error("Failure " + (aCopy ? "copying" : "moving") + " " + aDirEntry.path + 1.386 + " to " + aTargetDirectory.path); 1.387 + throw e; 1.388 + } 1.389 + }, 1.390 + 1.391 + /** 1.392 + * Moves a file or directory into a new directory. If an error occurs then all 1.393 + * files that have been moved will be moved back to their original location. 1.394 + * 1.395 + * @param aFile 1.396 + * The file or directory to be moved. 1.397 + * @param aTargetDirectory 1.398 + * The directory to move into, this is expected to be an empty 1.399 + * directory. 1.400 + */ 1.401 + move: function SIO_move(aFile, aTargetDirectory) { 1.402 + try { 1.403 + this._installDirEntry(aFile, aTargetDirectory, false); 1.404 + } 1.405 + catch (e) { 1.406 + this.rollback(); 1.407 + throw e; 1.408 + } 1.409 + }, 1.410 + 1.411 + /** 1.412 + * Copies a file or directory into a new directory. If an error occurs then 1.413 + * all new files that have been created will be removed. 1.414 + * 1.415 + * @param aFile 1.416 + * The file or directory to be copied. 1.417 + * @param aTargetDirectory 1.418 + * The directory to copy into, this is expected to be an empty 1.419 + * directory. 1.420 + */ 1.421 + copy: function SIO_copy(aFile, aTargetDirectory) { 1.422 + try { 1.423 + this._installDirEntry(aFile, aTargetDirectory, true); 1.424 + } 1.425 + catch (e) { 1.426 + this.rollback(); 1.427 + throw e; 1.428 + } 1.429 + }, 1.430 + 1.431 + /** 1.432 + * Rolls back all the moves that this operation performed. If an exception 1.433 + * occurs here then both old and new directories are left in an indeterminate 1.434 + * state 1.435 + */ 1.436 + rollback: function SIO_rollback() { 1.437 + while (this._installedFiles.length > 0) { 1.438 + let move = this._installedFiles.pop(); 1.439 + if (move.newFile.isDirectory()) { 1.440 + let oldDir = move.oldFile.parent.clone(); 1.441 + oldDir.append(move.oldFile.leafName); 1.442 + oldDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); 1.443 + } 1.444 + else if (!move.oldFile) { 1.445 + // No old file means this was a copied file 1.446 + move.newFile.remove(true); 1.447 + } 1.448 + else { 1.449 + move.newFile.moveTo(move.oldFile.parent, null); 1.450 + } 1.451 + } 1.452 + 1.453 + while (this._createdDirs.length > 0) 1.454 + recursiveRemove(this._createdDirs.pop()); 1.455 + } 1.456 +}; 1.457 + 1.458 +/** 1.459 + * Gets the currently selected locale for display. 1.460 + * @return the selected locale or "en-US" if none is selected 1.461 + */ 1.462 +function getLocale() { 1.463 + if (Prefs.getBoolPref(PREF_MATCH_OS_LOCALE, false)) 1.464 + return Services.locale.getLocaleComponentForUserAgent(); 1.465 + let locale = Prefs.getComplexValue(PREF_SELECTED_LOCALE, Ci.nsIPrefLocalizedString); 1.466 + if (locale) 1.467 + return locale; 1.468 + return Prefs.getCharPref(PREF_SELECTED_LOCALE, "en-US"); 1.469 +} 1.470 + 1.471 +/** 1.472 + * Selects the closest matching locale from a list of locales. 1.473 + * 1.474 + * @param aLocales 1.475 + * An array of locales 1.476 + * @return the best match for the currently selected locale 1.477 + */ 1.478 +function findClosestLocale(aLocales) { 1.479 + let appLocale = getLocale(); 1.480 + 1.481 + // Holds the best matching localized resource 1.482 + var bestmatch = null; 1.483 + // The number of locale parts it matched with 1.484 + var bestmatchcount = 0; 1.485 + // The number of locale parts in the match 1.486 + var bestpartcount = 0; 1.487 + 1.488 + var matchLocales = [appLocale.toLowerCase()]; 1.489 + /* If the current locale is English then it will find a match if there is 1.490 + a valid match for en-US so no point searching that locale too. */ 1.491 + if (matchLocales[0].substring(0, 3) != "en-") 1.492 + matchLocales.push("en-us"); 1.493 + 1.494 + for each (var locale in matchLocales) { 1.495 + var lparts = locale.split("-"); 1.496 + for each (var localized in aLocales) { 1.497 + for each (let found in localized.locales) { 1.498 + found = found.toLowerCase(); 1.499 + // Exact match is returned immediately 1.500 + if (locale == found) 1.501 + return localized; 1.502 + 1.503 + var fparts = found.split("-"); 1.504 + /* If we have found a possible match and this one isn't any longer 1.505 + then we dont need to check further. */ 1.506 + if (bestmatch && fparts.length < bestmatchcount) 1.507 + continue; 1.508 + 1.509 + // Count the number of parts that match 1.510 + var maxmatchcount = Math.min(fparts.length, lparts.length); 1.511 + var matchcount = 0; 1.512 + while (matchcount < maxmatchcount && 1.513 + fparts[matchcount] == lparts[matchcount]) 1.514 + matchcount++; 1.515 + 1.516 + /* If we matched more than the last best match or matched the same and 1.517 + this locale is less specific than the last best match. */ 1.518 + if (matchcount > bestmatchcount || 1.519 + (matchcount == bestmatchcount && fparts.length < bestpartcount)) { 1.520 + bestmatch = localized; 1.521 + bestmatchcount = matchcount; 1.522 + bestpartcount = fparts.length; 1.523 + } 1.524 + } 1.525 + } 1.526 + // If we found a valid match for this locale return it 1.527 + if (bestmatch) 1.528 + return bestmatch; 1.529 + } 1.530 + return null; 1.531 +} 1.532 + 1.533 +/** 1.534 + * Sets the userDisabled and softDisabled properties of an add-on based on what 1.535 + * values those properties had for a previous instance of the add-on. The 1.536 + * previous instance may be a previous install or in the case of an application 1.537 + * version change the same add-on. 1.538 + * 1.539 + * NOTE: this may modify aNewAddon in place; callers should save the database if 1.540 + * necessary 1.541 + * 1.542 + * @param aOldAddon 1.543 + * The previous instance of the add-on 1.544 + * @param aNewAddon 1.545 + * The new instance of the add-on 1.546 + * @param aAppVersion 1.547 + * The optional application version to use when checking the blocklist 1.548 + * or undefined to use the current application 1.549 + * @param aPlatformVersion 1.550 + * The optional platform version to use when checking the blocklist or 1.551 + * undefined to use the current platform 1.552 + */ 1.553 +function applyBlocklistChanges(aOldAddon, aNewAddon, aOldAppVersion, 1.554 + aOldPlatformVersion) { 1.555 + // Copy the properties by default 1.556 + aNewAddon.userDisabled = aOldAddon.userDisabled; 1.557 + aNewAddon.softDisabled = aOldAddon.softDisabled; 1.558 + 1.559 + let bs = Cc["@mozilla.org/extensions/blocklist;1"]. 1.560 + getService(Ci.nsIBlocklistService); 1.561 + 1.562 + let oldBlocklistState = bs.getAddonBlocklistState(createWrapper(aOldAddon), 1.563 + aOldAppVersion, 1.564 + aOldPlatformVersion); 1.565 + let newBlocklistState = bs.getAddonBlocklistState(createWrapper(aNewAddon)); 1.566 + 1.567 + // If the blocklist state hasn't changed then the properties don't need to 1.568 + // change 1.569 + if (newBlocklistState == oldBlocklistState) 1.570 + return; 1.571 + 1.572 + if (newBlocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED) { 1.573 + if (aNewAddon.type != "theme") { 1.574 + // The add-on has become softblocked, set softDisabled if it isn't already 1.575 + // userDisabled 1.576 + aNewAddon.softDisabled = !aNewAddon.userDisabled; 1.577 + } 1.578 + else { 1.579 + // Themes just get userDisabled to switch back to the default theme 1.580 + aNewAddon.userDisabled = true; 1.581 + } 1.582 + } 1.583 + else { 1.584 + // If the new add-on is not softblocked then it cannot be softDisabled 1.585 + aNewAddon.softDisabled = false; 1.586 + } 1.587 +} 1.588 + 1.589 +/** 1.590 + * Calculates whether an add-on should be appDisabled or not. 1.591 + * 1.592 + * @param aAddon 1.593 + * The add-on to check 1.594 + * @return true if the add-on should not be appDisabled 1.595 + */ 1.596 +function isUsableAddon(aAddon) { 1.597 + // Hack to ensure the default theme is always usable 1.598 + if (aAddon.type == "theme" && aAddon.internalName == XPIProvider.defaultSkin) 1.599 + return true; 1.600 + 1.601 + if (aAddon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) 1.602 + return false; 1.603 + 1.604 + if (AddonManager.checkUpdateSecurity && !aAddon.providesUpdatesSecurely) 1.605 + return false; 1.606 + 1.607 + if (!aAddon.isPlatformCompatible) 1.608 + return false; 1.609 + 1.610 + if (AddonManager.checkCompatibility) { 1.611 + if (!aAddon.isCompatible) 1.612 + return false; 1.613 + } 1.614 + else { 1.615 + let app = aAddon.matchingTargetApplication; 1.616 + if (!app) 1.617 + return false; 1.618 + 1.619 + // XXX Temporary solution to let applications opt-in to make themes safer 1.620 + // following significant UI changes even if checkCompatibility=false has 1.621 + // been set, until we get bug 962001. 1.622 + if (aAddon.type == "theme" && app.id == Services.appinfo.ID) { 1.623 + try { 1.624 + let minCompatVersion = Services.prefs.getCharPref(PREF_CHECKCOMAT_THEMEOVERRIDE); 1.625 + if (minCompatVersion && 1.626 + Services.vc.compare(minCompatVersion, app.maxVersion) > 0) { 1.627 + return false; 1.628 + } 1.629 + } catch (e) {} 1.630 + } 1.631 + } 1.632 + 1.633 + return true; 1.634 +} 1.635 + 1.636 +function isAddonDisabled(aAddon) { 1.637 + return aAddon.appDisabled || aAddon.softDisabled || aAddon.userDisabled; 1.638 +} 1.639 + 1.640 +XPCOMUtils.defineLazyServiceGetter(this, "gRDF", "@mozilla.org/rdf/rdf-service;1", 1.641 + Ci.nsIRDFService); 1.642 + 1.643 +function EM_R(aProperty) { 1.644 + return gRDF.GetResource(PREFIX_NS_EM + aProperty); 1.645 +} 1.646 + 1.647 +/** 1.648 + * Converts an RDF literal, resource or integer into a string. 1.649 + * 1.650 + * @param aLiteral 1.651 + * The RDF object to convert 1.652 + * @return a string if the object could be converted or null 1.653 + */ 1.654 +function getRDFValue(aLiteral) { 1.655 + if (aLiteral instanceof Ci.nsIRDFLiteral) 1.656 + return aLiteral.Value; 1.657 + if (aLiteral instanceof Ci.nsIRDFResource) 1.658 + return aLiteral.Value; 1.659 + if (aLiteral instanceof Ci.nsIRDFInt) 1.660 + return aLiteral.Value; 1.661 + return null; 1.662 +} 1.663 + 1.664 +/** 1.665 + * Gets an RDF property as a string 1.666 + * 1.667 + * @param aDs 1.668 + * The RDF datasource to read the property from 1.669 + * @param aResource 1.670 + * The RDF resource to read the property from 1.671 + * @param aProperty 1.672 + * The property to read 1.673 + * @return a string if the property existed or null 1.674 + */ 1.675 +function getRDFProperty(aDs, aResource, aProperty) { 1.676 + return getRDFValue(aDs.GetTarget(aResource, EM_R(aProperty), true)); 1.677 +} 1.678 + 1.679 +/** 1.680 + * Reads an AddonInternal object from an RDF stream. 1.681 + * 1.682 + * @param aUri 1.683 + * The URI that the manifest is being read from 1.684 + * @param aStream 1.685 + * An open stream to read the RDF from 1.686 + * @return an AddonInternal object 1.687 + * @throws if the install manifest in the RDF stream is corrupt or could not 1.688 + * be read 1.689 + */ 1.690 +function loadManifestFromRDF(aUri, aStream) { 1.691 + function getPropertyArray(aDs, aSource, aProperty) { 1.692 + let values = []; 1.693 + let targets = aDs.GetTargets(aSource, EM_R(aProperty), true); 1.694 + while (targets.hasMoreElements()) 1.695 + values.push(getRDFValue(targets.getNext())); 1.696 + 1.697 + return values; 1.698 + } 1.699 + 1.700 + /** 1.701 + * Reads locale properties from either the main install manifest root or 1.702 + * an em:localized section in the install manifest. 1.703 + * 1.704 + * @param aDs 1.705 + * The nsIRDFDatasource to read from 1.706 + * @param aSource 1.707 + * The nsIRDFResource to read the properties from 1.708 + * @param isDefault 1.709 + * True if the locale is to be read from the main install manifest 1.710 + * root 1.711 + * @param aSeenLocales 1.712 + * An array of locale names already seen for this install manifest. 1.713 + * Any locale names seen as a part of this function will be added to 1.714 + * this array 1.715 + * @return an object containing the locale properties 1.716 + */ 1.717 + function readLocale(aDs, aSource, isDefault, aSeenLocales) { 1.718 + let locale = { }; 1.719 + if (!isDefault) { 1.720 + locale.locales = []; 1.721 + let targets = ds.GetTargets(aSource, EM_R("locale"), true); 1.722 + while (targets.hasMoreElements()) { 1.723 + let localeName = getRDFValue(targets.getNext()); 1.724 + if (!localeName) { 1.725 + logger.warn("Ignoring empty locale in localized properties"); 1.726 + continue; 1.727 + } 1.728 + if (aSeenLocales.indexOf(localeName) != -1) { 1.729 + logger.warn("Ignoring duplicate locale in localized properties"); 1.730 + continue; 1.731 + } 1.732 + aSeenLocales.push(localeName); 1.733 + locale.locales.push(localeName); 1.734 + } 1.735 + 1.736 + if (locale.locales.length == 0) { 1.737 + logger.warn("Ignoring localized properties with no listed locales"); 1.738 + return null; 1.739 + } 1.740 + } 1.741 + 1.742 + PROP_LOCALE_SINGLE.forEach(function(aProp) { 1.743 + locale[aProp] = getRDFProperty(aDs, aSource, aProp); 1.744 + }); 1.745 + 1.746 + PROP_LOCALE_MULTI.forEach(function(aProp) { 1.747 + // Don't store empty arrays 1.748 + let props = getPropertyArray(aDs, aSource, 1.749 + aProp.substring(0, aProp.length - 1)); 1.750 + if (props.length > 0) 1.751 + locale[aProp] = props; 1.752 + }); 1.753 + 1.754 + return locale; 1.755 + } 1.756 + 1.757 + let rdfParser = Cc["@mozilla.org/rdf/xml-parser;1"]. 1.758 + createInstance(Ci.nsIRDFXMLParser) 1.759 + let ds = Cc["@mozilla.org/rdf/datasource;1?name=in-memory-datasource"]. 1.760 + createInstance(Ci.nsIRDFDataSource); 1.761 + let listener = rdfParser.parseAsync(ds, aUri); 1.762 + let channel = Cc["@mozilla.org/network/input-stream-channel;1"]. 1.763 + createInstance(Ci.nsIInputStreamChannel); 1.764 + channel.setURI(aUri); 1.765 + channel.contentStream = aStream; 1.766 + channel.QueryInterface(Ci.nsIChannel); 1.767 + channel.contentType = "text/xml"; 1.768 + 1.769 + listener.onStartRequest(channel, null); 1.770 + 1.771 + try { 1.772 + let pos = 0; 1.773 + let count = aStream.available(); 1.774 + while (count > 0) { 1.775 + listener.onDataAvailable(channel, null, aStream, pos, count); 1.776 + pos += count; 1.777 + count = aStream.available(); 1.778 + } 1.779 + listener.onStopRequest(channel, null, Components.results.NS_OK); 1.780 + } 1.781 + catch (e) { 1.782 + listener.onStopRequest(channel, null, e.result); 1.783 + throw e; 1.784 + } 1.785 + 1.786 + let root = gRDF.GetResource(RDFURI_INSTALL_MANIFEST_ROOT); 1.787 + let addon = new AddonInternal(); 1.788 + PROP_METADATA.forEach(function(aProp) { 1.789 + addon[aProp] = getRDFProperty(ds, root, aProp); 1.790 + }); 1.791 + addon.unpack = getRDFProperty(ds, root, "unpack") == "true"; 1.792 + 1.793 + if (!addon.type) { 1.794 + addon.type = addon.internalName ? "theme" : "extension"; 1.795 + } 1.796 + else { 1.797 + let type = addon.type; 1.798 + addon.type = null; 1.799 + for (let name in TYPES) { 1.800 + if (TYPES[name] == type) { 1.801 + addon.type = name; 1.802 + break; 1.803 + } 1.804 + } 1.805 + } 1.806 + 1.807 + if (!(addon.type in TYPES)) 1.808 + throw new Error("Install manifest specifies unknown type: " + addon.type); 1.809 + 1.810 + if (addon.type != "multipackage") { 1.811 + if (!addon.id) 1.812 + throw new Error("No ID in install manifest"); 1.813 + if (!gIDTest.test(addon.id)) 1.814 + throw new Error("Illegal add-on ID " + addon.id); 1.815 + if (!addon.version) 1.816 + throw new Error("No version in install manifest"); 1.817 + } 1.818 + 1.819 + addon.strictCompatibility = !(addon.type in COMPATIBLE_BY_DEFAULT_TYPES) || 1.820 + getRDFProperty(ds, root, "strictCompatibility") == "true"; 1.821 + 1.822 + // Only read the bootstrap property for extensions. 1.823 + if (addon.type == "extension") { 1.824 + addon.bootstrap = getRDFProperty(ds, root, "bootstrap") == "true"; 1.825 + if (addon.optionsType && 1.826 + addon.optionsType != AddonManager.OPTIONS_TYPE_DIALOG && 1.827 + addon.optionsType != AddonManager.OPTIONS_TYPE_INLINE && 1.828 + addon.optionsType != AddonManager.OPTIONS_TYPE_TAB && 1.829 + addon.optionsType != AddonManager.OPTIONS_TYPE_INLINE_INFO) { 1.830 + throw new Error("Install manifest specifies unknown type: " + addon.optionsType); 1.831 + } 1.832 + } 1.833 + else { 1.834 + // Some add-on types are always restartless. 1.835 + if (RESTARTLESS_TYPES.has(addon.type)) { 1.836 + addon.bootstrap = true; 1.837 + } 1.838 + 1.839 + // Only extensions are allowed to provide an optionsURL, optionsType or aboutURL. For 1.840 + // all other types they are silently ignored 1.841 + addon.optionsURL = null; 1.842 + addon.optionsType = null; 1.843 + addon.aboutURL = null; 1.844 + 1.845 + if (addon.type == "theme") { 1.846 + if (!addon.internalName) 1.847 + throw new Error("Themes must include an internalName property"); 1.848 + addon.skinnable = getRDFProperty(ds, root, "skinnable") == "true"; 1.849 + } 1.850 + } 1.851 + 1.852 + addon.defaultLocale = readLocale(ds, root, true); 1.853 + 1.854 + let seenLocales = []; 1.855 + addon.locales = []; 1.856 + let targets = ds.GetTargets(root, EM_R("localized"), true); 1.857 + while (targets.hasMoreElements()) { 1.858 + let target = targets.getNext().QueryInterface(Ci.nsIRDFResource); 1.859 + let locale = readLocale(ds, target, false, seenLocales); 1.860 + if (locale) 1.861 + addon.locales.push(locale); 1.862 + } 1.863 + 1.864 + let seenApplications = []; 1.865 + addon.targetApplications = []; 1.866 + targets = ds.GetTargets(root, EM_R("targetApplication"), true); 1.867 + while (targets.hasMoreElements()) { 1.868 + let target = targets.getNext().QueryInterface(Ci.nsIRDFResource); 1.869 + let targetAppInfo = {}; 1.870 + PROP_TARGETAPP.forEach(function(aProp) { 1.871 + targetAppInfo[aProp] = getRDFProperty(ds, target, aProp); 1.872 + }); 1.873 + if (!targetAppInfo.id || !targetAppInfo.minVersion || 1.874 + !targetAppInfo.maxVersion) { 1.875 + logger.warn("Ignoring invalid targetApplication entry in install manifest"); 1.876 + continue; 1.877 + } 1.878 + if (seenApplications.indexOf(targetAppInfo.id) != -1) { 1.879 + logger.warn("Ignoring duplicate targetApplication entry for " + targetAppInfo.id + 1.880 + " in install manifest"); 1.881 + continue; 1.882 + } 1.883 + seenApplications.push(targetAppInfo.id); 1.884 + addon.targetApplications.push(targetAppInfo); 1.885 + } 1.886 + 1.887 + // Note that we don't need to check for duplicate targetPlatform entries since 1.888 + // the RDF service coalesces them for us. 1.889 + let targetPlatforms = getPropertyArray(ds, root, "targetPlatform"); 1.890 + addon.targetPlatforms = []; 1.891 + targetPlatforms.forEach(function(aPlatform) { 1.892 + let platform = { 1.893 + os: null, 1.894 + abi: null 1.895 + }; 1.896 + 1.897 + let pos = aPlatform.indexOf("_"); 1.898 + if (pos != -1) { 1.899 + platform.os = aPlatform.substring(0, pos); 1.900 + platform.abi = aPlatform.substring(pos + 1); 1.901 + } 1.902 + else { 1.903 + platform.os = aPlatform; 1.904 + } 1.905 + 1.906 + addon.targetPlatforms.push(platform); 1.907 + }); 1.908 + 1.909 + // A theme's userDisabled value is true if the theme is not the selected skin 1.910 + // or if there is an active lightweight theme. We ignore whether softblocking 1.911 + // is in effect since it would change the active theme. 1.912 + if (addon.type == "theme") { 1.913 + addon.userDisabled = !!LightweightThemeManager.currentTheme || 1.914 + addon.internalName != XPIProvider.selectedSkin; 1.915 + } 1.916 + // Experiments are disabled by default. It is up to the Experiments Manager 1.917 + // to enable them (it drives installation). 1.918 + else if (addon.type == "experiment") { 1.919 + addon.userDisabled = true; 1.920 + } 1.921 + else { 1.922 + addon.userDisabled = false; 1.923 + addon.softDisabled = addon.blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED; 1.924 + } 1.925 + 1.926 + addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT; 1.927 + 1.928 + // Experiments are managed and updated through an external "experiments 1.929 + // manager." So disable some built-in mechanisms. 1.930 + if (addon.type == "experiment") { 1.931 + addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DISABLE; 1.932 + addon.updateURL = null; 1.933 + addon.updateKey = null; 1.934 + 1.935 + addon.targetApplications = []; 1.936 + addon.targetPlatforms = []; 1.937 + } 1.938 + 1.939 + // Load the storage service before NSS (nsIRandomGenerator), 1.940 + // to avoid a SQLite initialization error (bug 717904). 1.941 + let storage = Services.storage; 1.942 + 1.943 + // Generate random GUID used for Sync. 1.944 + // This was lifted from util.js:makeGUID() from services-sync. 1.945 + let rng = Cc["@mozilla.org/security/random-generator;1"]. 1.946 + createInstance(Ci.nsIRandomGenerator); 1.947 + let bytes = rng.generateRandomBytes(9); 1.948 + let byte_string = [String.fromCharCode(byte) for each (byte in bytes)] 1.949 + .join(""); 1.950 + // Base64 encode 1.951 + addon.syncGUID = btoa(byte_string).replace(/\+/g, '-') 1.952 + .replace(/\//g, '_'); 1.953 + 1.954 + return addon; 1.955 +} 1.956 + 1.957 +/** 1.958 + * Loads an AddonInternal object from an add-on extracted in a directory. 1.959 + * 1.960 + * @param aDir 1.961 + * The nsIFile directory holding the add-on 1.962 + * @return an AddonInternal object 1.963 + * @throws if the directory does not contain a valid install manifest 1.964 + */ 1.965 +function loadManifestFromDir(aDir) { 1.966 + function getFileSize(aFile) { 1.967 + if (aFile.isSymlink()) 1.968 + return 0; 1.969 + 1.970 + if (!aFile.isDirectory()) 1.971 + return aFile.fileSize; 1.972 + 1.973 + let size = 0; 1.974 + let entries = aFile.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator); 1.975 + let entry; 1.976 + while ((entry = entries.nextFile)) 1.977 + size += getFileSize(entry); 1.978 + entries.close(); 1.979 + return size; 1.980 + } 1.981 + 1.982 + let file = aDir.clone(); 1.983 + file.append(FILE_INSTALL_MANIFEST); 1.984 + if (!file.exists() || !file.isFile()) 1.985 + throw new Error("Directory " + aDir.path + " does not contain a valid " + 1.986 + "install manifest"); 1.987 + 1.988 + let fis = Cc["@mozilla.org/network/file-input-stream;1"]. 1.989 + createInstance(Ci.nsIFileInputStream); 1.990 + fis.init(file, -1, -1, false); 1.991 + let bis = Cc["@mozilla.org/network/buffered-input-stream;1"]. 1.992 + createInstance(Ci.nsIBufferedInputStream); 1.993 + bis.init(fis, 4096); 1.994 + 1.995 + try { 1.996 + let addon = loadManifestFromRDF(Services.io.newFileURI(file), bis); 1.997 + addon._sourceBundle = aDir.clone(); 1.998 + addon.size = getFileSize(aDir); 1.999 + 1.1000 + file = aDir.clone(); 1.1001 + file.append("chrome.manifest"); 1.1002 + let chromeManifest = ChromeManifestParser.parseSync(Services.io.newFileURI(file)); 1.1003 + addon.hasBinaryComponents = ChromeManifestParser.hasType(chromeManifest, 1.1004 + "binary-component"); 1.1005 + 1.1006 + addon.appDisabled = !isUsableAddon(addon); 1.1007 + return addon; 1.1008 + } 1.1009 + finally { 1.1010 + bis.close(); 1.1011 + fis.close(); 1.1012 + } 1.1013 +} 1.1014 + 1.1015 +/** 1.1016 + * Loads an AddonInternal object from an nsIZipReader for an add-on. 1.1017 + * 1.1018 + * @param aZipReader 1.1019 + * An open nsIZipReader for the add-on's files 1.1020 + * @return an AddonInternal object 1.1021 + * @throws if the XPI file does not contain a valid install manifest 1.1022 + */ 1.1023 +function loadManifestFromZipReader(aZipReader) { 1.1024 + let zis = aZipReader.getInputStream(FILE_INSTALL_MANIFEST); 1.1025 + let bis = Cc["@mozilla.org/network/buffered-input-stream;1"]. 1.1026 + createInstance(Ci.nsIBufferedInputStream); 1.1027 + bis.init(zis, 4096); 1.1028 + 1.1029 + try { 1.1030 + let uri = buildJarURI(aZipReader.file, FILE_INSTALL_MANIFEST); 1.1031 + let addon = loadManifestFromRDF(uri, bis); 1.1032 + addon._sourceBundle = aZipReader.file; 1.1033 + 1.1034 + addon.size = 0; 1.1035 + let entries = aZipReader.findEntries(null); 1.1036 + while (entries.hasMore()) 1.1037 + addon.size += aZipReader.getEntry(entries.getNext()).realSize; 1.1038 + 1.1039 + // Binary components can only be loaded from unpacked addons. 1.1040 + if (addon.unpack) { 1.1041 + uri = buildJarURI(aZipReader.file, "chrome.manifest"); 1.1042 + let chromeManifest = ChromeManifestParser.parseSync(uri); 1.1043 + addon.hasBinaryComponents = ChromeManifestParser.hasType(chromeManifest, 1.1044 + "binary-component"); 1.1045 + } else { 1.1046 + addon.hasBinaryComponents = false; 1.1047 + } 1.1048 + 1.1049 + addon.appDisabled = !isUsableAddon(addon); 1.1050 + return addon; 1.1051 + } 1.1052 + finally { 1.1053 + bis.close(); 1.1054 + zis.close(); 1.1055 + } 1.1056 +} 1.1057 + 1.1058 +/** 1.1059 + * Loads an AddonInternal object from an add-on in an XPI file. 1.1060 + * 1.1061 + * @param aXPIFile 1.1062 + * An nsIFile pointing to the add-on's XPI file 1.1063 + * @return an AddonInternal object 1.1064 + * @throws if the XPI file does not contain a valid install manifest 1.1065 + */ 1.1066 +function loadManifestFromZipFile(aXPIFile) { 1.1067 + let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"]. 1.1068 + createInstance(Ci.nsIZipReader); 1.1069 + try { 1.1070 + zipReader.open(aXPIFile); 1.1071 + 1.1072 + return loadManifestFromZipReader(zipReader); 1.1073 + } 1.1074 + finally { 1.1075 + zipReader.close(); 1.1076 + } 1.1077 +} 1.1078 + 1.1079 +function loadManifestFromFile(aFile) { 1.1080 + if (aFile.isFile()) 1.1081 + return loadManifestFromZipFile(aFile); 1.1082 + else 1.1083 + return loadManifestFromDir(aFile); 1.1084 +} 1.1085 + 1.1086 +/** 1.1087 + * Gets an nsIURI for a file within another file, either a directory or an XPI 1.1088 + * file. If aFile is a directory then this will return a file: URI, if it is an 1.1089 + * XPI file then it will return a jar: URI. 1.1090 + * 1.1091 + * @param aFile 1.1092 + * The file containing the resources, must be either a directory or an 1.1093 + * XPI file 1.1094 + * @param aPath 1.1095 + * The path to find the resource at, "/" separated. If aPath is empty 1.1096 + * then the uri to the root of the contained files will be returned 1.1097 + * @return an nsIURI pointing at the resource 1.1098 + */ 1.1099 +function getURIForResourceInFile(aFile, aPath) { 1.1100 + if (aFile.isDirectory()) { 1.1101 + let resource = aFile.clone(); 1.1102 + if (aPath) { 1.1103 + aPath.split("/").forEach(function(aPart) { 1.1104 + resource.append(aPart); 1.1105 + }); 1.1106 + } 1.1107 + return NetUtil.newURI(resource); 1.1108 + } 1.1109 + 1.1110 + return buildJarURI(aFile, aPath); 1.1111 +} 1.1112 + 1.1113 +/** 1.1114 + * Creates a jar: URI for a file inside a ZIP file. 1.1115 + * 1.1116 + * @param aJarfile 1.1117 + * The ZIP file as an nsIFile 1.1118 + * @param aPath 1.1119 + * The path inside the ZIP file 1.1120 + * @return an nsIURI for the file 1.1121 + */ 1.1122 +function buildJarURI(aJarfile, aPath) { 1.1123 + let uri = Services.io.newFileURI(aJarfile); 1.1124 + uri = "jar:" + uri.spec + "!/" + aPath; 1.1125 + return NetUtil.newURI(uri); 1.1126 +} 1.1127 + 1.1128 +/** 1.1129 + * Sends local and remote notifications to flush a JAR file cache entry 1.1130 + * 1.1131 + * @param aJarFile 1.1132 + * The ZIP/XPI/JAR file as a nsIFile 1.1133 + */ 1.1134 +function flushJarCache(aJarFile) { 1.1135 + Services.obs.notifyObservers(aJarFile, "flush-cache-entry", null); 1.1136 + Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageBroadcaster) 1.1137 + .broadcastAsyncMessage(MSG_JAR_FLUSH, aJarFile.path); 1.1138 +} 1.1139 + 1.1140 +function flushStartupCache() { 1.1141 + // Init this, so it will get the notification. 1.1142 + Services.obs.notifyObservers(null, "startupcache-invalidate", null); 1.1143 +} 1.1144 + 1.1145 +/** 1.1146 + * Creates and returns a new unique temporary file. The caller should delete 1.1147 + * the file when it is no longer needed. 1.1148 + * 1.1149 + * @return an nsIFile that points to a randomly named, initially empty file in 1.1150 + * the OS temporary files directory 1.1151 + */ 1.1152 +function getTemporaryFile() { 1.1153 + let file = FileUtils.getDir(KEY_TEMPDIR, []); 1.1154 + let random = Math.random().toString(36).replace(/0./, '').substr(-3); 1.1155 + file.append("tmp-" + random + ".xpi"); 1.1156 + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE); 1.1157 + 1.1158 + return file; 1.1159 +} 1.1160 + 1.1161 +/** 1.1162 + * Verifies that a zip file's contents are all signed by the same principal. 1.1163 + * Directory entries and anything in the META-INF directory are not checked. 1.1164 + * 1.1165 + * @param aZip 1.1166 + * A nsIZipReader to check 1.1167 + * @param aPrincipal 1.1168 + * The nsIPrincipal to compare against 1.1169 + * @return true if all the contents that should be signed were signed by the 1.1170 + * principal 1.1171 + */ 1.1172 +function verifyZipSigning(aZip, aPrincipal) { 1.1173 + var count = 0; 1.1174 + var entries = aZip.findEntries(null); 1.1175 + while (entries.hasMore()) { 1.1176 + var entry = entries.getNext(); 1.1177 + // Nothing in META-INF is in the manifest. 1.1178 + if (entry.substr(0, 9) == "META-INF/") 1.1179 + continue; 1.1180 + // Directory entries aren't in the manifest. 1.1181 + if (entry.substr(-1) == "/") 1.1182 + continue; 1.1183 + count++; 1.1184 + var entryPrincipal = aZip.getCertificatePrincipal(entry); 1.1185 + if (!entryPrincipal || !aPrincipal.equals(entryPrincipal)) 1.1186 + return false; 1.1187 + } 1.1188 + return aZip.manifestEntriesCount == count; 1.1189 +} 1.1190 + 1.1191 +/** 1.1192 + * Replaces %...% strings in an addon url (update and updateInfo) with 1.1193 + * appropriate values. 1.1194 + * 1.1195 + * @param aAddon 1.1196 + * The AddonInternal representing the add-on 1.1197 + * @param aUri 1.1198 + * The uri to escape 1.1199 + * @param aUpdateType 1.1200 + * An optional number representing the type of update, only applicable 1.1201 + * when creating a url for retrieving an update manifest 1.1202 + * @param aAppVersion 1.1203 + * The optional application version to use for %APP_VERSION% 1.1204 + * @return the appropriately escaped uri. 1.1205 + */ 1.1206 +function escapeAddonURI(aAddon, aUri, aUpdateType, aAppVersion) 1.1207 +{ 1.1208 + let uri = AddonManager.escapeAddonURI(aAddon, aUri, aAppVersion); 1.1209 + 1.1210 + // If there is an updateType then replace the UPDATE_TYPE string 1.1211 + if (aUpdateType) 1.1212 + uri = uri.replace(/%UPDATE_TYPE%/g, aUpdateType); 1.1213 + 1.1214 + // If this add-on has compatibility information for either the current 1.1215 + // application or toolkit then replace the ITEM_MAXAPPVERSION with the 1.1216 + // maxVersion 1.1217 + let app = aAddon.matchingTargetApplication; 1.1218 + if (app) 1.1219 + var maxVersion = app.maxVersion; 1.1220 + else 1.1221 + maxVersion = ""; 1.1222 + uri = uri.replace(/%ITEM_MAXAPPVERSION%/g, maxVersion); 1.1223 + 1.1224 + let compatMode = "normal"; 1.1225 + if (!AddonManager.checkCompatibility) 1.1226 + compatMode = "ignore"; 1.1227 + else if (AddonManager.strictCompatibility) 1.1228 + compatMode = "strict"; 1.1229 + uri = uri.replace(/%COMPATIBILITY_MODE%/g, compatMode); 1.1230 + 1.1231 + return uri; 1.1232 +} 1.1233 + 1.1234 +function removeAsync(aFile) { 1.1235 + return Task.spawn(function () { 1.1236 + let info = null; 1.1237 + try { 1.1238 + info = yield OS.File.stat(aFile.path); 1.1239 + if (info.isDir) 1.1240 + yield OS.File.removeDir(aFile.path); 1.1241 + else 1.1242 + yield OS.File.remove(aFile.path); 1.1243 + } 1.1244 + catch (e if e instanceof OS.File.Error && e.becauseNoSuchFile) { 1.1245 + // The file has already gone away 1.1246 + return; 1.1247 + } 1.1248 + }); 1.1249 +} 1.1250 + 1.1251 +/** 1.1252 + * Recursively removes a directory or file fixing permissions when necessary. 1.1253 + * 1.1254 + * @param aFile 1.1255 + * The nsIFile to remove 1.1256 + */ 1.1257 +function recursiveRemove(aFile) { 1.1258 + let isDir = null; 1.1259 + 1.1260 + try { 1.1261 + isDir = aFile.isDirectory(); 1.1262 + } 1.1263 + catch (e) { 1.1264 + // If the file has already gone away then don't worry about it, this can 1.1265 + // happen on OSX where the resource fork is automatically moved with the 1.1266 + // data fork for the file. See bug 733436. 1.1267 + if (e.result == Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) 1.1268 + return; 1.1269 + if (e.result == Cr.NS_ERROR_FILE_NOT_FOUND) 1.1270 + return; 1.1271 + 1.1272 + throw e; 1.1273 + } 1.1274 + 1.1275 + setFilePermissions(aFile, isDir ? FileUtils.PERMS_DIRECTORY 1.1276 + : FileUtils.PERMS_FILE); 1.1277 + 1.1278 + try { 1.1279 + aFile.remove(true); 1.1280 + return; 1.1281 + } 1.1282 + catch (e) { 1.1283 + if (!aFile.isDirectory()) { 1.1284 + logger.error("Failed to remove file " + aFile.path, e); 1.1285 + throw e; 1.1286 + } 1.1287 + } 1.1288 + 1.1289 + // Use a snapshot of the directory contents to avoid possible issues with 1.1290 + // iterating over a directory while removing files from it (the YAFFS2 1.1291 + // embedded filesystem has this issue, see bug 772238), and to remove 1.1292 + // normal files before their resource forks on OSX (see bug 733436). 1.1293 + let entries = getDirectoryEntries(aFile, true); 1.1294 + entries.forEach(recursiveRemove); 1.1295 + 1.1296 + try { 1.1297 + aFile.remove(true); 1.1298 + } 1.1299 + catch (e) { 1.1300 + logger.error("Failed to remove empty directory " + aFile.path, e); 1.1301 + throw e; 1.1302 + } 1.1303 +} 1.1304 + 1.1305 +/** 1.1306 + * Returns the timestamp and leaf file name of the most recently modified 1.1307 + * entry in a directory, 1.1308 + * or simply the file's own timestamp if it is not a directory. 1.1309 + * Also returns the total number of items (directories and files) visited in the scan 1.1310 + * 1.1311 + * @param aFile 1.1312 + * A non-null nsIFile object 1.1313 + * @return [File Name, Epoch time, items visited], as described above. 1.1314 + */ 1.1315 +function recursiveLastModifiedTime(aFile) { 1.1316 + try { 1.1317 + let modTime = aFile.lastModifiedTime; 1.1318 + let fileName = aFile.leafName; 1.1319 + if (aFile.isFile()) 1.1320 + return [fileName, modTime, 1]; 1.1321 + 1.1322 + if (aFile.isDirectory()) { 1.1323 + let entries = aFile.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator); 1.1324 + let entry; 1.1325 + let totalItems = 1; 1.1326 + while ((entry = entries.nextFile)) { 1.1327 + let [subName, subTime, items] = recursiveLastModifiedTime(entry); 1.1328 + totalItems += items; 1.1329 + if (subTime > modTime) { 1.1330 + modTime = subTime; 1.1331 + fileName = subName; 1.1332 + } 1.1333 + } 1.1334 + entries.close(); 1.1335 + return [fileName, modTime, totalItems]; 1.1336 + } 1.1337 + } 1.1338 + catch (e) { 1.1339 + logger.warn("Problem getting last modified time for " + aFile.path, e); 1.1340 + } 1.1341 + 1.1342 + // If the file is something else, just ignore it. 1.1343 + return ["", 0, 0]; 1.1344 +} 1.1345 + 1.1346 +/** 1.1347 + * Gets a snapshot of directory entries. 1.1348 + * 1.1349 + * @param aDir 1.1350 + * Directory to look at 1.1351 + * @param aSortEntries 1.1352 + * True to sort entries by filename 1.1353 + * @return An array of nsIFile, or an empty array if aDir is not a readable directory 1.1354 + */ 1.1355 +function getDirectoryEntries(aDir, aSortEntries) { 1.1356 + let dirEnum; 1.1357 + try { 1.1358 + dirEnum = aDir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator); 1.1359 + let entries = []; 1.1360 + while (dirEnum.hasMoreElements()) 1.1361 + entries.push(dirEnum.nextFile); 1.1362 + 1.1363 + if (aSortEntries) { 1.1364 + entries.sort(function sortDirEntries(a, b) { 1.1365 + return a.path > b.path ? -1 : 1; 1.1366 + }); 1.1367 + } 1.1368 + 1.1369 + return entries 1.1370 + } 1.1371 + catch (e) { 1.1372 + logger.warn("Can't iterate directory " + aDir.path, e); 1.1373 + return []; 1.1374 + } 1.1375 + finally { 1.1376 + if (dirEnum) { 1.1377 + dirEnum.close(); 1.1378 + } 1.1379 + } 1.1380 +} 1.1381 + 1.1382 +/** 1.1383 + * A helpful wrapper around the prefs service that allows for default values 1.1384 + * when requested values aren't set. 1.1385 + */ 1.1386 +var Prefs = { 1.1387 + /** 1.1388 + * Gets a preference from the default branch ignoring user-set values. 1.1389 + * 1.1390 + * @param aName 1.1391 + * The name of the preference 1.1392 + * @param aDefaultValue 1.1393 + * A value to return if the preference does not exist 1.1394 + * @return the default value of the preference or aDefaultValue if there is 1.1395 + * none 1.1396 + */ 1.1397 + getDefaultCharPref: function Prefs_getDefaultCharPref(aName, aDefaultValue) { 1.1398 + try { 1.1399 + return Services.prefs.getDefaultBranch("").getCharPref(aName); 1.1400 + } 1.1401 + catch (e) { 1.1402 + } 1.1403 + return aDefaultValue; 1.1404 + }, 1.1405 + 1.1406 + /** 1.1407 + * Gets a string preference. 1.1408 + * 1.1409 + * @param aName 1.1410 + * The name of the preference 1.1411 + * @param aDefaultValue 1.1412 + * A value to return if the preference does not exist 1.1413 + * @return the value of the preference or aDefaultValue if there is none 1.1414 + */ 1.1415 + getCharPref: function Prefs_getCharPref(aName, aDefaultValue) { 1.1416 + try { 1.1417 + return Services.prefs.getCharPref(aName); 1.1418 + } 1.1419 + catch (e) { 1.1420 + } 1.1421 + return aDefaultValue; 1.1422 + }, 1.1423 + 1.1424 + /** 1.1425 + * Gets a complex preference. 1.1426 + * 1.1427 + * @param aName 1.1428 + * The name of the preference 1.1429 + * @param aType 1.1430 + * The interface type of the preference 1.1431 + * @param aDefaultValue 1.1432 + * A value to return if the preference does not exist 1.1433 + * @return the value of the preference or aDefaultValue if there is none 1.1434 + */ 1.1435 + getComplexValue: function Prefs_getComplexValue(aName, aType, aDefaultValue) { 1.1436 + try { 1.1437 + return Services.prefs.getComplexValue(aName, aType).data; 1.1438 + } 1.1439 + catch (e) { 1.1440 + } 1.1441 + return aDefaultValue; 1.1442 + }, 1.1443 + 1.1444 + /** 1.1445 + * Gets a boolean preference. 1.1446 + * 1.1447 + * @param aName 1.1448 + * The name of the preference 1.1449 + * @param aDefaultValue 1.1450 + * A value to return if the preference does not exist 1.1451 + * @return the value of the preference or aDefaultValue if there is none 1.1452 + */ 1.1453 + getBoolPref: function Prefs_getBoolPref(aName, aDefaultValue) { 1.1454 + try { 1.1455 + return Services.prefs.getBoolPref(aName); 1.1456 + } 1.1457 + catch (e) { 1.1458 + } 1.1459 + return aDefaultValue; 1.1460 + }, 1.1461 + 1.1462 + /** 1.1463 + * Gets an integer preference. 1.1464 + * 1.1465 + * @param aName 1.1466 + * The name of the preference 1.1467 + * @param defaultValue 1.1468 + * A value to return if the preference does not exist 1.1469 + * @return the value of the preference or defaultValue if there is none 1.1470 + */ 1.1471 + getIntPref: function Prefs_getIntPref(aName, defaultValue) { 1.1472 + try { 1.1473 + return Services.prefs.getIntPref(aName); 1.1474 + } 1.1475 + catch (e) { 1.1476 + } 1.1477 + return defaultValue; 1.1478 + }, 1.1479 + 1.1480 + /** 1.1481 + * Clears a preference if it has a user value 1.1482 + * 1.1483 + * @param aName 1.1484 + * The name of the preference 1.1485 + */ 1.1486 + clearUserPref: function Prefs_clearUserPref(aName) { 1.1487 + if (Services.prefs.prefHasUserValue(aName)) 1.1488 + Services.prefs.clearUserPref(aName); 1.1489 + } 1.1490 +} 1.1491 + 1.1492 +// Helper function to compare JSON saved version of the directory state 1.1493 +// with the new state returned by getInstallLocationStates() 1.1494 +// Structure is: ordered array of {'name':?, 'addons': {addonID: {'descriptor':?, 'mtime':?} ...}} 1.1495 +function directoryStateDiffers(aState, aCache) 1.1496 +{ 1.1497 + // check equality of an object full of addons; fortunately we can destroy the 'aOld' object 1.1498 + function addonsMismatch(aNew, aOld) { 1.1499 + for (let [id, val] of aNew) { 1.1500 + if (!id in aOld) 1.1501 + return true; 1.1502 + if (val.descriptor != aOld[id].descriptor || 1.1503 + val.mtime != aOld[id].mtime) 1.1504 + return true; 1.1505 + delete aOld[id]; 1.1506 + } 1.1507 + // make sure aOld doesn't have any extra entries 1.1508 + for (let id in aOld) 1.1509 + return true; 1.1510 + return false; 1.1511 + } 1.1512 + 1.1513 + if (!aCache) 1.1514 + return true; 1.1515 + try { 1.1516 + let old = JSON.parse(aCache); 1.1517 + if (aState.length != old.length) 1.1518 + return true; 1.1519 + for (let i = 0; i < aState.length; i++) { 1.1520 + // conveniently, any missing fields would require a 'true' return, which is 1.1521 + // handled by our catch wrapper 1.1522 + if (aState[i].name != old[i].name) 1.1523 + return true; 1.1524 + if (addonsMismatch(aState[i].addons, old[i].addons)) 1.1525 + return true; 1.1526 + } 1.1527 + } 1.1528 + catch (e) { 1.1529 + return true; 1.1530 + } 1.1531 + return false; 1.1532 +} 1.1533 + 1.1534 +/** 1.1535 + * Wraps a function in an exception handler to protect against exceptions inside callbacks 1.1536 + * @param aFunction function(args...) 1.1537 + * @return function(args...), a function that takes the same arguments as aFunction 1.1538 + * and returns the same result unless aFunction throws, in which case it logs 1.1539 + * a warning and returns undefined. 1.1540 + */ 1.1541 +function makeSafe(aFunction) { 1.1542 + return function(...aArgs) { 1.1543 + try { 1.1544 + return aFunction(...aArgs); 1.1545 + } 1.1546 + catch(ex) { 1.1547 + logger.warn("XPIProvider callback failed", ex); 1.1548 + } 1.1549 + return undefined; 1.1550 + } 1.1551 +} 1.1552 + 1.1553 +this.XPIProvider = { 1.1554 + // An array of known install locations 1.1555 + installLocations: null, 1.1556 + // A dictionary of known install locations by name 1.1557 + installLocationsByName: null, 1.1558 + // An array of currently active AddonInstalls 1.1559 + installs: null, 1.1560 + // The default skin for the application 1.1561 + defaultSkin: "classic/1.0", 1.1562 + // The current skin used by the application 1.1563 + currentSkin: null, 1.1564 + // The selected skin to be used by the application when it is restarted. This 1.1565 + // will be the same as currentSkin when it is the skin to be used when the 1.1566 + // application is restarted 1.1567 + selectedSkin: null, 1.1568 + // The value of the minCompatibleAppVersion preference 1.1569 + minCompatibleAppVersion: null, 1.1570 + // The value of the minCompatiblePlatformVersion preference 1.1571 + minCompatiblePlatformVersion: null, 1.1572 + // A dictionary of the file descriptors for bootstrappable add-ons by ID 1.1573 + bootstrappedAddons: {}, 1.1574 + // A dictionary of JS scopes of loaded bootstrappable add-ons by ID 1.1575 + bootstrapScopes: {}, 1.1576 + // True if the platform could have activated extensions 1.1577 + extensionsActive: false, 1.1578 + // File / directory state of installed add-ons 1.1579 + installStates: [], 1.1580 + // True if all of the add-ons found during startup were installed in the 1.1581 + // application install location 1.1582 + allAppGlobal: true, 1.1583 + // A string listing the enabled add-ons for annotating crash reports 1.1584 + enabledAddons: null, 1.1585 + // An array of add-on IDs of add-ons that were inactive during startup 1.1586 + inactiveAddonIDs: [], 1.1587 + // Keep track of startup phases for telemetry 1.1588 + runPhase: XPI_STARTING, 1.1589 + // Keep track of the newest file in each add-on, in case we want to 1.1590 + // report it to telemetry. 1.1591 + _mostRecentlyModifiedFile: {}, 1.1592 + // Per-addon telemetry information 1.1593 + _telemetryDetails: {}, 1.1594 + // Experiments are disabled by default. Track ones that are locally enabled. 1.1595 + _enabledExperiments: null, 1.1596 + 1.1597 + /* 1.1598 + * Set a value in the telemetry hash for a given ID 1.1599 + */ 1.1600 + setTelemetry: function XPI_setTelemetry(aId, aName, aValue) { 1.1601 + if (!this._telemetryDetails[aId]) 1.1602 + this._telemetryDetails[aId] = {}; 1.1603 + this._telemetryDetails[aId][aName] = aValue; 1.1604 + }, 1.1605 + 1.1606 + // Keep track of in-progress operations that support cancel() 1.1607 + _inProgress: new Set(), 1.1608 + 1.1609 + doing: function XPI_doing(aCancellable) { 1.1610 + this._inProgress.add(aCancellable); 1.1611 + }, 1.1612 + 1.1613 + done: function XPI_done(aCancellable) { 1.1614 + return this._inProgress.delete(aCancellable); 1.1615 + }, 1.1616 + 1.1617 + cancelAll: function XPI_cancelAll() { 1.1618 + // Cancelling one may alter _inProgress, so restart the iterator after each 1.1619 + while (this._inProgress.size > 0) { 1.1620 + for (let c of this._inProgress) { 1.1621 + try { 1.1622 + c.cancel(); 1.1623 + } 1.1624 + catch (e) { 1.1625 + logger.warn("Cancel failed", e); 1.1626 + } 1.1627 + this._inProgress.delete(c); 1.1628 + } 1.1629 + } 1.1630 + }, 1.1631 + 1.1632 + /** 1.1633 + * Adds or updates a URI mapping for an Addon.id. 1.1634 + * 1.1635 + * Mappings should not be removed at any point. This is so that the mappings 1.1636 + * will be still valid after an add-on gets disabled or uninstalled, as 1.1637 + * consumers may still have URIs of (leaked) resources they want to map. 1.1638 + */ 1.1639 + _addURIMapping: function XPI__addURIMapping(aID, aFile) { 1.1640 + try { 1.1641 + // Always use our own mechanics instead of nsIIOService.newFileURI, so 1.1642 + // that we can be sure to map things as we want them mapped. 1.1643 + let uri = this._resolveURIToFile(getURIForResourceInFile(aFile, ".")); 1.1644 + if (!uri) { 1.1645 + throw new Error("Cannot resolve"); 1.1646 + } 1.1647 + this._ensureURIMappings(); 1.1648 + this._uriMappings[aID] = uri.spec; 1.1649 + } 1.1650 + catch (ex) { 1.1651 + logger.warn("Failed to add URI mapping", ex); 1.1652 + } 1.1653 + }, 1.1654 + 1.1655 + /** 1.1656 + * Ensures that the URI to Addon mappings are available. 1.1657 + * 1.1658 + * The function will add mappings for all non-bootstrapped but enabled 1.1659 + * add-ons. 1.1660 + * Bootstrapped add-on mappings will be added directly when the bootstrap 1.1661 + * scope get loaded. (See XPIProvider._addURIMapping() and callers) 1.1662 + */ 1.1663 + _ensureURIMappings: function XPI__ensureURIMappings() { 1.1664 + if (this._uriMappings) { 1.1665 + return; 1.1666 + } 1.1667 + // XXX Convert to Map(), once it gets stable with stable iterators 1.1668 + this._uriMappings = Object.create(null); 1.1669 + 1.1670 + // XXX Convert to Set(), once it gets stable with stable iterators 1.1671 + let enabled = Object.create(null); 1.1672 + let enabledAddons = this.enabledAddons || ""; 1.1673 + for (let a of enabledAddons.split(",")) { 1.1674 + a = decodeURIComponent(a.split(":")[0]); 1.1675 + enabled[a] = null; 1.1676 + } 1.1677 + 1.1678 + let cache = JSON.parse(Prefs.getCharPref(PREF_INSTALL_CACHE, "[]")); 1.1679 + for (let loc of cache) { 1.1680 + for (let [id, val] in Iterator(loc.addons)) { 1.1681 + if (!(id in enabled)) { 1.1682 + continue; 1.1683 + } 1.1684 + let file = new nsIFile(val.descriptor); 1.1685 + let spec = Services.io.newFileURI(file).spec; 1.1686 + this._uriMappings[id] = spec; 1.1687 + } 1.1688 + } 1.1689 + }, 1.1690 + 1.1691 + /** 1.1692 + * Resolve a URI back to physical file. 1.1693 + * 1.1694 + * Of course, this works only for URIs pointing to local resources. 1.1695 + * 1.1696 + * @param aURI 1.1697 + * URI to resolve 1.1698 + * @return 1.1699 + * resolved nsIFileURL 1.1700 + */ 1.1701 + _resolveURIToFile: function XPI__resolveURIToFile(aURI) { 1.1702 + switch (aURI.scheme) { 1.1703 + case "jar": 1.1704 + case "file": 1.1705 + if (aURI instanceof Ci.nsIJARURI) { 1.1706 + return this._resolveURIToFile(aURI.JARFile); 1.1707 + } 1.1708 + return aURI; 1.1709 + 1.1710 + case "chrome": 1.1711 + aURI = ChromeRegistry.convertChromeURL(aURI); 1.1712 + return this._resolveURIToFile(aURI); 1.1713 + 1.1714 + case "resource": 1.1715 + aURI = Services.io.newURI(ResProtocolHandler.resolveURI(aURI), null, 1.1716 + null); 1.1717 + return this._resolveURIToFile(aURI); 1.1718 + 1.1719 + case "view-source": 1.1720 + aURI = Services.io.newURI(aURI.path, null, null); 1.1721 + return this._resolveURIToFile(aURI); 1.1722 + 1.1723 + case "about": 1.1724 + if (aURI.spec == "about:blank") { 1.1725 + // Do not attempt to map about:blank 1.1726 + return null; 1.1727 + } 1.1728 + 1.1729 + let chan; 1.1730 + try { 1.1731 + chan = Services.io.newChannelFromURI(aURI); 1.1732 + } 1.1733 + catch (ex) { 1.1734 + return null; 1.1735 + } 1.1736 + // Avoid looping 1.1737 + if (chan.URI.equals(aURI)) { 1.1738 + return null; 1.1739 + } 1.1740 + // We want to clone the channel URI to avoid accidentially keeping 1.1741 + // unnecessary references to the channel or implementation details 1.1742 + // around. 1.1743 + return this._resolveURIToFile(chan.URI.clone()); 1.1744 + 1.1745 + default: 1.1746 + return null; 1.1747 + } 1.1748 + }, 1.1749 + 1.1750 + /** 1.1751 + * Starts the XPI provider initializes the install locations and prefs. 1.1752 + * 1.1753 + * @param aAppChanged 1.1754 + * A tri-state value. Undefined means the current profile was created 1.1755 + * for this session, true means the profile already existed but was 1.1756 + * last used with an application with a different version number, 1.1757 + * false means that the profile was last used by this version of the 1.1758 + * application. 1.1759 + * @param aOldAppVersion 1.1760 + * The version of the application last run with this profile or null 1.1761 + * if it is a new profile or the version is unknown 1.1762 + * @param aOldPlatformVersion 1.1763 + * The version of the platform last run with this profile or null 1.1764 + * if it is a new profile or the version is unknown 1.1765 + */ 1.1766 + startup: function XPI_startup(aAppChanged, aOldAppVersion, aOldPlatformVersion) { 1.1767 + function addDirectoryInstallLocation(aName, aKey, aPaths, aScope, aLocked) { 1.1768 + try { 1.1769 + var dir = FileUtils.getDir(aKey, aPaths); 1.1770 + } 1.1771 + catch (e) { 1.1772 + // Some directories aren't defined on some platforms, ignore them 1.1773 + logger.debug("Skipping unavailable install location " + aName); 1.1774 + return; 1.1775 + } 1.1776 + 1.1777 + try { 1.1778 + var location = new DirectoryInstallLocation(aName, dir, aScope, aLocked); 1.1779 + } 1.1780 + catch (e) { 1.1781 + logger.warn("Failed to add directory install location " + aName, e); 1.1782 + return; 1.1783 + } 1.1784 + 1.1785 + XPIProvider.installLocations.push(location); 1.1786 + XPIProvider.installLocationsByName[location.name] = location; 1.1787 + } 1.1788 + 1.1789 + function addRegistryInstallLocation(aName, aRootkey, aScope) { 1.1790 + try { 1.1791 + var location = new WinRegInstallLocation(aName, aRootkey, aScope); 1.1792 + } 1.1793 + catch (e) { 1.1794 + logger.warn("Failed to add registry install location " + aName, e); 1.1795 + return; 1.1796 + } 1.1797 + 1.1798 + XPIProvider.installLocations.push(location); 1.1799 + XPIProvider.installLocationsByName[location.name] = location; 1.1800 + } 1.1801 + 1.1802 + try { 1.1803 + AddonManagerPrivate.recordTimestamp("XPI_startup_begin"); 1.1804 + 1.1805 + logger.debug("startup"); 1.1806 + this.runPhase = XPI_STARTING; 1.1807 + this.installs = []; 1.1808 + this.installLocations = []; 1.1809 + this.installLocationsByName = {}; 1.1810 + // Hook for tests to detect when saving database at shutdown time fails 1.1811 + this._shutdownError = null; 1.1812 + // Clear this at startup for xpcshell test restarts 1.1813 + this._telemetryDetails = {}; 1.1814 + // Clear the set of enabled experiments (experiments disabled by default). 1.1815 + this._enabledExperiments = new Set(); 1.1816 + // Register our details structure with AddonManager 1.1817 + AddonManagerPrivate.setTelemetryDetails("XPI", this._telemetryDetails); 1.1818 + 1.1819 + let hasRegistry = ("nsIWindowsRegKey" in Ci); 1.1820 + 1.1821 + let enabledScopes = Prefs.getIntPref(PREF_EM_ENABLED_SCOPES, 1.1822 + AddonManager.SCOPE_ALL); 1.1823 + 1.1824 + // These must be in order of priority for processFileChanges etc. to work 1.1825 + if (enabledScopes & AddonManager.SCOPE_SYSTEM) { 1.1826 + if (hasRegistry) { 1.1827 + addRegistryInstallLocation("winreg-app-global", 1.1828 + Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE, 1.1829 + AddonManager.SCOPE_SYSTEM); 1.1830 + } 1.1831 + addDirectoryInstallLocation(KEY_APP_SYSTEM_LOCAL, "XRESysLExtPD", 1.1832 + [Services.appinfo.ID], 1.1833 + AddonManager.SCOPE_SYSTEM, true); 1.1834 + addDirectoryInstallLocation(KEY_APP_SYSTEM_SHARE, "XRESysSExtPD", 1.1835 + [Services.appinfo.ID], 1.1836 + AddonManager.SCOPE_SYSTEM, true); 1.1837 + } 1.1838 + 1.1839 + if (enabledScopes & AddonManager.SCOPE_APPLICATION) { 1.1840 + addDirectoryInstallLocation(KEY_APP_GLOBAL, KEY_APPDIR, 1.1841 + [DIR_EXTENSIONS], 1.1842 + AddonManager.SCOPE_APPLICATION, true); 1.1843 + } 1.1844 + 1.1845 + if (enabledScopes & AddonManager.SCOPE_USER) { 1.1846 + if (hasRegistry) { 1.1847 + addRegistryInstallLocation("winreg-app-user", 1.1848 + Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, 1.1849 + AddonManager.SCOPE_USER); 1.1850 + } 1.1851 + addDirectoryInstallLocation(KEY_APP_SYSTEM_USER, "XREUSysExt", 1.1852 + [Services.appinfo.ID], 1.1853 + AddonManager.SCOPE_USER, true); 1.1854 + } 1.1855 + 1.1856 + // The profile location is always enabled 1.1857 + addDirectoryInstallLocation(KEY_APP_PROFILE, KEY_PROFILEDIR, 1.1858 + [DIR_EXTENSIONS], 1.1859 + AddonManager.SCOPE_PROFILE, false); 1.1860 + 1.1861 + this.defaultSkin = Prefs.getDefaultCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN, 1.1862 + "classic/1.0"); 1.1863 + this.currentSkin = Prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN, 1.1864 + this.defaultSkin); 1.1865 + this.selectedSkin = this.currentSkin; 1.1866 + this.applyThemeChange(); 1.1867 + 1.1868 + this.minCompatibleAppVersion = Prefs.getCharPref(PREF_EM_MIN_COMPAT_APP_VERSION, 1.1869 + null); 1.1870 + this.minCompatiblePlatformVersion = Prefs.getCharPref(PREF_EM_MIN_COMPAT_PLATFORM_VERSION, 1.1871 + null); 1.1872 + this.enabledAddons = ""; 1.1873 + 1.1874 + Services.prefs.addObserver(PREF_EM_MIN_COMPAT_APP_VERSION, this, false); 1.1875 + Services.prefs.addObserver(PREF_EM_MIN_COMPAT_PLATFORM_VERSION, this, false); 1.1876 + Services.obs.addObserver(this, NOTIFICATION_FLUSH_PERMISSIONS, false); 1.1877 + 1.1878 + try { 1.1879 + BrowserToolboxProcess.on("connectionchange", 1.1880 + this.onDebugConnectionChange.bind(this)); 1.1881 + } 1.1882 + catch (e) { 1.1883 + // BrowserToolboxProcess is not available in all applications 1.1884 + } 1.1885 + 1.1886 + let flushCaches = this.checkForChanges(aAppChanged, aOldAppVersion, 1.1887 + aOldPlatformVersion); 1.1888 + 1.1889 + // Changes to installed extensions may have changed which theme is selected 1.1890 + this.applyThemeChange(); 1.1891 + 1.1892 + // If the application has been upgraded and there are add-ons outside the 1.1893 + // application directory then we may need to synchronize compatibility 1.1894 + // information but only if the mismatch UI isn't disabled 1.1895 + if (aAppChanged && !this.allAppGlobal && 1.1896 + Prefs.getBoolPref(PREF_EM_SHOW_MISMATCH_UI, true)) { 1.1897 + this.showUpgradeUI(); 1.1898 + flushCaches = true; 1.1899 + } 1.1900 + else if (aAppChanged === undefined) { 1.1901 + // For new profiles we will never need to show the add-on selection UI 1.1902 + Services.prefs.setBoolPref(PREF_SHOWN_SELECTION_UI, true); 1.1903 + } 1.1904 + 1.1905 + if (flushCaches) { 1.1906 + flushStartupCache(); 1.1907 + 1.1908 + // UI displayed early in startup (like the compatibility UI) may have 1.1909 + // caused us to cache parts of the skin or locale in memory. These must 1.1910 + // be flushed to allow extension provided skins and locales to take full 1.1911 + // effect 1.1912 + Services.obs.notifyObservers(null, "chrome-flush-skin-caches", null); 1.1913 + Services.obs.notifyObservers(null, "chrome-flush-caches", null); 1.1914 + } 1.1915 + 1.1916 + this.enabledAddons = Prefs.getCharPref(PREF_EM_ENABLED_ADDONS, ""); 1.1917 + 1.1918 + // Invalidate the URI mappings now that |enabledAddons| was updated. 1.1919 + // |_ensureMappings()| will re-create the mappings when needed. 1.1920 + delete this._uriMappings; 1.1921 + 1.1922 + if ("nsICrashReporter" in Ci && 1.1923 + Services.appinfo instanceof Ci.nsICrashReporter) { 1.1924 + // Annotate the crash report with relevant add-on information. 1.1925 + try { 1.1926 + Services.appinfo.annotateCrashReport("Theme", this.currentSkin); 1.1927 + } catch (e) { } 1.1928 + try { 1.1929 + Services.appinfo.annotateCrashReport("EMCheckCompatibility", 1.1930 + AddonManager.checkCompatibility); 1.1931 + } catch (e) { } 1.1932 + this.addAddonsToCrashReporter(); 1.1933 + } 1.1934 + 1.1935 + try { 1.1936 + AddonManagerPrivate.recordTimestamp("XPI_bootstrap_addons_begin"); 1.1937 + for (let id in this.bootstrappedAddons) { 1.1938 + try { 1.1939 + let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); 1.1940 + file.persistentDescriptor = this.bootstrappedAddons[id].descriptor; 1.1941 + let reason = BOOTSTRAP_REASONS.APP_STARTUP; 1.1942 + // Eventually set INSTALLED reason when a bootstrap addon 1.1943 + // is dropped in profile folder and automatically installed 1.1944 + if (AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_INSTALLED) 1.1945 + .indexOf(id) !== -1) 1.1946 + reason = BOOTSTRAP_REASONS.ADDON_INSTALL; 1.1947 + this.callBootstrapMethod(id, this.bootstrappedAddons[id].version, 1.1948 + this.bootstrappedAddons[id].type, file, 1.1949 + "startup", reason); 1.1950 + } 1.1951 + catch (e) { 1.1952 + logger.error("Failed to load bootstrap addon " + id + " from " + 1.1953 + this.bootstrappedAddons[id].descriptor, e); 1.1954 + } 1.1955 + } 1.1956 + AddonManagerPrivate.recordTimestamp("XPI_bootstrap_addons_end"); 1.1957 + } 1.1958 + catch (e) { 1.1959 + logger.error("bootstrap startup failed", e); 1.1960 + AddonManagerPrivate.recordException("XPI-BOOTSTRAP", "startup failed", e); 1.1961 + } 1.1962 + 1.1963 + // Let these shutdown a little earlier when they still have access to most 1.1964 + // of XPCOM 1.1965 + Services.obs.addObserver({ 1.1966 + observe: function shutdownObserver(aSubject, aTopic, aData) { 1.1967 + for (let id in XPIProvider.bootstrappedAddons) { 1.1968 + let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); 1.1969 + file.persistentDescriptor = XPIProvider.bootstrappedAddons[id].descriptor; 1.1970 + XPIProvider.callBootstrapMethod(id, XPIProvider.bootstrappedAddons[id].version, 1.1971 + XPIProvider.bootstrappedAddons[id].type, file, "shutdown", 1.1972 + BOOTSTRAP_REASONS.APP_SHUTDOWN); 1.1973 + } 1.1974 + Services.obs.removeObserver(this, "quit-application-granted"); 1.1975 + } 1.1976 + }, "quit-application-granted", false); 1.1977 + 1.1978 + // Detect final-ui-startup for telemetry reporting 1.1979 + Services.obs.addObserver({ 1.1980 + observe: function uiStartupObserver(aSubject, aTopic, aData) { 1.1981 + AddonManagerPrivate.recordTimestamp("XPI_finalUIStartup"); 1.1982 + XPIProvider.runPhase = XPI_AFTER_UI_STARTUP; 1.1983 + Services.obs.removeObserver(this, "final-ui-startup"); 1.1984 + } 1.1985 + }, "final-ui-startup", false); 1.1986 + 1.1987 + AddonManagerPrivate.recordTimestamp("XPI_startup_end"); 1.1988 + 1.1989 + this.extensionsActive = true; 1.1990 + this.runPhase = XPI_BEFORE_UI_STARTUP; 1.1991 + } 1.1992 + catch (e) { 1.1993 + logger.error("startup failed", e); 1.1994 + AddonManagerPrivate.recordException("XPI", "startup failed", e); 1.1995 + } 1.1996 + }, 1.1997 + 1.1998 + /** 1.1999 + * Shuts down the database and releases all references. 1.2000 + * Return: Promise{integer} resolves / rejects with the result of 1.2001 + * flushing the XPI Database if it was loaded, 1.2002 + * 0 otherwise. 1.2003 + */ 1.2004 + shutdown: function XPI_shutdown() { 1.2005 + logger.debug("shutdown"); 1.2006 + 1.2007 + // Stop anything we were doing asynchronously 1.2008 + this.cancelAll(); 1.2009 + 1.2010 + this.bootstrappedAddons = {}; 1.2011 + this.bootstrapScopes = {}; 1.2012 + this.enabledAddons = null; 1.2013 + this.allAppGlobal = true; 1.2014 + 1.2015 + this.inactiveAddonIDs = []; 1.2016 + 1.2017 + // If there are pending operations then we must update the list of active 1.2018 + // add-ons 1.2019 + if (Prefs.getBoolPref(PREF_PENDING_OPERATIONS, false)) { 1.2020 + XPIDatabase.updateActiveAddons(); 1.2021 + Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, 1.2022 + !XPIDatabase.writeAddonsList()); 1.2023 + } 1.2024 + 1.2025 + this.installs = null; 1.2026 + this.installLocations = null; 1.2027 + this.installLocationsByName = null; 1.2028 + 1.2029 + // This is needed to allow xpcshell tests to simulate a restart 1.2030 + this.extensionsActive = false; 1.2031 + 1.2032 + // Remove URI mappings again 1.2033 + delete this._uriMappings; 1.2034 + 1.2035 + if (gLazyObjectsLoaded) { 1.2036 + let done = XPIDatabase.shutdown(); 1.2037 + done.then( 1.2038 + ret => { 1.2039 + logger.debug("Notifying XPI shutdown observers"); 1.2040 + Services.obs.notifyObservers(null, "xpi-provider-shutdown", null); 1.2041 + }, 1.2042 + err => { 1.2043 + logger.debug("Notifying XPI shutdown observers"); 1.2044 + this._shutdownError = err; 1.2045 + Services.obs.notifyObservers(null, "xpi-provider-shutdown", err); 1.2046 + } 1.2047 + ); 1.2048 + return done; 1.2049 + } 1.2050 + else { 1.2051 + logger.debug("Notifying XPI shutdown observers"); 1.2052 + Services.obs.notifyObservers(null, "xpi-provider-shutdown", null); 1.2053 + } 1.2054 + }, 1.2055 + 1.2056 + /** 1.2057 + * Applies any pending theme change to the preferences. 1.2058 + */ 1.2059 + applyThemeChange: function XPI_applyThemeChange() { 1.2060 + if (!Prefs.getBoolPref(PREF_DSS_SWITCHPENDING, false)) 1.2061 + return; 1.2062 + 1.2063 + // Tell the Chrome Registry which Skin to select 1.2064 + try { 1.2065 + this.selectedSkin = Prefs.getCharPref(PREF_DSS_SKIN_TO_SELECT); 1.2066 + Services.prefs.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN, 1.2067 + this.selectedSkin); 1.2068 + Services.prefs.clearUserPref(PREF_DSS_SKIN_TO_SELECT); 1.2069 + logger.debug("Changed skin to " + this.selectedSkin); 1.2070 + this.currentSkin = this.selectedSkin; 1.2071 + } 1.2072 + catch (e) { 1.2073 + logger.error("Error applying theme change", e); 1.2074 + } 1.2075 + Services.prefs.clearUserPref(PREF_DSS_SWITCHPENDING); 1.2076 + }, 1.2077 + 1.2078 + /** 1.2079 + * Shows the "Compatibility Updates" UI 1.2080 + */ 1.2081 + showUpgradeUI: function XPI_showUpgradeUI() { 1.2082 + // Flip a flag to indicate that we interrupted startup with an interactive prompt 1.2083 + Services.startup.interrupted = true; 1.2084 + 1.2085 + if (!Prefs.getBoolPref(PREF_SHOWN_SELECTION_UI, false)) { 1.2086 + // This *must* be modal as it has to block startup. 1.2087 + var features = "chrome,centerscreen,dialog,titlebar,modal"; 1.2088 + Services.ww.openWindow(null, URI_EXTENSION_SELECT_DIALOG, "", features, null); 1.2089 + Services.prefs.setBoolPref(PREF_SHOWN_SELECTION_UI, true); 1.2090 + } 1.2091 + else { 1.2092 + var variant = Cc["@mozilla.org/variant;1"]. 1.2093 + createInstance(Ci.nsIWritableVariant); 1.2094 + variant.setFromVariant(this.inactiveAddonIDs); 1.2095 + 1.2096 + // This *must* be modal as it has to block startup. 1.2097 + var features = "chrome,centerscreen,dialog,titlebar,modal"; 1.2098 + var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"]. 1.2099 + getService(Ci.nsIWindowWatcher); 1.2100 + ww.openWindow(null, URI_EXTENSION_UPDATE_DIALOG, "", features, variant); 1.2101 + } 1.2102 + 1.2103 + // Ensure any changes to the add-ons list are flushed to disk 1.2104 + Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, 1.2105 + !XPIDatabase.writeAddonsList()); 1.2106 + }, 1.2107 + 1.2108 + /** 1.2109 + * Persists changes to XPIProvider.bootstrappedAddons to its store (a pref). 1.2110 + */ 1.2111 + persistBootstrappedAddons: function XPI_persistBootstrappedAddons() { 1.2112 + // Experiments are disabled upon app load, so don't persist references. 1.2113 + let filtered = {}; 1.2114 + for (let id in this.bootstrappedAddons) { 1.2115 + let entry = this.bootstrappedAddons[id]; 1.2116 + if (entry.type == "experiment") { 1.2117 + continue; 1.2118 + } 1.2119 + 1.2120 + filtered[id] = entry; 1.2121 + } 1.2122 + 1.2123 + Services.prefs.setCharPref(PREF_BOOTSTRAP_ADDONS, 1.2124 + JSON.stringify(filtered)); 1.2125 + }, 1.2126 + 1.2127 + /** 1.2128 + * Adds a list of currently active add-ons to the next crash report. 1.2129 + */ 1.2130 + addAddonsToCrashReporter: function XPI_addAddonsToCrashReporter() { 1.2131 + if (!("nsICrashReporter" in Ci) || 1.2132 + !(Services.appinfo instanceof Ci.nsICrashReporter)) 1.2133 + return; 1.2134 + 1.2135 + // In safe mode no add-ons are loaded so we should not include them in the 1.2136 + // crash report 1.2137 + if (Services.appinfo.inSafeMode) 1.2138 + return; 1.2139 + 1.2140 + let data = this.enabledAddons; 1.2141 + for (let id in this.bootstrappedAddons) { 1.2142 + data += (data ? "," : "") + encodeURIComponent(id) + ":" + 1.2143 + encodeURIComponent(this.bootstrappedAddons[id].version); 1.2144 + } 1.2145 + 1.2146 + try { 1.2147 + Services.appinfo.annotateCrashReport("Add-ons", data); 1.2148 + } 1.2149 + catch (e) { } 1.2150 + 1.2151 + Cu.import("resource://gre/modules/TelemetryPing.jsm", {}).TelemetryPing.setAddOns(data); 1.2152 + }, 1.2153 + 1.2154 + /** 1.2155 + * Gets the add-on states for an install location. 1.2156 + * This function may be expensive because of the recursiveLastModifiedTime call. 1.2157 + * 1.2158 + * @param location 1.2159 + * The install location to retrieve the add-on states for 1.2160 + * @return a dictionary mapping add-on IDs to objects with a descriptor 1.2161 + * property which contains the add-ons dir/file descriptor and an 1.2162 + * mtime property which contains the add-on's last modified time as 1.2163 + * the number of milliseconds since the epoch. 1.2164 + */ 1.2165 + getAddonStates: function XPI_getAddonStates(aLocation) { 1.2166 + let addonStates = {}; 1.2167 + for (let file of aLocation.addonLocations) { 1.2168 + let scanStarted = Date.now(); 1.2169 + let id = aLocation.getIDForLocation(file); 1.2170 + let unpacked = 0; 1.2171 + let [modFile, modTime, items] = recursiveLastModifiedTime(file); 1.2172 + addonStates[id] = { 1.2173 + descriptor: file.persistentDescriptor, 1.2174 + mtime: modTime 1.2175 + }; 1.2176 + try { 1.2177 + // get the install.rdf update time, if any 1.2178 + file.append(FILE_INSTALL_MANIFEST); 1.2179 + let rdfTime = file.lastModifiedTime; 1.2180 + addonStates[id].rdfTime = rdfTime; 1.2181 + unpacked = 1; 1.2182 + } 1.2183 + catch (e) { } 1.2184 + this._mostRecentlyModifiedFile[id] = modFile; 1.2185 + this.setTelemetry(id, "unpacked", unpacked); 1.2186 + this.setTelemetry(id, "location", aLocation.name); 1.2187 + this.setTelemetry(id, "scan_MS", Date.now() - scanStarted); 1.2188 + this.setTelemetry(id, "scan_items", items); 1.2189 + } 1.2190 + 1.2191 + return addonStates; 1.2192 + }, 1.2193 + 1.2194 + /** 1.2195 + * Gets an array of install location states which uniquely describes all 1.2196 + * installed add-ons with the add-on's InstallLocation name and last modified 1.2197 + * time. This function may be expensive because of the getAddonStates() call. 1.2198 + * 1.2199 + * @return an array of add-on states for each install location. Each state 1.2200 + * is an object with a name property holding the location's name and 1.2201 + * an addons property holding the add-on states for the location 1.2202 + */ 1.2203 + getInstallLocationStates: function XPI_getInstallLocationStates() { 1.2204 + let states = []; 1.2205 + this.installLocations.forEach(function(aLocation) { 1.2206 + let addons = aLocation.addonLocations; 1.2207 + if (addons.length == 0) 1.2208 + return; 1.2209 + 1.2210 + let locationState = { 1.2211 + name: aLocation.name, 1.2212 + addons: this.getAddonStates(aLocation) 1.2213 + }; 1.2214 + 1.2215 + states.push(locationState); 1.2216 + }, this); 1.2217 + return states; 1.2218 + }, 1.2219 + 1.2220 + /** 1.2221 + * Check the staging directories of install locations for any add-ons to be 1.2222 + * installed or add-ons to be uninstalled. 1.2223 + * 1.2224 + * @param aManifests 1.2225 + * A dictionary to add detected install manifests to for the purpose 1.2226 + * of passing through updated compatibility information 1.2227 + * @return true if an add-on was installed or uninstalled 1.2228 + */ 1.2229 + processPendingFileChanges: function XPI_processPendingFileChanges(aManifests) { 1.2230 + let changed = false; 1.2231 + this.installLocations.forEach(function(aLocation) { 1.2232 + aManifests[aLocation.name] = {}; 1.2233 + // We can't install or uninstall anything in locked locations 1.2234 + if (aLocation.locked) 1.2235 + return; 1.2236 + 1.2237 + let stagedXPIDir = aLocation.getXPIStagingDir(); 1.2238 + let stagingDir = aLocation.getStagingDir(); 1.2239 + 1.2240 + if (stagedXPIDir.exists() && stagedXPIDir.isDirectory()) { 1.2241 + let entries = stagedXPIDir.directoryEntries 1.2242 + .QueryInterface(Ci.nsIDirectoryEnumerator); 1.2243 + while (entries.hasMoreElements()) { 1.2244 + let stageDirEntry = entries.nextFile; 1.2245 + 1.2246 + if (!stageDirEntry.isDirectory()) { 1.2247 + logger.warn("Ignoring file in XPI staging directory: " + stageDirEntry.path); 1.2248 + continue; 1.2249 + } 1.2250 + 1.2251 + // Find the last added XPI file in the directory 1.2252 + let stagedXPI = null; 1.2253 + var xpiEntries = stageDirEntry.directoryEntries 1.2254 + .QueryInterface(Ci.nsIDirectoryEnumerator); 1.2255 + while (xpiEntries.hasMoreElements()) { 1.2256 + let file = xpiEntries.nextFile; 1.2257 + if (file.isDirectory()) 1.2258 + continue; 1.2259 + 1.2260 + let extension = file.leafName; 1.2261 + extension = extension.substring(extension.length - 4); 1.2262 + 1.2263 + if (extension != ".xpi" && extension != ".jar") 1.2264 + continue; 1.2265 + 1.2266 + stagedXPI = file; 1.2267 + } 1.2268 + xpiEntries.close(); 1.2269 + 1.2270 + if (!stagedXPI) 1.2271 + continue; 1.2272 + 1.2273 + let addon = null; 1.2274 + try { 1.2275 + addon = loadManifestFromZipFile(stagedXPI); 1.2276 + } 1.2277 + catch (e) { 1.2278 + logger.error("Unable to read add-on manifest from " + stagedXPI.path, e); 1.2279 + continue; 1.2280 + } 1.2281 + 1.2282 + logger.debug("Migrating staged install of " + addon.id + " in " + aLocation.name); 1.2283 + 1.2284 + if (addon.unpack || Prefs.getBoolPref(PREF_XPI_UNPACK, false)) { 1.2285 + let targetDir = stagingDir.clone(); 1.2286 + targetDir.append(addon.id); 1.2287 + try { 1.2288 + targetDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); 1.2289 + } 1.2290 + catch (e) { 1.2291 + logger.error("Failed to create staging directory for add-on " + addon.id, e); 1.2292 + continue; 1.2293 + } 1.2294 + 1.2295 + try { 1.2296 + ZipUtils.extractFiles(stagedXPI, targetDir); 1.2297 + } 1.2298 + catch (e) { 1.2299 + logger.error("Failed to extract staged XPI for add-on " + addon.id + " in " + 1.2300 + aLocation.name, e); 1.2301 + } 1.2302 + } 1.2303 + else { 1.2304 + try { 1.2305 + stagedXPI.moveTo(stagingDir, addon.id + ".xpi"); 1.2306 + } 1.2307 + catch (e) { 1.2308 + logger.error("Failed to move staged XPI for add-on " + addon.id + " in " + 1.2309 + aLocation.name, e); 1.2310 + } 1.2311 + } 1.2312 + } 1.2313 + entries.close(); 1.2314 + } 1.2315 + 1.2316 + if (stagedXPIDir.exists()) { 1.2317 + try { 1.2318 + recursiveRemove(stagedXPIDir); 1.2319 + } 1.2320 + catch (e) { 1.2321 + // Non-critical, just saves some perf on startup if we clean this up. 1.2322 + logger.debug("Error removing XPI staging dir " + stagedXPIDir.path, e); 1.2323 + } 1.2324 + } 1.2325 + 1.2326 + try { 1.2327 + if (!stagingDir || !stagingDir.exists() || !stagingDir.isDirectory()) 1.2328 + return; 1.2329 + } 1.2330 + catch (e) { 1.2331 + logger.warn("Failed to find staging directory", e); 1.2332 + return; 1.2333 + } 1.2334 + 1.2335 + let seenFiles = []; 1.2336 + // Use a snapshot of the directory contents to avoid possible issues with 1.2337 + // iterating over a directory while removing files from it (the YAFFS2 1.2338 + // embedded filesystem has this issue, see bug 772238), and to remove 1.2339 + // normal files before their resource forks on OSX (see bug 733436). 1.2340 + let stagingDirEntries = getDirectoryEntries(stagingDir, true); 1.2341 + for (let stageDirEntry of stagingDirEntries) { 1.2342 + let id = stageDirEntry.leafName; 1.2343 + 1.2344 + let isDir; 1.2345 + try { 1.2346 + isDir = stageDirEntry.isDirectory(); 1.2347 + } 1.2348 + catch (e if e.result == Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) { 1.2349 + // If the file has already gone away then don't worry about it, this 1.2350 + // can happen on OSX where the resource fork is automatically moved 1.2351 + // with the data fork for the file. See bug 733436. 1.2352 + continue; 1.2353 + } 1.2354 + 1.2355 + if (!isDir) { 1.2356 + if (id.substring(id.length - 4).toLowerCase() == ".xpi") { 1.2357 + id = id.substring(0, id.length - 4); 1.2358 + } 1.2359 + else { 1.2360 + if (id.substring(id.length - 5).toLowerCase() != ".json") { 1.2361 + logger.warn("Ignoring file: " + stageDirEntry.path); 1.2362 + seenFiles.push(stageDirEntry.leafName); 1.2363 + } 1.2364 + continue; 1.2365 + } 1.2366 + } 1.2367 + 1.2368 + // Check that the directory's name is a valid ID. 1.2369 + if (!gIDTest.test(id)) { 1.2370 + logger.warn("Ignoring directory whose name is not a valid add-on ID: " + 1.2371 + stageDirEntry.path); 1.2372 + seenFiles.push(stageDirEntry.leafName); 1.2373 + continue; 1.2374 + } 1.2375 + 1.2376 + changed = true; 1.2377 + 1.2378 + if (isDir) { 1.2379 + // Check if the directory contains an install manifest. 1.2380 + let manifest = stageDirEntry.clone(); 1.2381 + manifest.append(FILE_INSTALL_MANIFEST); 1.2382 + 1.2383 + // If the install manifest doesn't exist uninstall this add-on in this 1.2384 + // install location. 1.2385 + if (!manifest.exists()) { 1.2386 + logger.debug("Processing uninstall of " + id + " in " + aLocation.name); 1.2387 + try { 1.2388 + aLocation.uninstallAddon(id); 1.2389 + seenFiles.push(stageDirEntry.leafName); 1.2390 + } 1.2391 + catch (e) { 1.2392 + logger.error("Failed to uninstall add-on " + id + " in " + aLocation.name, e); 1.2393 + } 1.2394 + // The file check later will spot the removal and cleanup the database 1.2395 + continue; 1.2396 + } 1.2397 + } 1.2398 + 1.2399 + aManifests[aLocation.name][id] = null; 1.2400 + let existingAddonID = id; 1.2401 + 1.2402 + let jsonfile = stagingDir.clone(); 1.2403 + jsonfile.append(id + ".json"); 1.2404 + 1.2405 + try { 1.2406 + aManifests[aLocation.name][id] = loadManifestFromFile(stageDirEntry); 1.2407 + } 1.2408 + catch (e) { 1.2409 + logger.error("Unable to read add-on manifest from " + stageDirEntry.path, e); 1.2410 + // This add-on can't be installed so just remove it now 1.2411 + seenFiles.push(stageDirEntry.leafName); 1.2412 + seenFiles.push(jsonfile.leafName); 1.2413 + continue; 1.2414 + } 1.2415 + 1.2416 + // Check for a cached metadata for this add-on, it may contain updated 1.2417 + // compatibility information 1.2418 + if (jsonfile.exists()) { 1.2419 + logger.debug("Found updated metadata for " + id + " in " + aLocation.name); 1.2420 + let fis = Cc["@mozilla.org/network/file-input-stream;1"]. 1.2421 + createInstance(Ci.nsIFileInputStream); 1.2422 + let json = Cc["@mozilla.org/dom/json;1"]. 1.2423 + createInstance(Ci.nsIJSON); 1.2424 + 1.2425 + try { 1.2426 + fis.init(jsonfile, -1, 0, 0); 1.2427 + let metadata = json.decodeFromStream(fis, jsonfile.fileSize); 1.2428 + aManifests[aLocation.name][id].importMetadata(metadata); 1.2429 + } 1.2430 + catch (e) { 1.2431 + // If some data can't be recovered from the cached metadata then it 1.2432 + // is unlikely to be a problem big enough to justify throwing away 1.2433 + // the install, just log and error and continue 1.2434 + logger.error("Unable to read metadata from " + jsonfile.path, e); 1.2435 + } 1.2436 + finally { 1.2437 + fis.close(); 1.2438 + } 1.2439 + } 1.2440 + seenFiles.push(jsonfile.leafName); 1.2441 + 1.2442 + existingAddonID = aManifests[aLocation.name][id].existingAddonID || id; 1.2443 + 1.2444 + var oldBootstrap = null; 1.2445 + logger.debug("Processing install of " + id + " in " + aLocation.name); 1.2446 + if (existingAddonID in this.bootstrappedAddons) { 1.2447 + try { 1.2448 + var existingAddon = aLocation.getLocationForID(existingAddonID); 1.2449 + if (this.bootstrappedAddons[existingAddonID].descriptor == 1.2450 + existingAddon.persistentDescriptor) { 1.2451 + oldBootstrap = this.bootstrappedAddons[existingAddonID]; 1.2452 + 1.2453 + // We'll be replacing a currently active bootstrapped add-on so 1.2454 + // call its uninstall method 1.2455 + let newVersion = aManifests[aLocation.name][id].version; 1.2456 + let oldVersion = oldBootstrap.version; 1.2457 + let uninstallReason = Services.vc.compare(oldVersion, newVersion) < 0 ? 1.2458 + BOOTSTRAP_REASONS.ADDON_UPGRADE : 1.2459 + BOOTSTRAP_REASONS.ADDON_DOWNGRADE; 1.2460 + 1.2461 + this.callBootstrapMethod(existingAddonID, oldBootstrap.version, 1.2462 + oldBootstrap.type, existingAddon, "uninstall", uninstallReason, 1.2463 + { newVersion: newVersion }); 1.2464 + this.unloadBootstrapScope(existingAddonID); 1.2465 + flushStartupCache(); 1.2466 + } 1.2467 + } 1.2468 + catch (e) { 1.2469 + } 1.2470 + } 1.2471 + 1.2472 + try { 1.2473 + var addonInstallLocation = aLocation.installAddon(id, stageDirEntry, 1.2474 + existingAddonID); 1.2475 + if (aManifests[aLocation.name][id]) 1.2476 + aManifests[aLocation.name][id]._sourceBundle = addonInstallLocation; 1.2477 + } 1.2478 + catch (e) { 1.2479 + logger.error("Failed to install staged add-on " + id + " in " + aLocation.name, 1.2480 + e); 1.2481 + // Re-create the staged install 1.2482 + AddonInstall.createStagedInstall(aLocation, stageDirEntry, 1.2483 + aManifests[aLocation.name][id]); 1.2484 + // Make sure not to delete the cached manifest json file 1.2485 + seenFiles.pop(); 1.2486 + 1.2487 + delete aManifests[aLocation.name][id]; 1.2488 + 1.2489 + if (oldBootstrap) { 1.2490 + // Re-install the old add-on 1.2491 + this.callBootstrapMethod(existingAddonID, oldBootstrap.version, 1.2492 + oldBootstrap.type, existingAddon, "install", 1.2493 + BOOTSTRAP_REASONS.ADDON_INSTALL); 1.2494 + } 1.2495 + continue; 1.2496 + } 1.2497 + } 1.2498 + 1.2499 + try { 1.2500 + aLocation.cleanStagingDir(seenFiles); 1.2501 + } 1.2502 + catch (e) { 1.2503 + // Non-critical, just saves some perf on startup if we clean this up. 1.2504 + logger.debug("Error cleaning staging dir " + stagingDir.path, e); 1.2505 + } 1.2506 + }, this); 1.2507 + return changed; 1.2508 + }, 1.2509 + 1.2510 + /** 1.2511 + * Installs any add-ons located in the extensions directory of the 1.2512 + * application's distribution specific directory into the profile unless a 1.2513 + * newer version already exists or the user has previously uninstalled the 1.2514 + * distributed add-on. 1.2515 + * 1.2516 + * @param aManifests 1.2517 + * A dictionary to add new install manifests to to save having to 1.2518 + * reload them later 1.2519 + * @return true if any new add-ons were installed 1.2520 + */ 1.2521 + installDistributionAddons: function XPI_installDistributionAddons(aManifests) { 1.2522 + let distroDir; 1.2523 + try { 1.2524 + distroDir = FileUtils.getDir(KEY_APP_DISTRIBUTION, [DIR_EXTENSIONS]); 1.2525 + } 1.2526 + catch (e) { 1.2527 + return false; 1.2528 + } 1.2529 + 1.2530 + if (!distroDir.exists()) 1.2531 + return false; 1.2532 + 1.2533 + if (!distroDir.isDirectory()) 1.2534 + return false; 1.2535 + 1.2536 + let changed = false; 1.2537 + let profileLocation = this.installLocationsByName[KEY_APP_PROFILE]; 1.2538 + 1.2539 + let entries = distroDir.directoryEntries 1.2540 + .QueryInterface(Ci.nsIDirectoryEnumerator); 1.2541 + let entry; 1.2542 + while ((entry = entries.nextFile)) { 1.2543 + 1.2544 + let id = entry.leafName; 1.2545 + 1.2546 + if (entry.isFile()) { 1.2547 + if (id.substring(id.length - 4).toLowerCase() == ".xpi") { 1.2548 + id = id.substring(0, id.length - 4); 1.2549 + } 1.2550 + else { 1.2551 + logger.debug("Ignoring distribution add-on that isn't an XPI: " + entry.path); 1.2552 + continue; 1.2553 + } 1.2554 + } 1.2555 + else if (!entry.isDirectory()) { 1.2556 + logger.debug("Ignoring distribution add-on that isn't a file or directory: " + 1.2557 + entry.path); 1.2558 + continue; 1.2559 + } 1.2560 + 1.2561 + if (!gIDTest.test(id)) { 1.2562 + logger.debug("Ignoring distribution add-on whose name is not a valid add-on ID: " + 1.2563 + entry.path); 1.2564 + continue; 1.2565 + } 1.2566 + 1.2567 + let addon; 1.2568 + try { 1.2569 + addon = loadManifestFromFile(entry); 1.2570 + } 1.2571 + catch (e) { 1.2572 + logger.warn("File entry " + entry.path + " contains an invalid add-on", e); 1.2573 + continue; 1.2574 + } 1.2575 + 1.2576 + if (addon.id != id) { 1.2577 + logger.warn("File entry " + entry.path + " contains an add-on with an " + 1.2578 + "incorrect ID") 1.2579 + continue; 1.2580 + } 1.2581 + 1.2582 + let existingEntry = null; 1.2583 + try { 1.2584 + existingEntry = profileLocation.getLocationForID(id); 1.2585 + } 1.2586 + catch (e) { 1.2587 + } 1.2588 + 1.2589 + if (existingEntry) { 1.2590 + let existingAddon; 1.2591 + try { 1.2592 + existingAddon = loadManifestFromFile(existingEntry); 1.2593 + 1.2594 + if (Services.vc.compare(addon.version, existingAddon.version) <= 0) 1.2595 + continue; 1.2596 + } 1.2597 + catch (e) { 1.2598 + // Bad add-on in the profile so just proceed and install over the top 1.2599 + logger.warn("Profile contains an add-on with a bad or missing install " + 1.2600 + "manifest at " + existingEntry.path + ", overwriting", e); 1.2601 + } 1.2602 + } 1.2603 + else if (Prefs.getBoolPref(PREF_BRANCH_INSTALLED_ADDON + id, false)) { 1.2604 + continue; 1.2605 + } 1.2606 + 1.2607 + // Install the add-on 1.2608 + try { 1.2609 + profileLocation.installAddon(id, entry, null, true); 1.2610 + logger.debug("Installed distribution add-on " + id); 1.2611 + 1.2612 + Services.prefs.setBoolPref(PREF_BRANCH_INSTALLED_ADDON + id, true) 1.2613 + 1.2614 + // aManifests may contain a copy of a newly installed add-on's manifest 1.2615 + // and we'll have overwritten that so instead cache our install manifest 1.2616 + // which will later be put into the database in processFileChanges 1.2617 + if (!(KEY_APP_PROFILE in aManifests)) 1.2618 + aManifests[KEY_APP_PROFILE] = {}; 1.2619 + aManifests[KEY_APP_PROFILE][id] = addon; 1.2620 + changed = true; 1.2621 + } 1.2622 + catch (e) { 1.2623 + logger.error("Failed to install distribution add-on " + entry.path, e); 1.2624 + } 1.2625 + } 1.2626 + 1.2627 + entries.close(); 1.2628 + 1.2629 + return changed; 1.2630 + }, 1.2631 + 1.2632 + /** 1.2633 + * Compares the add-ons that are currently installed to those that were 1.2634 + * known to be installed when the application last ran and applies any 1.2635 + * changes found to the database. Also sends "startupcache-invalidate" signal to 1.2636 + * observerservice if it detects that data may have changed. 1.2637 + * 1.2638 + * @param aState 1.2639 + * The array of current install location states 1.2640 + * @param aManifests 1.2641 + * A dictionary of cached AddonInstalls for add-ons that have been 1.2642 + * installed 1.2643 + * @param aUpdateCompatibility 1.2644 + * true to update add-ons appDisabled property when the application 1.2645 + * version has changed 1.2646 + * @param aOldAppVersion 1.2647 + * The version of the application last run with this profile or null 1.2648 + * if it is a new profile or the version is unknown 1.2649 + * @param aOldPlatformVersion 1.2650 + * The version of the platform last run with this profile or null 1.2651 + * if it is a new profile or the version is unknown 1.2652 + * @return a boolean indicating if a change requiring flushing the caches was 1.2653 + * detected 1.2654 + */ 1.2655 + processFileChanges: function XPI_processFileChanges(aState, aManifests, 1.2656 + aUpdateCompatibility, 1.2657 + aOldAppVersion, 1.2658 + aOldPlatformVersion) { 1.2659 + let visibleAddons = {}; 1.2660 + let oldBootstrappedAddons = this.bootstrappedAddons; 1.2661 + this.bootstrappedAddons = {}; 1.2662 + 1.2663 + /** 1.2664 + * Updates an add-on's metadata and determines if a restart of the 1.2665 + * application is necessary. This is called when either the add-on's 1.2666 + * install directory path or last modified time has changed. 1.2667 + * 1.2668 + * @param aInstallLocation 1.2669 + * The install location containing the add-on 1.2670 + * @param aOldAddon 1.2671 + * The AddonInternal as it appeared the last time the application 1.2672 + * ran 1.2673 + * @param aAddonState 1.2674 + * The new state of the add-on 1.2675 + * @return a boolean indicating if flushing caches is required to complete 1.2676 + * changing this add-on 1.2677 + */ 1.2678 + function updateMetadata(aInstallLocation, aOldAddon, aAddonState) { 1.2679 + logger.debug("Add-on " + aOldAddon.id + " modified in " + aInstallLocation.name); 1.2680 + 1.2681 + // Check if there is an updated install manifest for this add-on 1.2682 + let newAddon = aManifests[aInstallLocation.name][aOldAddon.id]; 1.2683 + 1.2684 + try { 1.2685 + // If not load it 1.2686 + if (!newAddon) { 1.2687 + let file = aInstallLocation.getLocationForID(aOldAddon.id); 1.2688 + newAddon = loadManifestFromFile(file); 1.2689 + applyBlocklistChanges(aOldAddon, newAddon); 1.2690 + 1.2691 + // Carry over any pendingUninstall state to add-ons modified directly 1.2692 + // in the profile. This is important when the attempt to remove the 1.2693 + // add-on in processPendingFileChanges failed and caused an mtime 1.2694 + // change to the add-ons files. 1.2695 + newAddon.pendingUninstall = aOldAddon.pendingUninstall; 1.2696 + } 1.2697 + 1.2698 + // The ID in the manifest that was loaded must match the ID of the old 1.2699 + // add-on. 1.2700 + if (newAddon.id != aOldAddon.id) 1.2701 + throw new Error("Incorrect id in install manifest"); 1.2702 + } 1.2703 + catch (e) { 1.2704 + logger.warn("Add-on is invalid", e); 1.2705 + XPIDatabase.removeAddonMetadata(aOldAddon); 1.2706 + if (!aInstallLocation.locked) 1.2707 + aInstallLocation.uninstallAddon(aOldAddon.id); 1.2708 + else 1.2709 + logger.warn("Could not uninstall invalid item from locked install location"); 1.2710 + // If this was an active add-on then we must force a restart 1.2711 + if (aOldAddon.active) 1.2712 + return true; 1.2713 + 1.2714 + return false; 1.2715 + } 1.2716 + 1.2717 + // Set the additional properties on the new AddonInternal 1.2718 + newAddon._installLocation = aInstallLocation; 1.2719 + newAddon.updateDate = aAddonState.mtime; 1.2720 + newAddon.visible = !(newAddon.id in visibleAddons); 1.2721 + 1.2722 + // Update the database 1.2723 + let newDBAddon = XPIDatabase.updateAddonMetadata(aOldAddon, newAddon, 1.2724 + aAddonState.descriptor); 1.2725 + if (newDBAddon.visible) { 1.2726 + visibleAddons[newDBAddon.id] = newDBAddon; 1.2727 + // Remember add-ons that were changed during startup 1.2728 + AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED, 1.2729 + newDBAddon.id); 1.2730 + 1.2731 + // If this was the active theme and it is now disabled then enable the 1.2732 + // default theme 1.2733 + if (aOldAddon.active && isAddonDisabled(newDBAddon)) 1.2734 + XPIProvider.enableDefaultTheme(); 1.2735 + 1.2736 + // If the new add-on is bootstrapped and active then call its install method 1.2737 + if (newDBAddon.active && newDBAddon.bootstrap) { 1.2738 + // Startup cache must be flushed before calling the bootstrap script 1.2739 + flushStartupCache(); 1.2740 + 1.2741 + let installReason = Services.vc.compare(aOldAddon.version, newDBAddon.version) < 0 ? 1.2742 + BOOTSTRAP_REASONS.ADDON_UPGRADE : 1.2743 + BOOTSTRAP_REASONS.ADDON_DOWNGRADE; 1.2744 + 1.2745 + let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); 1.2746 + file.persistentDescriptor = aAddonState.descriptor; 1.2747 + XPIProvider.callBootstrapMethod(newDBAddon.id, newDBAddon.version, 1.2748 + newDBAddon.type, file, "install", 1.2749 + installReason, { oldVersion: aOldAddon.version }); 1.2750 + return false; 1.2751 + } 1.2752 + 1.2753 + return true; 1.2754 + } 1.2755 + 1.2756 + return false; 1.2757 + } 1.2758 + 1.2759 + /** 1.2760 + * Updates an add-on's descriptor for when the add-on has moved in the 1.2761 + * filesystem but hasn't changed in any other way. 1.2762 + * 1.2763 + * @param aInstallLocation 1.2764 + * The install location containing the add-on 1.2765 + * @param aOldAddon 1.2766 + * The AddonInternal as it appeared the last time the application 1.2767 + * ran 1.2768 + * @param aAddonState 1.2769 + * The new state of the add-on 1.2770 + * @return a boolean indicating if flushing caches is required to complete 1.2771 + * changing this add-on 1.2772 + */ 1.2773 + function updateDescriptor(aInstallLocation, aOldAddon, aAddonState) { 1.2774 + logger.debug("Add-on " + aOldAddon.id + " moved to " + aAddonState.descriptor); 1.2775 + 1.2776 + aOldAddon.descriptor = aAddonState.descriptor; 1.2777 + aOldAddon.visible = !(aOldAddon.id in visibleAddons); 1.2778 + XPIDatabase.saveChanges(); 1.2779 + 1.2780 + if (aOldAddon.visible) { 1.2781 + visibleAddons[aOldAddon.id] = aOldAddon; 1.2782 + 1.2783 + if (aOldAddon.bootstrap && aOldAddon.active) { 1.2784 + let bootstrap = oldBootstrappedAddons[aOldAddon.id]; 1.2785 + bootstrap.descriptor = aAddonState.descriptor; 1.2786 + XPIProvider.bootstrappedAddons[aOldAddon.id] = bootstrap; 1.2787 + } 1.2788 + 1.2789 + return true; 1.2790 + } 1.2791 + 1.2792 + return false; 1.2793 + } 1.2794 + 1.2795 + /** 1.2796 + * Called when no change has been detected for an add-on's metadata. The 1.2797 + * add-on may have become visible due to other add-ons being removed or 1.2798 + * the add-on may need to be updated when the application version has 1.2799 + * changed. 1.2800 + * 1.2801 + * @param aInstallLocation 1.2802 + * The install location containing the add-on 1.2803 + * @param aOldAddon 1.2804 + * The AddonInternal as it appeared the last time the application 1.2805 + * ran 1.2806 + * @param aAddonState 1.2807 + * The new state of the add-on 1.2808 + * @return a boolean indicating if flushing caches is required to complete 1.2809 + * changing this add-on 1.2810 + */ 1.2811 + function updateVisibilityAndCompatibility(aInstallLocation, aOldAddon, 1.2812 + aAddonState) { 1.2813 + let changed = false; 1.2814 + 1.2815 + // This add-ons metadata has not changed but it may have become visible 1.2816 + if (!(aOldAddon.id in visibleAddons)) { 1.2817 + visibleAddons[aOldAddon.id] = aOldAddon; 1.2818 + 1.2819 + if (!aOldAddon.visible) { 1.2820 + // Remember add-ons that were changed during startup. 1.2821 + AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED, 1.2822 + aOldAddon.id); 1.2823 + XPIDatabase.makeAddonVisible(aOldAddon); 1.2824 + 1.2825 + if (aOldAddon.bootstrap) { 1.2826 + // The add-on is bootstrappable so call its install script 1.2827 + let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); 1.2828 + file.persistentDescriptor = aAddonState.descriptor; 1.2829 + XPIProvider.callBootstrapMethod(aOldAddon.id, aOldAddon.version, aOldAddon.type, file, 1.2830 + "install", 1.2831 + BOOTSTRAP_REASONS.ADDON_INSTALL); 1.2832 + 1.2833 + // If it should be active then mark it as active otherwise unload 1.2834 + // its scope 1.2835 + if (!isAddonDisabled(aOldAddon)) { 1.2836 + XPIDatabase.updateAddonActive(aOldAddon, true); 1.2837 + } 1.2838 + else { 1.2839 + XPIProvider.unloadBootstrapScope(newAddon.id); 1.2840 + } 1.2841 + } 1.2842 + else { 1.2843 + // Otherwise a restart is necessary 1.2844 + changed = true; 1.2845 + } 1.2846 + } 1.2847 + } 1.2848 + 1.2849 + // App version changed, we may need to update the appDisabled property. 1.2850 + if (aUpdateCompatibility) { 1.2851 + let wasDisabled = isAddonDisabled(aOldAddon); 1.2852 + let wasAppDisabled = aOldAddon.appDisabled; 1.2853 + let wasUserDisabled = aOldAddon.userDisabled; 1.2854 + let wasSoftDisabled = aOldAddon.softDisabled; 1.2855 + 1.2856 + // This updates the addon's JSON cached data in place 1.2857 + applyBlocklistChanges(aOldAddon, aOldAddon, aOldAppVersion, 1.2858 + aOldPlatformVersion); 1.2859 + aOldAddon.appDisabled = !isUsableAddon(aOldAddon); 1.2860 + 1.2861 + let isDisabled = isAddonDisabled(aOldAddon); 1.2862 + 1.2863 + // If either property has changed update the database. 1.2864 + if (wasAppDisabled != aOldAddon.appDisabled || 1.2865 + wasUserDisabled != aOldAddon.userDisabled || 1.2866 + wasSoftDisabled != aOldAddon.softDisabled) { 1.2867 + logger.debug("Add-on " + aOldAddon.id + " changed appDisabled state to " + 1.2868 + aOldAddon.appDisabled + ", userDisabled state to " + 1.2869 + aOldAddon.userDisabled + " and softDisabled state to " + 1.2870 + aOldAddon.softDisabled); 1.2871 + XPIDatabase.saveChanges(); 1.2872 + } 1.2873 + 1.2874 + // If this is a visible add-on and it has changed disabled state then we 1.2875 + // may need a restart or to update the bootstrap list. 1.2876 + if (aOldAddon.visible && wasDisabled != isDisabled) { 1.2877 + // Remember add-ons that became disabled or enabled by the application 1.2878 + // change 1.2879 + let change = isDisabled ? AddonManager.STARTUP_CHANGE_DISABLED 1.2880 + : AddonManager.STARTUP_CHANGE_ENABLED; 1.2881 + AddonManagerPrivate.addStartupChange(change, aOldAddon.id); 1.2882 + if (aOldAddon.bootstrap) { 1.2883 + // Update the add-ons active state 1.2884 + XPIDatabase.updateAddonActive(aOldAddon, !isDisabled); 1.2885 + } 1.2886 + else { 1.2887 + changed = true; 1.2888 + } 1.2889 + } 1.2890 + } 1.2891 + 1.2892 + if (aOldAddon.visible && aOldAddon.active && aOldAddon.bootstrap) { 1.2893 + XPIProvider.bootstrappedAddons[aOldAddon.id] = { 1.2894 + version: aOldAddon.version, 1.2895 + type: aOldAddon.type, 1.2896 + descriptor: aAddonState.descriptor 1.2897 + }; 1.2898 + } 1.2899 + 1.2900 + return changed; 1.2901 + } 1.2902 + 1.2903 + /** 1.2904 + * Called when an add-on has been removed. 1.2905 + * 1.2906 + * @param aOldAddon 1.2907 + * The AddonInternal as it appeared the last time the application 1.2908 + * ran 1.2909 + * @return a boolean indicating if flushing caches is required to complete 1.2910 + * changing this add-on 1.2911 + */ 1.2912 + function removeMetadata(aOldAddon) { 1.2913 + // This add-on has disappeared 1.2914 + logger.debug("Add-on " + aOldAddon.id + " removed from " + aOldAddon.location); 1.2915 + XPIDatabase.removeAddonMetadata(aOldAddon); 1.2916 + 1.2917 + // Remember add-ons that were uninstalled during startup 1.2918 + if (aOldAddon.visible) { 1.2919 + AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_UNINSTALLED, 1.2920 + aOldAddon.id); 1.2921 + } 1.2922 + else if (AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_INSTALLED) 1.2923 + .indexOf(aOldAddon.id) != -1) { 1.2924 + AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED, 1.2925 + aOldAddon.id); 1.2926 + } 1.2927 + 1.2928 + if (aOldAddon.active) { 1.2929 + // Enable the default theme if the previously active theme has been 1.2930 + // removed 1.2931 + if (aOldAddon.type == "theme") 1.2932 + XPIProvider.enableDefaultTheme(); 1.2933 + 1.2934 + return true; 1.2935 + } 1.2936 + 1.2937 + return false; 1.2938 + } 1.2939 + 1.2940 + /** 1.2941 + * Called to add the metadata for an add-on in one of the install locations 1.2942 + * to the database. This can be called in three different cases. Either an 1.2943 + * add-on has been dropped into the location from outside of Firefox, or 1.2944 + * an add-on has been installed through the application, or the database 1.2945 + * has been upgraded or become corrupt and add-on data has to be reloaded 1.2946 + * into it. 1.2947 + * 1.2948 + * @param aInstallLocation 1.2949 + * The install location containing the add-on 1.2950 + * @param aId 1.2951 + * The ID of the add-on 1.2952 + * @param aAddonState 1.2953 + * The new state of the add-on 1.2954 + * @param aMigrateData 1.2955 + * If during startup the database had to be upgraded this will 1.2956 + * contain data that used to be held about this add-on 1.2957 + * @return a boolean indicating if flushing caches is required to complete 1.2958 + * changing this add-on 1.2959 + */ 1.2960 + function addMetadata(aInstallLocation, aId, aAddonState, aMigrateData) { 1.2961 + logger.debug("New add-on " + aId + " installed in " + aInstallLocation.name); 1.2962 + 1.2963 + let newAddon = null; 1.2964 + let sameVersion = false; 1.2965 + // Check the updated manifests lists for the install location, If there 1.2966 + // is no manifest for the add-on ID then newAddon will be undefined 1.2967 + if (aInstallLocation.name in aManifests) 1.2968 + newAddon = aManifests[aInstallLocation.name][aId]; 1.2969 + 1.2970 + // If we had staged data for this add-on or we aren't recovering from a 1.2971 + // corrupt database and we don't have migration data for this add-on then 1.2972 + // this must be a new install. 1.2973 + let isNewInstall = (!!newAddon) || (!XPIDatabase.activeBundles && !aMigrateData); 1.2974 + 1.2975 + // If it's a new install and we haven't yet loaded the manifest then it 1.2976 + // must be something dropped directly into the install location 1.2977 + let isDetectedInstall = isNewInstall && !newAddon; 1.2978 + 1.2979 + // Load the manifest if necessary and sanity check the add-on ID 1.2980 + try { 1.2981 + if (!newAddon) { 1.2982 + // Load the manifest from the add-on. 1.2983 + let file = aInstallLocation.getLocationForID(aId); 1.2984 + newAddon = loadManifestFromFile(file); 1.2985 + } 1.2986 + // The add-on in the manifest should match the add-on ID. 1.2987 + if (newAddon.id != aId) { 1.2988 + throw new Error("Invalid addon ID: expected addon ID " + aId + 1.2989 + ", found " + newAddon.id + " in manifest"); 1.2990 + } 1.2991 + } 1.2992 + catch (e) { 1.2993 + logger.warn("Add-on is invalid", e); 1.2994 + 1.2995 + // Remove the invalid add-on from the install location if the install 1.2996 + // location isn't locked, no restart will be necessary 1.2997 + if (!aInstallLocation.locked) 1.2998 + aInstallLocation.uninstallAddon(aId); 1.2999 + else 1.3000 + logger.warn("Could not uninstall invalid item from locked install location"); 1.3001 + return false; 1.3002 + } 1.3003 + 1.3004 + // Update the AddonInternal properties. 1.3005 + newAddon._installLocation = aInstallLocation; 1.3006 + newAddon.visible = !(newAddon.id in visibleAddons); 1.3007 + newAddon.installDate = aAddonState.mtime; 1.3008 + newAddon.updateDate = aAddonState.mtime; 1.3009 + newAddon.foreignInstall = isDetectedInstall; 1.3010 + 1.3011 + if (aMigrateData) { 1.3012 + // If there is migration data then apply it. 1.3013 + logger.debug("Migrating data from old database"); 1.3014 + 1.3015 + DB_MIGRATE_METADATA.forEach(function(aProp) { 1.3016 + // A theme's disabled state is determined by the selected theme 1.3017 + // preference which is read in loadManifestFromRDF 1.3018 + if (aProp == "userDisabled" && newAddon.type == "theme") 1.3019 + return; 1.3020 + 1.3021 + if (aProp in aMigrateData) 1.3022 + newAddon[aProp] = aMigrateData[aProp]; 1.3023 + }); 1.3024 + 1.3025 + // Force all non-profile add-ons to be foreignInstalls since they can't 1.3026 + // have been installed through the API 1.3027 + newAddon.foreignInstall |= aInstallLocation.name != KEY_APP_PROFILE; 1.3028 + 1.3029 + // Some properties should only be migrated if the add-on hasn't changed. 1.3030 + // The version property isn't a perfect check for this but covers the 1.3031 + // vast majority of cases. 1.3032 + if (aMigrateData.version == newAddon.version) { 1.3033 + logger.debug("Migrating compatibility info"); 1.3034 + sameVersion = true; 1.3035 + if ("targetApplications" in aMigrateData) 1.3036 + newAddon.applyCompatibilityUpdate(aMigrateData, true); 1.3037 + } 1.3038 + 1.3039 + // Since the DB schema has changed make sure softDisabled is correct 1.3040 + applyBlocklistChanges(newAddon, newAddon, aOldAppVersion, 1.3041 + aOldPlatformVersion); 1.3042 + } 1.3043 + 1.3044 + // The default theme is never a foreign install 1.3045 + if (newAddon.type == "theme" && newAddon.internalName == XPIProvider.defaultSkin) 1.3046 + newAddon.foreignInstall = false; 1.3047 + 1.3048 + if (isDetectedInstall && newAddon.foreignInstall) { 1.3049 + // If the add-on is a foreign install and is in a scope where add-ons 1.3050 + // that were dropped in should default to disabled then disable it 1.3051 + let disablingScopes = Prefs.getIntPref(PREF_EM_AUTO_DISABLED_SCOPES, 0); 1.3052 + if (aInstallLocation.scope & disablingScopes) 1.3053 + newAddon.userDisabled = true; 1.3054 + } 1.3055 + 1.3056 + // If we have a list of what add-ons should be marked as active then use 1.3057 + // it to guess at migration data. 1.3058 + if (!isNewInstall && XPIDatabase.activeBundles) { 1.3059 + // For themes we know which is active by the current skin setting 1.3060 + if (newAddon.type == "theme") 1.3061 + newAddon.active = newAddon.internalName == XPIProvider.currentSkin; 1.3062 + else 1.3063 + newAddon.active = XPIDatabase.activeBundles.indexOf(aAddonState.descriptor) != -1; 1.3064 + 1.3065 + // If the add-on wasn't active and it isn't already disabled in some way 1.3066 + // then it was probably either softDisabled or userDisabled 1.3067 + if (!newAddon.active && newAddon.visible && !isAddonDisabled(newAddon)) { 1.3068 + // If the add-on is softblocked then assume it is softDisabled 1.3069 + if (newAddon.blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED) 1.3070 + newAddon.softDisabled = true; 1.3071 + else 1.3072 + newAddon.userDisabled = true; 1.3073 + } 1.3074 + } 1.3075 + else { 1.3076 + newAddon.active = (newAddon.visible && !isAddonDisabled(newAddon)) 1.3077 + } 1.3078 + 1.3079 + let newDBAddon = XPIDatabase.addAddonMetadata(newAddon, aAddonState.descriptor); 1.3080 + 1.3081 + if (newDBAddon.visible) { 1.3082 + // Remember add-ons that were first detected during startup. 1.3083 + if (isDetectedInstall) { 1.3084 + // If a copy from a higher priority location was removed then this 1.3085 + // add-on has changed 1.3086 + if (AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_UNINSTALLED) 1.3087 + .indexOf(newDBAddon.id) != -1) { 1.3088 + AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED, 1.3089 + newDBAddon.id); 1.3090 + } 1.3091 + else { 1.3092 + AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_INSTALLED, 1.3093 + newDBAddon.id); 1.3094 + } 1.3095 + } 1.3096 + 1.3097 + // Note if any visible add-on is not in the application install location 1.3098 + if (newDBAddon._installLocation.name != KEY_APP_GLOBAL) 1.3099 + XPIProvider.allAppGlobal = false; 1.3100 + 1.3101 + visibleAddons[newDBAddon.id] = newDBAddon; 1.3102 + 1.3103 + let installReason = BOOTSTRAP_REASONS.ADDON_INSTALL; 1.3104 + let extraParams = {}; 1.3105 + 1.3106 + // If we're hiding a bootstrapped add-on then call its uninstall method 1.3107 + if (newDBAddon.id in oldBootstrappedAddons) { 1.3108 + let oldBootstrap = oldBootstrappedAddons[newDBAddon.id]; 1.3109 + extraParams.oldVersion = oldBootstrap.version; 1.3110 + XPIProvider.bootstrappedAddons[newDBAddon.id] = oldBootstrap; 1.3111 + 1.3112 + // If the old version is the same as the new version, or we're 1.3113 + // recovering from a corrupt DB, don't call uninstall and install 1.3114 + // methods. 1.3115 + if (sameVersion || !isNewInstall) 1.3116 + return false; 1.3117 + 1.3118 + installReason = Services.vc.compare(oldBootstrap.version, newDBAddon.version) < 0 ? 1.3119 + BOOTSTRAP_REASONS.ADDON_UPGRADE : 1.3120 + BOOTSTRAP_REASONS.ADDON_DOWNGRADE; 1.3121 + 1.3122 + let oldAddonFile = Cc["@mozilla.org/file/local;1"]. 1.3123 + createInstance(Ci.nsIFile); 1.3124 + oldAddonFile.persistentDescriptor = oldBootstrap.descriptor; 1.3125 + 1.3126 + XPIProvider.callBootstrapMethod(newDBAddon.id, oldBootstrap.version, 1.3127 + oldBootstrap.type, oldAddonFile, "uninstall", 1.3128 + installReason, { newVersion: newDBAddon.version }); 1.3129 + XPIProvider.unloadBootstrapScope(newDBAddon.id); 1.3130 + 1.3131 + // If the new add-on is bootstrapped then we must flush the caches 1.3132 + // before calling the new bootstrap script 1.3133 + if (newDBAddon.bootstrap) 1.3134 + flushStartupCache(); 1.3135 + } 1.3136 + 1.3137 + if (!newDBAddon.bootstrap) 1.3138 + return true; 1.3139 + 1.3140 + // Visible bootstrapped add-ons need to have their install method called 1.3141 + let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); 1.3142 + file.persistentDescriptor = aAddonState.descriptor; 1.3143 + XPIProvider.callBootstrapMethod(newDBAddon.id, newDBAddon.version, newDBAddon.type, file, 1.3144 + "install", installReason, extraParams); 1.3145 + if (!newDBAddon.active) 1.3146 + XPIProvider.unloadBootstrapScope(newDBAddon.id); 1.3147 + } 1.3148 + 1.3149 + return false; 1.3150 + } 1.3151 + 1.3152 + let changed = false; 1.3153 + let knownLocations = XPIDatabase.getInstallLocations(); 1.3154 + 1.3155 + // The install locations are iterated in reverse order of priority so when 1.3156 + // there are multiple add-ons installed with the same ID the one that 1.3157 + // should be visible is the first one encountered. 1.3158 + for (let aSt of aState.reverse()) { 1.3159 + 1.3160 + // We can't include the install location directly in the state as it has 1.3161 + // to be cached as JSON. 1.3162 + let installLocation = this.installLocationsByName[aSt.name]; 1.3163 + let addonStates = aSt.addons; 1.3164 + 1.3165 + // Check if the database knows about any add-ons in this install location. 1.3166 + if (knownLocations.has(installLocation.name)) { 1.3167 + knownLocations.delete(installLocation.name); 1.3168 + let addons = XPIDatabase.getAddonsInLocation(installLocation.name); 1.3169 + // Iterate through the add-ons installed the last time the application 1.3170 + // ran 1.3171 + for (let aOldAddon of addons) { 1.3172 + // If a version of this add-on has been installed in an higher 1.3173 + // priority install location then count it as changed 1.3174 + if (AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_INSTALLED) 1.3175 + .indexOf(aOldAddon.id) != -1) { 1.3176 + AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED, 1.3177 + aOldAddon.id); 1.3178 + } 1.3179 + 1.3180 + // Check if the add-on is still installed 1.3181 + if (aOldAddon.id in addonStates) { 1.3182 + let addonState = addonStates[aOldAddon.id]; 1.3183 + delete addonStates[aOldAddon.id]; 1.3184 + 1.3185 + // Remember add-ons that were inactive during startup 1.3186 + if (aOldAddon.visible && !aOldAddon.active) 1.3187 + XPIProvider.inactiveAddonIDs.push(aOldAddon.id); 1.3188 + 1.3189 + // record a bit more per-addon telemetry 1.3190 + let loc = aOldAddon.defaultLocale; 1.3191 + if (loc) { 1.3192 + XPIProvider.setTelemetry(aOldAddon.id, "name", loc.name); 1.3193 + XPIProvider.setTelemetry(aOldAddon.id, "creator", loc.creator); 1.3194 + } 1.3195 + 1.3196 + // Check if the add-on has been changed outside the XPI provider 1.3197 + if (aOldAddon.updateDate != addonState.mtime) { 1.3198 + // Did time change in the wrong direction? 1.3199 + if (addonState.mtime < aOldAddon.updateDate) { 1.3200 + this.setTelemetry(aOldAddon.id, "olderFile", { 1.3201 + name: this._mostRecentlyModifiedFile[aOldAddon.id], 1.3202 + mtime: addonState.mtime, 1.3203 + oldtime: aOldAddon.updateDate 1.3204 + }); 1.3205 + } 1.3206 + // Is the add-on unpacked? 1.3207 + else if (addonState.rdfTime) { 1.3208 + // Was the addon manifest "install.rdf" modified, or some other file? 1.3209 + if (addonState.rdfTime > aOldAddon.updateDate) { 1.3210 + this.setTelemetry(aOldAddon.id, "modifiedInstallRDF", 1); 1.3211 + } 1.3212 + else { 1.3213 + this.setTelemetry(aOldAddon.id, "modifiedFile", 1.3214 + this._mostRecentlyModifiedFile[aOldAddon.id]); 1.3215 + } 1.3216 + } 1.3217 + else { 1.3218 + this.setTelemetry(aOldAddon.id, "modifiedXPI", 1); 1.3219 + } 1.3220 + } 1.3221 + 1.3222 + // The add-on has changed if the modification time has changed, or 1.3223 + // we have an updated manifest for it. Also reload the metadata for 1.3224 + // add-ons in the application directory when the application version 1.3225 + // has changed 1.3226 + if (aOldAddon.id in aManifests[installLocation.name] || 1.3227 + aOldAddon.updateDate != addonState.mtime || 1.3228 + (aUpdateCompatibility && installLocation.name == KEY_APP_GLOBAL)) { 1.3229 + changed = updateMetadata(installLocation, aOldAddon, addonState) || 1.3230 + changed; 1.3231 + } 1.3232 + else if (aOldAddon.descriptor != addonState.descriptor) { 1.3233 + changed = updateDescriptor(installLocation, aOldAddon, addonState) || 1.3234 + changed; 1.3235 + } 1.3236 + else { 1.3237 + changed = updateVisibilityAndCompatibility(installLocation, 1.3238 + aOldAddon, addonState) || 1.3239 + changed; 1.3240 + } 1.3241 + if (aOldAddon.visible && aOldAddon._installLocation.name != KEY_APP_GLOBAL) 1.3242 + XPIProvider.allAppGlobal = false; 1.3243 + } 1.3244 + else { 1.3245 + changed = removeMetadata(aOldAddon) || changed; 1.3246 + } 1.3247 + } 1.3248 + } 1.3249 + 1.3250 + // All the remaining add-ons in this install location must be new. 1.3251 + 1.3252 + // Get the migration data for this install location. 1.3253 + let locMigrateData = {}; 1.3254 + if (XPIDatabase.migrateData && installLocation.name in XPIDatabase.migrateData) 1.3255 + locMigrateData = XPIDatabase.migrateData[installLocation.name]; 1.3256 + for (let id in addonStates) { 1.3257 + changed = addMetadata(installLocation, id, addonStates[id], 1.3258 + (locMigrateData[id] || null)) || changed; 1.3259 + } 1.3260 + } 1.3261 + 1.3262 + // The remaining locations that had add-ons installed in them no longer 1.3263 + // have any add-ons installed in them, or the locations no longer exist. 1.3264 + // The metadata for the add-ons that were in them must be removed from the 1.3265 + // database. 1.3266 + for (let location of knownLocations) { 1.3267 + let addons = XPIDatabase.getAddonsInLocation(location); 1.3268 + for (let aOldAddon of addons) { 1.3269 + changed = removeMetadata(aOldAddon) || changed; 1.3270 + } 1.3271 + } 1.3272 + 1.3273 + // Cache the new install location states 1.3274 + this.installStates = this.getInstallLocationStates(); 1.3275 + let cache = JSON.stringify(this.installStates); 1.3276 + Services.prefs.setCharPref(PREF_INSTALL_CACHE, cache); 1.3277 + this.persistBootstrappedAddons(); 1.3278 + 1.3279 + // Clear out any cached migration data. 1.3280 + XPIDatabase.migrateData = null; 1.3281 + 1.3282 + return changed; 1.3283 + }, 1.3284 + 1.3285 + /** 1.3286 + * Imports the xpinstall permissions from preferences into the permissions 1.3287 + * manager for the user to change later. 1.3288 + */ 1.3289 + importPermissions: function XPI_importPermissions() { 1.3290 + PermissionsUtils.importFromPrefs(PREF_XPI_PERMISSIONS_BRANCH, 1.3291 + XPI_PERMISSION); 1.3292 + }, 1.3293 + 1.3294 + /** 1.3295 + * Checks for any changes that have occurred since the last time the 1.3296 + * application was launched. 1.3297 + * 1.3298 + * @param aAppChanged 1.3299 + * A tri-state value. Undefined means the current profile was created 1.3300 + * for this session, true means the profile already existed but was 1.3301 + * last used with an application with a different version number, 1.3302 + * false means that the profile was last used by this version of the 1.3303 + * application. 1.3304 + * @param aOldAppVersion 1.3305 + * The version of the application last run with this profile or null 1.3306 + * if it is a new profile or the version is unknown 1.3307 + * @param aOldPlatformVersion 1.3308 + * The version of the platform last run with this profile or null 1.3309 + * if it is a new profile or the version is unknown 1.3310 + * @return true if a change requiring a restart was detected 1.3311 + */ 1.3312 + checkForChanges: function XPI_checkForChanges(aAppChanged, aOldAppVersion, 1.3313 + aOldPlatformVersion) { 1.3314 + logger.debug("checkForChanges"); 1.3315 + 1.3316 + // Keep track of whether and why we need to open and update the database at 1.3317 + // startup time. 1.3318 + let updateReasons = []; 1.3319 + if (aAppChanged) { 1.3320 + updateReasons.push("appChanged"); 1.3321 + } 1.3322 + 1.3323 + // Load the list of bootstrapped add-ons first so processFileChanges can 1.3324 + // modify it 1.3325 + try { 1.3326 + this.bootstrappedAddons = JSON.parse(Prefs.getCharPref(PREF_BOOTSTRAP_ADDONS, 1.3327 + "{}")); 1.3328 + } catch (e) { 1.3329 + logger.warn("Error parsing enabled bootstrapped extensions cache", e); 1.3330 + } 1.3331 + 1.3332 + // First install any new add-ons into the locations, if there are any 1.3333 + // changes then we must update the database with the information in the 1.3334 + // install locations 1.3335 + let manifests = {}; 1.3336 + let updated = this.processPendingFileChanges(manifests); 1.3337 + if (updated) { 1.3338 + updateReasons.push("pendingFileChanges"); 1.3339 + } 1.3340 + 1.3341 + // This will be true if the previous session made changes that affect the 1.3342 + // active state of add-ons but didn't commit them properly (normally due 1.3343 + // to the application crashing) 1.3344 + let hasPendingChanges = Prefs.getBoolPref(PREF_PENDING_OPERATIONS); 1.3345 + if (hasPendingChanges) { 1.3346 + updateReasons.push("hasPendingChanges"); 1.3347 + } 1.3348 + 1.3349 + // If the application has changed then check for new distribution add-ons 1.3350 + if (aAppChanged !== false && 1.3351 + Prefs.getBoolPref(PREF_INSTALL_DISTRO_ADDONS, true)) 1.3352 + { 1.3353 + updated = this.installDistributionAddons(manifests); 1.3354 + if (updated) { 1.3355 + updateReasons.push("installDistributionAddons"); 1.3356 + } 1.3357 + } 1.3358 + 1.3359 + // Telemetry probe added around getInstallLocationStates() to check perf 1.3360 + let telemetryCaptureTime = Date.now(); 1.3361 + this.installStates = this.getInstallLocationStates(); 1.3362 + let telemetry = Services.telemetry; 1.3363 + telemetry.getHistogramById("CHECK_ADDONS_MODIFIED_MS").add(Date.now() - telemetryCaptureTime); 1.3364 + 1.3365 + // If the install directory state has changed then we must update the database 1.3366 + let cache = Prefs.getCharPref(PREF_INSTALL_CACHE, "[]"); 1.3367 + // For a little while, gather telemetry on whether the deep comparison 1.3368 + // makes a difference 1.3369 + let newState = JSON.stringify(this.installStates); 1.3370 + if (cache != newState) { 1.3371 + logger.debug("Directory state JSON differs: cache " + cache + " state " + newState); 1.3372 + if (directoryStateDiffers(this.installStates, cache)) { 1.3373 + updateReasons.push("directoryState"); 1.3374 + } 1.3375 + else { 1.3376 + AddonManagerPrivate.recordSimpleMeasure("XPIDB_startup_state_badCompare", 1); 1.3377 + } 1.3378 + } 1.3379 + 1.3380 + // If the schema appears to have changed then we should update the database 1.3381 + if (DB_SCHEMA != Prefs.getIntPref(PREF_DB_SCHEMA, 0)) { 1.3382 + // If we don't have any add-ons, just update the pref, since we don't need to 1.3383 + // write the database 1.3384 + if (this.installStates.length == 0) { 1.3385 + logger.debug("Empty XPI database, setting schema version preference to " + DB_SCHEMA); 1.3386 + Services.prefs.setIntPref(PREF_DB_SCHEMA, DB_SCHEMA); 1.3387 + } 1.3388 + else { 1.3389 + updateReasons.push("schemaChanged"); 1.3390 + } 1.3391 + } 1.3392 + 1.3393 + // If the database doesn't exist and there are add-ons installed then we 1.3394 + // must update the database however if there are no add-ons then there is 1.3395 + // no need to update the database. 1.3396 + let dbFile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_DATABASE], true); 1.3397 + if (!dbFile.exists() && this.installStates.length > 0) { 1.3398 + updateReasons.push("needNewDatabase"); 1.3399 + } 1.3400 + 1.3401 + if (updateReasons.length == 0) { 1.3402 + let bootstrapDescriptors = [this.bootstrappedAddons[b].descriptor 1.3403 + for (b in this.bootstrappedAddons)]; 1.3404 + 1.3405 + this.installStates.forEach(function(aInstallLocationState) { 1.3406 + for (let id in aInstallLocationState.addons) { 1.3407 + let pos = bootstrapDescriptors.indexOf(aInstallLocationState.addons[id].descriptor); 1.3408 + if (pos != -1) 1.3409 + bootstrapDescriptors.splice(pos, 1); 1.3410 + } 1.3411 + }); 1.3412 + 1.3413 + if (bootstrapDescriptors.length > 0) { 1.3414 + logger.warn("Bootstrap state is invalid (missing add-ons: " + bootstrapDescriptors.toSource() + ")"); 1.3415 + updateReasons.push("missingBootstrapAddon"); 1.3416 + } 1.3417 + } 1.3418 + 1.3419 + // Catch and log any errors during the main startup 1.3420 + try { 1.3421 + let extensionListChanged = false; 1.3422 + // If the database needs to be updated then open it and then update it 1.3423 + // from the filesystem 1.3424 + if (updateReasons.length > 0) { 1.3425 + AddonManagerPrivate.recordSimpleMeasure("XPIDB_startup_load_reasons", updateReasons); 1.3426 + XPIDatabase.syncLoadDB(false); 1.3427 + try { 1.3428 + extensionListChanged = this.processFileChanges(this.installStates, manifests, 1.3429 + aAppChanged, 1.3430 + aOldAppVersion, 1.3431 + aOldPlatformVersion); 1.3432 + } 1.3433 + catch (e) { 1.3434 + logger.error("Failed to process extension changes at startup", e); 1.3435 + } 1.3436 + } 1.3437 + 1.3438 + if (aAppChanged) { 1.3439 + // When upgrading the app and using a custom skin make sure it is still 1.3440 + // compatible otherwise switch back the default 1.3441 + if (this.currentSkin != this.defaultSkin) { 1.3442 + let oldSkin = XPIDatabase.getVisibleAddonForInternalName(this.currentSkin); 1.3443 + if (!oldSkin || isAddonDisabled(oldSkin)) 1.3444 + this.enableDefaultTheme(); 1.3445 + } 1.3446 + 1.3447 + // When upgrading remove the old extensions cache to force older 1.3448 + // versions to rescan the entire list of extensions 1.3449 + try { 1.3450 + let oldCache = FileUtils.getFile(KEY_PROFILEDIR, [FILE_OLD_CACHE], true); 1.3451 + if (oldCache.exists()) 1.3452 + oldCache.remove(true); 1.3453 + } 1.3454 + catch (e) { 1.3455 + logger.warn("Unable to remove old extension cache " + oldCache.path, e); 1.3456 + } 1.3457 + } 1.3458 + 1.3459 + // If the application crashed before completing any pending operations then 1.3460 + // we should perform them now. 1.3461 + if (extensionListChanged || hasPendingChanges) { 1.3462 + logger.debug("Updating database with changes to installed add-ons"); 1.3463 + XPIDatabase.updateActiveAddons(); 1.3464 + Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, 1.3465 + !XPIDatabase.writeAddonsList()); 1.3466 + Services.prefs.setCharPref(PREF_BOOTSTRAP_ADDONS, 1.3467 + JSON.stringify(this.bootstrappedAddons)); 1.3468 + return true; 1.3469 + } 1.3470 + 1.3471 + logger.debug("No changes found"); 1.3472 + } 1.3473 + catch (e) { 1.3474 + logger.error("Error during startup file checks", e); 1.3475 + } 1.3476 + 1.3477 + // Check that the add-ons list still exists 1.3478 + let addonsList = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST], 1.3479 + true); 1.3480 + if (addonsList.exists() == (this.installStates.length == 0)) { 1.3481 + logger.debug("Add-ons list is invalid, rebuilding"); 1.3482 + XPIDatabase.writeAddonsList(); 1.3483 + } 1.3484 + 1.3485 + return false; 1.3486 + }, 1.3487 + 1.3488 + /** 1.3489 + * Called to test whether this provider supports installing a particular 1.3490 + * mimetype. 1.3491 + * 1.3492 + * @param aMimetype 1.3493 + * The mimetype to check for 1.3494 + * @return true if the mimetype is application/x-xpinstall 1.3495 + */ 1.3496 + supportsMimetype: function XPI_supportsMimetype(aMimetype) { 1.3497 + return aMimetype == "application/x-xpinstall"; 1.3498 + }, 1.3499 + 1.3500 + /** 1.3501 + * Called to test whether installing XPI add-ons is enabled. 1.3502 + * 1.3503 + * @return true if installing is enabled 1.3504 + */ 1.3505 + isInstallEnabled: function XPI_isInstallEnabled() { 1.3506 + // Default to enabled if the preference does not exist 1.3507 + return Prefs.getBoolPref(PREF_XPI_ENABLED, true); 1.3508 + }, 1.3509 + 1.3510 + /** 1.3511 + * Called to test whether installing XPI add-ons by direct URL requests is 1.3512 + * whitelisted. 1.3513 + * 1.3514 + * @return true if installing by direct requests is whitelisted 1.3515 + */ 1.3516 + isDirectRequestWhitelisted: function XPI_isDirectRequestWhitelisted() { 1.3517 + // Default to whitelisted if the preference does not exist. 1.3518 + return Prefs.getBoolPref(PREF_XPI_DIRECT_WHITELISTED, true); 1.3519 + }, 1.3520 + 1.3521 + /** 1.3522 + * Called to test whether installing XPI add-ons from file referrers is 1.3523 + * whitelisted. 1.3524 + * 1.3525 + * @return true if installing from file referrers is whitelisted 1.3526 + */ 1.3527 + isFileRequestWhitelisted: function XPI_isFileRequestWhitelisted() { 1.3528 + // Default to whitelisted if the preference does not exist. 1.3529 + return Prefs.getBoolPref(PREF_XPI_FILE_WHITELISTED, true); 1.3530 + }, 1.3531 + 1.3532 + /** 1.3533 + * Called to test whether installing XPI add-ons from a URI is allowed. 1.3534 + * 1.3535 + * @param aUri 1.3536 + * The URI being installed from 1.3537 + * @return true if installing is allowed 1.3538 + */ 1.3539 + isInstallAllowed: function XPI_isInstallAllowed(aUri) { 1.3540 + if (!this.isInstallEnabled()) 1.3541 + return false; 1.3542 + 1.3543 + // Direct requests without a referrer are either whitelisted or blocked. 1.3544 + if (!aUri) 1.3545 + return this.isDirectRequestWhitelisted(); 1.3546 + 1.3547 + // Local referrers can be whitelisted. 1.3548 + if (this.isFileRequestWhitelisted() && 1.3549 + (aUri.schemeIs("chrome") || aUri.schemeIs("file"))) 1.3550 + return true; 1.3551 + 1.3552 + this.importPermissions(); 1.3553 + 1.3554 + let permission = Services.perms.testPermission(aUri, XPI_PERMISSION); 1.3555 + if (permission == Ci.nsIPermissionManager.DENY_ACTION) 1.3556 + return false; 1.3557 + 1.3558 + let requireWhitelist = Prefs.getBoolPref(PREF_XPI_WHITELIST_REQUIRED, true); 1.3559 + if (requireWhitelist && (permission != Ci.nsIPermissionManager.ALLOW_ACTION)) 1.3560 + return false; 1.3561 + 1.3562 + return true; 1.3563 + }, 1.3564 + 1.3565 + /** 1.3566 + * Called to get an AddonInstall to download and install an add-on from a URL. 1.3567 + * 1.3568 + * @param aUrl 1.3569 + * The URL to be installed 1.3570 + * @param aHash 1.3571 + * A hash for the install 1.3572 + * @param aName 1.3573 + * A name for the install 1.3574 + * @param aIcons 1.3575 + * Icon URLs for the install 1.3576 + * @param aVersion 1.3577 + * A version for the install 1.3578 + * @param aLoadGroup 1.3579 + * An nsILoadGroup to associate requests with 1.3580 + * @param aCallback 1.3581 + * A callback to pass the AddonInstall to 1.3582 + */ 1.3583 + getInstallForURL: function XPI_getInstallForURL(aUrl, aHash, aName, aIcons, 1.3584 + aVersion, aLoadGroup, aCallback) { 1.3585 + AddonInstall.createDownload(function getInstallForURL_createDownload(aInstall) { 1.3586 + aCallback(aInstall.wrapper); 1.3587 + }, aUrl, aHash, aName, aIcons, aVersion, aLoadGroup); 1.3588 + }, 1.3589 + 1.3590 + /** 1.3591 + * Called to get an AddonInstall to install an add-on from a local file. 1.3592 + * 1.3593 + * @param aFile 1.3594 + * The file to be installed 1.3595 + * @param aCallback 1.3596 + * A callback to pass the AddonInstall to 1.3597 + */ 1.3598 + getInstallForFile: function XPI_getInstallForFile(aFile, aCallback) { 1.3599 + AddonInstall.createInstall(function getInstallForFile_createInstall(aInstall) { 1.3600 + if (aInstall) 1.3601 + aCallback(aInstall.wrapper); 1.3602 + else 1.3603 + aCallback(null); 1.3604 + }, aFile); 1.3605 + }, 1.3606 + 1.3607 + /** 1.3608 + * Removes an AddonInstall from the list of active installs. 1.3609 + * 1.3610 + * @param install 1.3611 + * The AddonInstall to remove 1.3612 + */ 1.3613 + removeActiveInstall: function XPI_removeActiveInstall(aInstall) { 1.3614 + this.installs = this.installs.filter(function installFilter(i) i != aInstall); 1.3615 + }, 1.3616 + 1.3617 + /** 1.3618 + * Called to get an Addon with a particular ID. 1.3619 + * 1.3620 + * @param aId 1.3621 + * The ID of the add-on to retrieve 1.3622 + * @param aCallback 1.3623 + * A callback to pass the Addon to 1.3624 + */ 1.3625 + getAddonByID: function XPI_getAddonByID(aId, aCallback) { 1.3626 + XPIDatabase.getVisibleAddonForID (aId, function getAddonByID_getVisibleAddonForID(aAddon) { 1.3627 + aCallback(createWrapper(aAddon)); 1.3628 + }); 1.3629 + }, 1.3630 + 1.3631 + /** 1.3632 + * Called to get Addons of a particular type. 1.3633 + * 1.3634 + * @param aTypes 1.3635 + * An array of types to fetch. Can be null to get all types. 1.3636 + * @param aCallback 1.3637 + * A callback to pass an array of Addons to 1.3638 + */ 1.3639 + getAddonsByTypes: function XPI_getAddonsByTypes(aTypes, aCallback) { 1.3640 + XPIDatabase.getVisibleAddons(aTypes, function getAddonsByTypes_getVisibleAddons(aAddons) { 1.3641 + aCallback([createWrapper(a) for each (a in aAddons)]); 1.3642 + }); 1.3643 + }, 1.3644 + 1.3645 + /** 1.3646 + * Obtain an Addon having the specified Sync GUID. 1.3647 + * 1.3648 + * @param aGUID 1.3649 + * String GUID of add-on to retrieve 1.3650 + * @param aCallback 1.3651 + * A callback to pass the Addon to. Receives null if not found. 1.3652 + */ 1.3653 + getAddonBySyncGUID: function XPI_getAddonBySyncGUID(aGUID, aCallback) { 1.3654 + XPIDatabase.getAddonBySyncGUID(aGUID, function getAddonBySyncGUID_getAddonBySyncGUID(aAddon) { 1.3655 + aCallback(createWrapper(aAddon)); 1.3656 + }); 1.3657 + }, 1.3658 + 1.3659 + /** 1.3660 + * Called to get Addons that have pending operations. 1.3661 + * 1.3662 + * @param aTypes 1.3663 + * An array of types to fetch. Can be null to get all types 1.3664 + * @param aCallback 1.3665 + * A callback to pass an array of Addons to 1.3666 + */ 1.3667 + getAddonsWithOperationsByTypes: 1.3668 + function XPI_getAddonsWithOperationsByTypes(aTypes, aCallback) { 1.3669 + XPIDatabase.getVisibleAddonsWithPendingOperations(aTypes, 1.3670 + function getAddonsWithOpsByTypes_getVisibleAddonsWithPendingOps(aAddons) { 1.3671 + let results = [createWrapper(a) for each (a in aAddons)]; 1.3672 + XPIProvider.installs.forEach(function(aInstall) { 1.3673 + if (aInstall.state == AddonManager.STATE_INSTALLED && 1.3674 + !(aInstall.addon.inDatabase)) 1.3675 + results.push(createWrapper(aInstall.addon)); 1.3676 + }); 1.3677 + aCallback(results); 1.3678 + }); 1.3679 + }, 1.3680 + 1.3681 + /** 1.3682 + * Called to get the current AddonInstalls, optionally limiting to a list of 1.3683 + * types. 1.3684 + * 1.3685 + * @param aTypes 1.3686 + * An array of types or null to get all types 1.3687 + * @param aCallback 1.3688 + * A callback to pass the array of AddonInstalls to 1.3689 + */ 1.3690 + getInstallsByTypes: function XPI_getInstallsByTypes(aTypes, aCallback) { 1.3691 + let results = []; 1.3692 + this.installs.forEach(function(aInstall) { 1.3693 + if (!aTypes || aTypes.indexOf(aInstall.type) >= 0) 1.3694 + results.push(aInstall.wrapper); 1.3695 + }); 1.3696 + aCallback(results); 1.3697 + }, 1.3698 + 1.3699 + /** 1.3700 + * Synchronously map a URI to the corresponding Addon ID. 1.3701 + * 1.3702 + * Mappable URIs are limited to in-application resources belonging to the 1.3703 + * add-on, such as Javascript compartments, XUL windows, XBL bindings, etc. 1.3704 + * but do not include URIs from meta data, such as the add-on homepage. 1.3705 + * 1.3706 + * @param aURI 1.3707 + * nsIURI to map or null 1.3708 + * @return string containing the Addon ID 1.3709 + * @see AddonManager.mapURIToAddonID 1.3710 + * @see amIAddonManager.mapURIToAddonID 1.3711 + */ 1.3712 + mapURIToAddonID: function XPI_mapURIToAddonID(aURI) { 1.3713 + this._ensureURIMappings(); 1.3714 + let resolved = this._resolveURIToFile(aURI); 1.3715 + if (!resolved) { 1.3716 + return null; 1.3717 + } 1.3718 + resolved = resolved.spec; 1.3719 + for (let [id, spec] in Iterator(this._uriMappings)) { 1.3720 + if (resolved.startsWith(spec)) { 1.3721 + return id; 1.3722 + } 1.3723 + } 1.3724 + return null; 1.3725 + }, 1.3726 + 1.3727 + /** 1.3728 + * Called when a new add-on has been enabled when only one add-on of that type 1.3729 + * can be enabled. 1.3730 + * 1.3731 + * @param aId 1.3732 + * The ID of the newly enabled add-on 1.3733 + * @param aType 1.3734 + * The type of the newly enabled add-on 1.3735 + * @param aPendingRestart 1.3736 + * true if the newly enabled add-on will only become enabled after a 1.3737 + * restart 1.3738 + */ 1.3739 + addonChanged: function XPI_addonChanged(aId, aType, aPendingRestart) { 1.3740 + // We only care about themes in this provider 1.3741 + if (aType != "theme") 1.3742 + return; 1.3743 + 1.3744 + if (!aId) { 1.3745 + // Fallback to the default theme when no theme was enabled 1.3746 + this.enableDefaultTheme(); 1.3747 + return; 1.3748 + } 1.3749 + 1.3750 + // Look for the previously enabled theme and find the internalName of the 1.3751 + // currently selected theme 1.3752 + let previousTheme = null; 1.3753 + let newSkin = this.defaultSkin; 1.3754 + let addons = XPIDatabase.getAddonsByType("theme"); 1.3755 + addons.forEach(function(aTheme) { 1.3756 + if (!aTheme.visible) 1.3757 + return; 1.3758 + if (aTheme.id == aId) 1.3759 + newSkin = aTheme.internalName; 1.3760 + else if (aTheme.userDisabled == false && !aTheme.pendingUninstall) 1.3761 + previousTheme = aTheme; 1.3762 + }, this); 1.3763 + 1.3764 + if (aPendingRestart) { 1.3765 + Services.prefs.setBoolPref(PREF_DSS_SWITCHPENDING, true); 1.3766 + Services.prefs.setCharPref(PREF_DSS_SKIN_TO_SELECT, newSkin); 1.3767 + } 1.3768 + else if (newSkin == this.currentSkin) { 1.3769 + try { 1.3770 + Services.prefs.clearUserPref(PREF_DSS_SWITCHPENDING); 1.3771 + } 1.3772 + catch (e) { } 1.3773 + try { 1.3774 + Services.prefs.clearUserPref(PREF_DSS_SKIN_TO_SELECT); 1.3775 + } 1.3776 + catch (e) { } 1.3777 + } 1.3778 + else { 1.3779 + Services.prefs.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN, newSkin); 1.3780 + this.currentSkin = newSkin; 1.3781 + } 1.3782 + this.selectedSkin = newSkin; 1.3783 + 1.3784 + // Flush the preferences to disk so they don't get out of sync with the 1.3785 + // database 1.3786 + Services.prefs.savePrefFile(null); 1.3787 + 1.3788 + // Mark the previous theme as disabled. This won't cause recursion since 1.3789 + // only enabled calls notifyAddonChanged. 1.3790 + if (previousTheme) 1.3791 + this.updateAddonDisabledState(previousTheme, true); 1.3792 + }, 1.3793 + 1.3794 + /** 1.3795 + * Update the appDisabled property for all add-ons. 1.3796 + */ 1.3797 + updateAddonAppDisabledStates: function XPI_updateAddonAppDisabledStates() { 1.3798 + let addons = XPIDatabase.getAddons(); 1.3799 + addons.forEach(function(aAddon) { 1.3800 + this.updateAddonDisabledState(aAddon); 1.3801 + }, this); 1.3802 + }, 1.3803 + 1.3804 + /** 1.3805 + * Update the repositoryAddon property for all add-ons. 1.3806 + * 1.3807 + * @param aCallback 1.3808 + * Function to call when operation is complete. 1.3809 + */ 1.3810 + updateAddonRepositoryData: function XPI_updateAddonRepositoryData(aCallback) { 1.3811 + let self = this; 1.3812 + XPIDatabase.getVisibleAddons(null, function UARD_getVisibleAddonsCallback(aAddons) { 1.3813 + let pending = aAddons.length; 1.3814 + logger.debug("updateAddonRepositoryData found " + pending + " visible add-ons"); 1.3815 + if (pending == 0) { 1.3816 + aCallback(); 1.3817 + return; 1.3818 + } 1.3819 + 1.3820 + function notifyComplete() { 1.3821 + if (--pending == 0) 1.3822 + aCallback(); 1.3823 + } 1.3824 + 1.3825 + for (let addon of aAddons) { 1.3826 + AddonRepository.getCachedAddonByID(addon.id, 1.3827 + function UARD_getCachedAddonCallback(aRepoAddon) { 1.3828 + if (aRepoAddon) { 1.3829 + logger.debug("updateAddonRepositoryData got info for " + addon.id); 1.3830 + addon._repositoryAddon = aRepoAddon; 1.3831 + addon.compatibilityOverrides = aRepoAddon.compatibilityOverrides; 1.3832 + self.updateAddonDisabledState(addon); 1.3833 + } 1.3834 + 1.3835 + notifyComplete(); 1.3836 + }); 1.3837 + }; 1.3838 + }); 1.3839 + }, 1.3840 + 1.3841 + /** 1.3842 + * When the previously selected theme is removed this method will be called 1.3843 + * to enable the default theme. 1.3844 + */ 1.3845 + enableDefaultTheme: function XPI_enableDefaultTheme() { 1.3846 + logger.debug("Activating default theme"); 1.3847 + let addon = XPIDatabase.getVisibleAddonForInternalName(this.defaultSkin); 1.3848 + if (addon) { 1.3849 + if (addon.userDisabled) { 1.3850 + this.updateAddonDisabledState(addon, false); 1.3851 + } 1.3852 + else if (!this.extensionsActive) { 1.3853 + // During startup we may end up trying to enable the default theme when 1.3854 + // the database thinks it is already enabled (see f.e. bug 638847). In 1.3855 + // this case just force the theme preferences to be correct 1.3856 + Services.prefs.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN, 1.3857 + addon.internalName); 1.3858 + this.currentSkin = this.selectedSkin = addon.internalName; 1.3859 + Prefs.clearUserPref(PREF_DSS_SKIN_TO_SELECT); 1.3860 + Prefs.clearUserPref(PREF_DSS_SWITCHPENDING); 1.3861 + } 1.3862 + else { 1.3863 + logger.warn("Attempting to activate an already active default theme"); 1.3864 + } 1.3865 + } 1.3866 + else { 1.3867 + logger.warn("Unable to activate the default theme"); 1.3868 + } 1.3869 + }, 1.3870 + 1.3871 + onDebugConnectionChange: function(aEvent, aWhat, aConnection) { 1.3872 + if (aWhat != "opened") 1.3873 + return; 1.3874 + 1.3875 + for (let id of Object.keys(this.bootstrapScopes)) { 1.3876 + aConnection.setAddonOptions(id, { global: this.bootstrapScopes[id] }); 1.3877 + } 1.3878 + }, 1.3879 + 1.3880 + /** 1.3881 + * Notified when a preference we're interested in has changed. 1.3882 + * 1.3883 + * @see nsIObserver 1.3884 + */ 1.3885 + observe: function XPI_observe(aSubject, aTopic, aData) { 1.3886 + if (aTopic == NOTIFICATION_FLUSH_PERMISSIONS) { 1.3887 + if (!aData || aData == XPI_PERMISSION) { 1.3888 + this.importPermissions(); 1.3889 + } 1.3890 + return; 1.3891 + } 1.3892 + 1.3893 + if (aTopic == "nsPref:changed") { 1.3894 + switch (aData) { 1.3895 + case PREF_EM_MIN_COMPAT_APP_VERSION: 1.3896 + case PREF_EM_MIN_COMPAT_PLATFORM_VERSION: 1.3897 + this.minCompatibleAppVersion = Prefs.getCharPref(PREF_EM_MIN_COMPAT_APP_VERSION, 1.3898 + null); 1.3899 + this.minCompatiblePlatformVersion = Prefs.getCharPref(PREF_EM_MIN_COMPAT_PLATFORM_VERSION, 1.3900 + null); 1.3901 + this.updateAddonAppDisabledStates(); 1.3902 + break; 1.3903 + } 1.3904 + } 1.3905 + }, 1.3906 + 1.3907 + /** 1.3908 + * Tests whether enabling an add-on will require a restart. 1.3909 + * 1.3910 + * @param aAddon 1.3911 + * The add-on to test 1.3912 + * @return true if the operation requires a restart 1.3913 + */ 1.3914 + enableRequiresRestart: function XPI_enableRequiresRestart(aAddon) { 1.3915 + // If the platform couldn't have activated extensions then we can make 1.3916 + // changes without any restart. 1.3917 + if (!this.extensionsActive) 1.3918 + return false; 1.3919 + 1.3920 + // If the application is in safe mode then any change can be made without 1.3921 + // restarting 1.3922 + if (Services.appinfo.inSafeMode) 1.3923 + return false; 1.3924 + 1.3925 + // Anything that is active is already enabled 1.3926 + if (aAddon.active) 1.3927 + return false; 1.3928 + 1.3929 + if (aAddon.type == "theme") { 1.3930 + // If dynamic theme switching is enabled then switching themes does not 1.3931 + // require a restart 1.3932 + if (Prefs.getBoolPref(PREF_EM_DSS_ENABLED)) 1.3933 + return false; 1.3934 + 1.3935 + // If the theme is already the theme in use then no restart is necessary. 1.3936 + // This covers the case where the default theme is in use but a 1.3937 + // lightweight theme is considered active. 1.3938 + return aAddon.internalName != this.currentSkin; 1.3939 + } 1.3940 + 1.3941 + return !aAddon.bootstrap; 1.3942 + }, 1.3943 + 1.3944 + /** 1.3945 + * Tests whether disabling an add-on will require a restart. 1.3946 + * 1.3947 + * @param aAddon 1.3948 + * The add-on to test 1.3949 + * @return true if the operation requires a restart 1.3950 + */ 1.3951 + disableRequiresRestart: function XPI_disableRequiresRestart(aAddon) { 1.3952 + // If the platform couldn't have activated up extensions then we can make 1.3953 + // changes without any restart. 1.3954 + if (!this.extensionsActive) 1.3955 + return false; 1.3956 + 1.3957 + // If the application is in safe mode then any change can be made without 1.3958 + // restarting 1.3959 + if (Services.appinfo.inSafeMode) 1.3960 + return false; 1.3961 + 1.3962 + // Anything that isn't active is already disabled 1.3963 + if (!aAddon.active) 1.3964 + return false; 1.3965 + 1.3966 + if (aAddon.type == "theme") { 1.3967 + // If dynamic theme switching is enabled then switching themes does not 1.3968 + // require a restart 1.3969 + if (Prefs.getBoolPref(PREF_EM_DSS_ENABLED)) 1.3970 + return false; 1.3971 + 1.3972 + // Non-default themes always require a restart to disable since it will 1.3973 + // be switching from one theme to another or to the default theme and a 1.3974 + // lightweight theme. 1.3975 + if (aAddon.internalName != this.defaultSkin) 1.3976 + return true; 1.3977 + 1.3978 + // The default theme requires a restart to disable if we are in the 1.3979 + // process of switching to a different theme. Note that this makes the 1.3980 + // disabled flag of operationsRequiringRestart incorrect for the default 1.3981 + // theme (it will be false most of the time). Bug 520124 would be required 1.3982 + // to fix it. For the UI this isn't a problem since we never try to 1.3983 + // disable or uninstall the default theme. 1.3984 + return this.selectedSkin != this.currentSkin; 1.3985 + } 1.3986 + 1.3987 + return !aAddon.bootstrap; 1.3988 + }, 1.3989 + 1.3990 + /** 1.3991 + * Tests whether installing an add-on will require a restart. 1.3992 + * 1.3993 + * @param aAddon 1.3994 + * The add-on to test 1.3995 + * @return true if the operation requires a restart 1.3996 + */ 1.3997 + installRequiresRestart: function XPI_installRequiresRestart(aAddon) { 1.3998 + // If the platform couldn't have activated up extensions then we can make 1.3999 + // changes without any restart. 1.4000 + if (!this.extensionsActive) 1.4001 + return false; 1.4002 + 1.4003 + // If the application is in safe mode then any change can be made without 1.4004 + // restarting 1.4005 + if (Services.appinfo.inSafeMode) 1.4006 + return false; 1.4007 + 1.4008 + // Add-ons that are already installed don't require a restart to install. 1.4009 + // This wouldn't normally be called for an already installed add-on (except 1.4010 + // for forming the operationsRequiringRestart flags) so is really here as 1.4011 + // a safety measure. 1.4012 + if (aAddon.inDatabase) 1.4013 + return false; 1.4014 + 1.4015 + // If we have an AddonInstall for this add-on then we can see if there is 1.4016 + // an existing installed add-on with the same ID 1.4017 + if ("_install" in aAddon && aAddon._install) { 1.4018 + // If there is an existing installed add-on and uninstalling it would 1.4019 + // require a restart then installing the update will also require a 1.4020 + // restart 1.4021 + let existingAddon = aAddon._install.existingAddon; 1.4022 + if (existingAddon && this.uninstallRequiresRestart(existingAddon)) 1.4023 + return true; 1.4024 + } 1.4025 + 1.4026 + // If the add-on is not going to be active after installation then it 1.4027 + // doesn't require a restart to install. 1.4028 + if (isAddonDisabled(aAddon)) 1.4029 + return false; 1.4030 + 1.4031 + // Themes will require a restart (even if dynamic switching is enabled due 1.4032 + // to some caching issues) and non-bootstrapped add-ons will require a 1.4033 + // restart 1.4034 + return aAddon.type == "theme" || !aAddon.bootstrap; 1.4035 + }, 1.4036 + 1.4037 + /** 1.4038 + * Tests whether uninstalling an add-on will require a restart. 1.4039 + * 1.4040 + * @param aAddon 1.4041 + * The add-on to test 1.4042 + * @return true if the operation requires a restart 1.4043 + */ 1.4044 + uninstallRequiresRestart: function XPI_uninstallRequiresRestart(aAddon) { 1.4045 + // If the platform couldn't have activated up extensions then we can make 1.4046 + // changes without any restart. 1.4047 + if (!this.extensionsActive) 1.4048 + return false; 1.4049 + 1.4050 + // If the application is in safe mode then any change can be made without 1.4051 + // restarting 1.4052 + if (Services.appinfo.inSafeMode) 1.4053 + return false; 1.4054 + 1.4055 + // If the add-on can be disabled without a restart then it can also be 1.4056 + // uninstalled without a restart 1.4057 + return this.disableRequiresRestart(aAddon); 1.4058 + }, 1.4059 + 1.4060 + /** 1.4061 + * Loads a bootstrapped add-on's bootstrap.js into a sandbox and the reason 1.4062 + * values as constants in the scope. This will also add information about the 1.4063 + * add-on to the bootstrappedAddons dictionary and notify the crash reporter 1.4064 + * that new add-ons have been loaded. 1.4065 + * 1.4066 + * @param aId 1.4067 + * The add-on's ID 1.4068 + * @param aFile 1.4069 + * The nsIFile for the add-on 1.4070 + * @param aVersion 1.4071 + * The add-on's version 1.4072 + * @param aType 1.4073 + * The type for the add-on 1.4074 + * @return a JavaScript scope 1.4075 + */ 1.4076 + loadBootstrapScope: function XPI_loadBootstrapScope(aId, aFile, aVersion, aType) { 1.4077 + // Mark the add-on as active for the crash reporter before loading 1.4078 + this.bootstrappedAddons[aId] = { 1.4079 + version: aVersion, 1.4080 + type: aType, 1.4081 + descriptor: aFile.persistentDescriptor 1.4082 + }; 1.4083 + this.persistBootstrappedAddons(); 1.4084 + this.addAddonsToCrashReporter(); 1.4085 + 1.4086 + // Locales only contain chrome and can't have bootstrap scripts 1.4087 + if (aType == "locale") { 1.4088 + this.bootstrapScopes[aId] = null; 1.4089 + return; 1.4090 + } 1.4091 + 1.4092 + logger.debug("Loading bootstrap scope from " + aFile.path); 1.4093 + 1.4094 + let principal = Cc["@mozilla.org/systemprincipal;1"]. 1.4095 + createInstance(Ci.nsIPrincipal); 1.4096 + 1.4097 + if (!aFile.exists()) { 1.4098 + this.bootstrapScopes[aId] = 1.4099 + new Cu.Sandbox(principal, { sandboxName: aFile.path, 1.4100 + wantGlobalProperties: ["indexedDB"], 1.4101 + metadata: { addonID: aId } }); 1.4102 + logger.error("Attempted to load bootstrap scope from missing directory " + aFile.path); 1.4103 + return; 1.4104 + } 1.4105 + 1.4106 + let uri = getURIForResourceInFile(aFile, "bootstrap.js").spec; 1.4107 + if (aType == "dictionary") 1.4108 + uri = "resource://gre/modules/addons/SpellCheckDictionaryBootstrap.js" 1.4109 + 1.4110 + this.bootstrapScopes[aId] = 1.4111 + new Cu.Sandbox(principal, { sandboxName: uri, 1.4112 + wantGlobalProperties: ["indexedDB"], 1.4113 + metadata: { addonID: aId, URI: uri } }); 1.4114 + 1.4115 + let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]. 1.4116 + createInstance(Ci.mozIJSSubScriptLoader); 1.4117 + 1.4118 + // Add a mapping for XPIProvider.mapURIToAddonID 1.4119 + this._addURIMapping(aId, aFile); 1.4120 + 1.4121 + try { 1.4122 + // Copy the reason values from the global object into the bootstrap scope. 1.4123 + for (let name in BOOTSTRAP_REASONS) 1.4124 + this.bootstrapScopes[aId][name] = BOOTSTRAP_REASONS[name]; 1.4125 + 1.4126 + // Add other stuff that extensions want. 1.4127 + const features = [ "Worker", "ChromeWorker" ]; 1.4128 + 1.4129 + for (let feature of features) 1.4130 + this.bootstrapScopes[aId][feature] = gGlobalScope[feature]; 1.4131 + 1.4132 + // As we don't want our caller to control the JS version used for the 1.4133 + // bootstrap file, we run loadSubScript within the context of the 1.4134 + // sandbox with the latest JS version set explicitly. 1.4135 + this.bootstrapScopes[aId].__SCRIPT_URI_SPEC__ = uri; 1.4136 + Components.utils.evalInSandbox( 1.4137 + "Components.classes['@mozilla.org/moz/jssubscript-loader;1'] \ 1.4138 + .createInstance(Components.interfaces.mozIJSSubScriptLoader) \ 1.4139 + .loadSubScript(__SCRIPT_URI_SPEC__);", this.bootstrapScopes[aId], "ECMAv5"); 1.4140 + } 1.4141 + catch (e) { 1.4142 + logger.warn("Error loading bootstrap.js for " + aId, e); 1.4143 + } 1.4144 + 1.4145 + try { 1.4146 + BrowserToolboxProcess.setAddonOptions(aId, { global: this.bootstrapScopes[aId] }); 1.4147 + } 1.4148 + catch (e) { 1.4149 + // BrowserToolboxProcess is not available in all applications 1.4150 + } 1.4151 + }, 1.4152 + 1.4153 + /** 1.4154 + * Unloads a bootstrap scope by dropping all references to it and then 1.4155 + * updating the list of active add-ons with the crash reporter. 1.4156 + * 1.4157 + * @param aId 1.4158 + * The add-on's ID 1.4159 + */ 1.4160 + unloadBootstrapScope: function XPI_unloadBootstrapScope(aId) { 1.4161 + delete this.bootstrapScopes[aId]; 1.4162 + delete this.bootstrappedAddons[aId]; 1.4163 + this.persistBootstrappedAddons(); 1.4164 + this.addAddonsToCrashReporter(); 1.4165 + 1.4166 + try { 1.4167 + BrowserToolboxProcess.setAddonOptions(aId, { global: null }); 1.4168 + } 1.4169 + catch (e) { 1.4170 + // BrowserToolboxProcess is not available in all applications 1.4171 + } 1.4172 + }, 1.4173 + 1.4174 + /** 1.4175 + * Calls a bootstrap method for an add-on. 1.4176 + * 1.4177 + * @param aId 1.4178 + * The ID of the add-on 1.4179 + * @param aVersion 1.4180 + * The version of the add-on 1.4181 + * @param aType 1.4182 + * The type for the add-on 1.4183 + * @param aFile 1.4184 + * The nsIFile for the add-on 1.4185 + * @param aMethod 1.4186 + * The name of the bootstrap method to call 1.4187 + * @param aReason 1.4188 + * The reason flag to pass to the bootstrap's startup method 1.4189 + * @param aExtraParams 1.4190 + * An object of additional key/value pairs to pass to the method in 1.4191 + * the params argument 1.4192 + */ 1.4193 + callBootstrapMethod: function XPI_callBootstrapMethod(aId, aVersion, aType, aFile, 1.4194 + aMethod, aReason, aExtraParams) { 1.4195 + // Never call any bootstrap methods in safe mode 1.4196 + if (Services.appinfo.inSafeMode) 1.4197 + return; 1.4198 + 1.4199 + let timeStart = new Date(); 1.4200 + if (aMethod == "startup") { 1.4201 + logger.debug("Registering manifest for " + aFile.path); 1.4202 + Components.manager.addBootstrappedManifestLocation(aFile); 1.4203 + } 1.4204 + 1.4205 + try { 1.4206 + // Load the scope if it hasn't already been loaded 1.4207 + if (!(aId in this.bootstrapScopes)) 1.4208 + this.loadBootstrapScope(aId, aFile, aVersion, aType); 1.4209 + 1.4210 + // Nothing to call for locales 1.4211 + if (aType == "locale") 1.4212 + return; 1.4213 + 1.4214 + if (!(aMethod in this.bootstrapScopes[aId])) { 1.4215 + logger.warn("Add-on " + aId + " is missing bootstrap method " + aMethod); 1.4216 + return; 1.4217 + } 1.4218 + 1.4219 + let params = { 1.4220 + id: aId, 1.4221 + version: aVersion, 1.4222 + installPath: aFile.clone(), 1.4223 + resourceURI: getURIForResourceInFile(aFile, "") 1.4224 + }; 1.4225 + 1.4226 + if (aExtraParams) { 1.4227 + for (let key in aExtraParams) { 1.4228 + params[key] = aExtraParams[key]; 1.4229 + } 1.4230 + } 1.4231 + 1.4232 + logger.debug("Calling bootstrap method " + aMethod + " on " + aId + " version " + 1.4233 + aVersion); 1.4234 + try { 1.4235 + this.bootstrapScopes[aId][aMethod](params, aReason); 1.4236 + } 1.4237 + catch (e) { 1.4238 + logger.warn("Exception running bootstrap method " + aMethod + " on " + aId, e); 1.4239 + } 1.4240 + } 1.4241 + finally { 1.4242 + if (aMethod == "shutdown" && aReason != BOOTSTRAP_REASONS.APP_SHUTDOWN) { 1.4243 + logger.debug("Removing manifest for " + aFile.path); 1.4244 + Components.manager.removeBootstrappedManifestLocation(aFile); 1.4245 + } 1.4246 + this.setTelemetry(aId, aMethod + "_MS", new Date() - timeStart); 1.4247 + } 1.4248 + }, 1.4249 + 1.4250 + /** 1.4251 + * Updates the disabled state for an add-on. Its appDisabled property will be 1.4252 + * calculated and if the add-on is changed the database will be saved and 1.4253 + * appropriate notifications will be sent out to the registered AddonListeners. 1.4254 + * 1.4255 + * @param aAddon 1.4256 + * The DBAddonInternal to update 1.4257 + * @param aUserDisabled 1.4258 + * Value for the userDisabled property. If undefined the value will 1.4259 + * not change 1.4260 + * @param aSoftDisabled 1.4261 + * Value for the softDisabled property. If undefined the value will 1.4262 + * not change. If true this will force userDisabled to be true 1.4263 + * @throws if addon is not a DBAddonInternal 1.4264 + */ 1.4265 + updateAddonDisabledState: function XPI_updateAddonDisabledState(aAddon, 1.4266 + aUserDisabled, 1.4267 + aSoftDisabled) { 1.4268 + if (!(aAddon.inDatabase)) 1.4269 + throw new Error("Can only update addon states for installed addons."); 1.4270 + if (aUserDisabled !== undefined && aSoftDisabled !== undefined) { 1.4271 + throw new Error("Cannot change userDisabled and softDisabled at the " + 1.4272 + "same time"); 1.4273 + } 1.4274 + 1.4275 + if (aUserDisabled === undefined) { 1.4276 + aUserDisabled = aAddon.userDisabled; 1.4277 + } 1.4278 + else if (!aUserDisabled) { 1.4279 + // If enabling the add-on then remove softDisabled 1.4280 + aSoftDisabled = false; 1.4281 + } 1.4282 + 1.4283 + // If not changing softDisabled or the add-on is already userDisabled then 1.4284 + // use the existing value for softDisabled 1.4285 + if (aSoftDisabled === undefined || aUserDisabled) 1.4286 + aSoftDisabled = aAddon.softDisabled; 1.4287 + 1.4288 + let appDisabled = !isUsableAddon(aAddon); 1.4289 + // No change means nothing to do here 1.4290 + if (aAddon.userDisabled == aUserDisabled && 1.4291 + aAddon.appDisabled == appDisabled && 1.4292 + aAddon.softDisabled == aSoftDisabled) 1.4293 + return; 1.4294 + 1.4295 + let wasDisabled = isAddonDisabled(aAddon); 1.4296 + let isDisabled = aUserDisabled || aSoftDisabled || appDisabled; 1.4297 + 1.4298 + // If appDisabled changes but the result of isAddonDisabled() doesn't, 1.4299 + // no onDisabling/onEnabling is sent - so send a onPropertyChanged. 1.4300 + let appDisabledChanged = aAddon.appDisabled != appDisabled; 1.4301 + 1.4302 + // Update the properties in the database. 1.4303 + // We never persist this for experiments because the disabled flags 1.4304 + // are controlled by the Experiments Manager. 1.4305 + if (aAddon.type != "experiment") { 1.4306 + XPIDatabase.setAddonProperties(aAddon, { 1.4307 + userDisabled: aUserDisabled, 1.4308 + appDisabled: appDisabled, 1.4309 + softDisabled: aSoftDisabled 1.4310 + }); 1.4311 + } 1.4312 + 1.4313 + if (appDisabledChanged) { 1.4314 + AddonManagerPrivate.callAddonListeners("onPropertyChanged", 1.4315 + aAddon, 1.4316 + ["appDisabled"]); 1.4317 + } 1.4318 + 1.4319 + // If the add-on is not visible or the add-on is not changing state then 1.4320 + // there is no need to do anything else 1.4321 + if (!aAddon.visible || (wasDisabled == isDisabled)) 1.4322 + return; 1.4323 + 1.4324 + // Flag that active states in the database need to be updated on shutdown 1.4325 + Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true); 1.4326 + 1.4327 + let wrapper = createWrapper(aAddon); 1.4328 + // Have we just gone back to the current state? 1.4329 + if (isDisabled != aAddon.active) { 1.4330 + AddonManagerPrivate.callAddonListeners("onOperationCancelled", wrapper); 1.4331 + } 1.4332 + else { 1.4333 + if (isDisabled) { 1.4334 + var needsRestart = this.disableRequiresRestart(aAddon); 1.4335 + AddonManagerPrivate.callAddonListeners("onDisabling", wrapper, 1.4336 + needsRestart); 1.4337 + } 1.4338 + else { 1.4339 + needsRestart = this.enableRequiresRestart(aAddon); 1.4340 + AddonManagerPrivate.callAddonListeners("onEnabling", wrapper, 1.4341 + needsRestart); 1.4342 + } 1.4343 + 1.4344 + if (!needsRestart) { 1.4345 + XPIDatabase.updateAddonActive(aAddon, !isDisabled); 1.4346 + if (isDisabled) { 1.4347 + if (aAddon.bootstrap) { 1.4348 + let file = aAddon._installLocation.getLocationForID(aAddon.id); 1.4349 + this.callBootstrapMethod(aAddon.id, aAddon.version, aAddon.type, file, "shutdown", 1.4350 + BOOTSTRAP_REASONS.ADDON_DISABLE); 1.4351 + this.unloadBootstrapScope(aAddon.id); 1.4352 + } 1.4353 + AddonManagerPrivate.callAddonListeners("onDisabled", wrapper); 1.4354 + } 1.4355 + else { 1.4356 + if (aAddon.bootstrap) { 1.4357 + let file = aAddon._installLocation.getLocationForID(aAddon.id); 1.4358 + this.callBootstrapMethod(aAddon.id, aAddon.version, aAddon.type, file, "startup", 1.4359 + BOOTSTRAP_REASONS.ADDON_ENABLE); 1.4360 + } 1.4361 + AddonManagerPrivate.callAddonListeners("onEnabled", wrapper); 1.4362 + } 1.4363 + } 1.4364 + } 1.4365 + 1.4366 + // Notify any other providers that a new theme has been enabled 1.4367 + if (aAddon.type == "theme" && !isDisabled) 1.4368 + AddonManagerPrivate.notifyAddonChanged(aAddon.id, aAddon.type, needsRestart); 1.4369 + }, 1.4370 + 1.4371 + /** 1.4372 + * Uninstalls an add-on, immediately if possible or marks it as pending 1.4373 + * uninstall if not. 1.4374 + * 1.4375 + * @param aAddon 1.4376 + * The DBAddonInternal to uninstall 1.4377 + * @throws if the addon cannot be uninstalled because it is in an install 1.4378 + * location that does not allow it 1.4379 + */ 1.4380 + uninstallAddon: function XPI_uninstallAddon(aAddon) { 1.4381 + if (!(aAddon.inDatabase)) 1.4382 + throw new Error("Can only uninstall installed addons."); 1.4383 + 1.4384 + if (aAddon._installLocation.locked) 1.4385 + throw new Error("Cannot uninstall addons from locked install locations"); 1.4386 + 1.4387 + if ("_hasResourceCache" in aAddon) 1.4388 + aAddon._hasResourceCache = new Map(); 1.4389 + 1.4390 + if (aAddon._updateCheck) { 1.4391 + logger.debug("Cancel in-progress update check for " + aAddon.id); 1.4392 + aAddon._updateCheck.cancel(); 1.4393 + } 1.4394 + 1.4395 + // Inactive add-ons don't require a restart to uninstall 1.4396 + let requiresRestart = this.uninstallRequiresRestart(aAddon); 1.4397 + 1.4398 + if (requiresRestart) { 1.4399 + // We create an empty directory in the staging directory to indicate that 1.4400 + // an uninstall is necessary on next startup. 1.4401 + let stage = aAddon._installLocation.getStagingDir(); 1.4402 + stage.append(aAddon.id); 1.4403 + if (!stage.exists()) 1.4404 + stage.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); 1.4405 + 1.4406 + XPIDatabase.setAddonProperties(aAddon, { 1.4407 + pendingUninstall: true 1.4408 + }); 1.4409 + Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true); 1.4410 + } 1.4411 + 1.4412 + // If the add-on is not visible then there is no need to notify listeners. 1.4413 + if (!aAddon.visible) 1.4414 + return; 1.4415 + 1.4416 + let wrapper = createWrapper(aAddon); 1.4417 + AddonManagerPrivate.callAddonListeners("onUninstalling", wrapper, 1.4418 + requiresRestart); 1.4419 + 1.4420 + // Reveal the highest priority add-on with the same ID 1.4421 + function revealAddon(aAddon) { 1.4422 + XPIDatabase.makeAddonVisible(aAddon); 1.4423 + 1.4424 + let wrappedAddon = createWrapper(aAddon); 1.4425 + AddonManagerPrivate.callAddonListeners("onInstalling", wrappedAddon, false); 1.4426 + 1.4427 + if (!isAddonDisabled(aAddon) && !XPIProvider.enableRequiresRestart(aAddon)) { 1.4428 + XPIDatabase.updateAddonActive(aAddon, true); 1.4429 + } 1.4430 + 1.4431 + if (aAddon.bootstrap) { 1.4432 + let file = aAddon._installLocation.getLocationForID(aAddon.id); 1.4433 + XPIProvider.callBootstrapMethod(aAddon.id, aAddon.version, aAddon.type, file, 1.4434 + "install", BOOTSTRAP_REASONS.ADDON_INSTALL); 1.4435 + 1.4436 + if (aAddon.active) { 1.4437 + XPIProvider.callBootstrapMethod(aAddon.id, aAddon.version, aAddon.type, file, 1.4438 + "startup", BOOTSTRAP_REASONS.ADDON_INSTALL); 1.4439 + } 1.4440 + else { 1.4441 + XPIProvider.unloadBootstrapScope(aAddon.id); 1.4442 + } 1.4443 + } 1.4444 + 1.4445 + // We always send onInstalled even if a restart is required to enable 1.4446 + // the revealed add-on 1.4447 + AddonManagerPrivate.callAddonListeners("onInstalled", wrappedAddon); 1.4448 + } 1.4449 + 1.4450 + function checkInstallLocation(aPos) { 1.4451 + if (aPos < 0) 1.4452 + return; 1.4453 + 1.4454 + let location = XPIProvider.installLocations[aPos]; 1.4455 + XPIDatabase.getAddonInLocation(aAddon.id, location.name, 1.4456 + function checkInstallLocation_getAddonInLocation(aNewAddon) { 1.4457 + if (aNewAddon) 1.4458 + revealAddon(aNewAddon); 1.4459 + else 1.4460 + checkInstallLocation(aPos - 1); 1.4461 + }) 1.4462 + } 1.4463 + 1.4464 + if (!requiresRestart) { 1.4465 + if (aAddon.bootstrap) { 1.4466 + let file = aAddon._installLocation.getLocationForID(aAddon.id); 1.4467 + if (aAddon.active) { 1.4468 + this.callBootstrapMethod(aAddon.id, aAddon.version, aAddon.type, file, 1.4469 + "shutdown", 1.4470 + BOOTSTRAP_REASONS.ADDON_UNINSTALL); 1.4471 + } 1.4472 + 1.4473 + this.callBootstrapMethod(aAddon.id, aAddon.version, aAddon.type, file, 1.4474 + "uninstall", 1.4475 + BOOTSTRAP_REASONS.ADDON_UNINSTALL); 1.4476 + this.unloadBootstrapScope(aAddon.id); 1.4477 + flushStartupCache(); 1.4478 + } 1.4479 + aAddon._installLocation.uninstallAddon(aAddon.id); 1.4480 + XPIDatabase.removeAddonMetadata(aAddon); 1.4481 + AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper); 1.4482 + 1.4483 + checkInstallLocation(this.installLocations.length - 1); 1.4484 + } 1.4485 + 1.4486 + // Notify any other providers that a new theme has been enabled 1.4487 + if (aAddon.type == "theme" && aAddon.active) 1.4488 + AddonManagerPrivate.notifyAddonChanged(null, aAddon.type, requiresRestart); 1.4489 + }, 1.4490 + 1.4491 + /** 1.4492 + * Cancels the pending uninstall of an add-on. 1.4493 + * 1.4494 + * @param aAddon 1.4495 + * The DBAddonInternal to cancel uninstall for 1.4496 + */ 1.4497 + cancelUninstallAddon: function XPI_cancelUninstallAddon(aAddon) { 1.4498 + if (!(aAddon.inDatabase)) 1.4499 + throw new Error("Can only cancel uninstall for installed addons."); 1.4500 + 1.4501 + aAddon._installLocation.cleanStagingDir([aAddon.id]); 1.4502 + 1.4503 + XPIDatabase.setAddonProperties(aAddon, { 1.4504 + pendingUninstall: false 1.4505 + }); 1.4506 + 1.4507 + if (!aAddon.visible) 1.4508 + return; 1.4509 + 1.4510 + Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true); 1.4511 + 1.4512 + // TODO hide hidden add-ons (bug 557710) 1.4513 + let wrapper = createWrapper(aAddon); 1.4514 + AddonManagerPrivate.callAddonListeners("onOperationCancelled", wrapper); 1.4515 + 1.4516 + // Notify any other providers that this theme is now enabled again. 1.4517 + if (aAddon.type == "theme" && aAddon.active) 1.4518 + AddonManagerPrivate.notifyAddonChanged(aAddon.id, aAddon.type, false); 1.4519 + } 1.4520 +}; 1.4521 + 1.4522 +function getHashStringForCrypto(aCrypto) { 1.4523 + // return the two-digit hexadecimal code for a byte 1.4524 + function toHexString(charCode) 1.4525 + ("0" + charCode.toString(16)).slice(-2); 1.4526 + 1.4527 + // convert the binary hash data to a hex string. 1.4528 + let binary = aCrypto.finish(false); 1.4529 + return [toHexString(binary.charCodeAt(i)) for (i in binary)].join("").toLowerCase() 1.4530 +} 1.4531 + 1.4532 +/** 1.4533 + * Instantiates an AddonInstall. 1.4534 + * 1.4535 + * @param aInstallLocation 1.4536 + * The install location the add-on will be installed into 1.4537 + * @param aUrl 1.4538 + * The nsIURL to get the add-on from. If this is an nsIFileURL then 1.4539 + * the add-on will not need to be downloaded 1.4540 + * @param aHash 1.4541 + * An optional hash for the add-on 1.4542 + * @param aReleaseNotesURI 1.4543 + * An optional nsIURI of release notes for the add-on 1.4544 + * @param aExistingAddon 1.4545 + * The add-on this install will update if known 1.4546 + * @param aLoadGroup 1.4547 + * The nsILoadGroup to associate any requests with 1.4548 + * @throws if the url is the url of a local file and the hash does not match 1.4549 + * or the add-on does not contain an valid install manifest 1.4550 + */ 1.4551 +function AddonInstall(aInstallLocation, aUrl, aHash, aReleaseNotesURI, 1.4552 + aExistingAddon, aLoadGroup) { 1.4553 + this.wrapper = new AddonInstallWrapper(this); 1.4554 + this.installLocation = aInstallLocation; 1.4555 + this.sourceURI = aUrl; 1.4556 + this.releaseNotesURI = aReleaseNotesURI; 1.4557 + if (aHash) { 1.4558 + let hashSplit = aHash.toLowerCase().split(":"); 1.4559 + this.originalHash = { 1.4560 + algorithm: hashSplit[0], 1.4561 + data: hashSplit[1] 1.4562 + }; 1.4563 + } 1.4564 + this.hash = this.originalHash; 1.4565 + this.loadGroup = aLoadGroup; 1.4566 + this.listeners = []; 1.4567 + this.icons = {}; 1.4568 + this.existingAddon = aExistingAddon; 1.4569 + this.error = 0; 1.4570 + if (aLoadGroup) 1.4571 + this.window = aLoadGroup.notificationCallbacks 1.4572 + .getInterface(Ci.nsIDOMWindow); 1.4573 + else 1.4574 + this.window = null; 1.4575 + 1.4576 + // Giving each instance of AddonInstall a reference to the logger. 1.4577 + this.logger = logger; 1.4578 +} 1.4579 + 1.4580 +AddonInstall.prototype = { 1.4581 + installLocation: null, 1.4582 + wrapper: null, 1.4583 + stream: null, 1.4584 + crypto: null, 1.4585 + originalHash: null, 1.4586 + hash: null, 1.4587 + loadGroup: null, 1.4588 + badCertHandler: null, 1.4589 + listeners: null, 1.4590 + restartDownload: false, 1.4591 + 1.4592 + name: null, 1.4593 + type: null, 1.4594 + version: null, 1.4595 + icons: null, 1.4596 + releaseNotesURI: null, 1.4597 + sourceURI: null, 1.4598 + file: null, 1.4599 + ownsTempFile: false, 1.4600 + certificate: null, 1.4601 + certName: null, 1.4602 + 1.4603 + linkedInstalls: null, 1.4604 + existingAddon: null, 1.4605 + addon: null, 1.4606 + 1.4607 + state: null, 1.4608 + error: null, 1.4609 + progress: null, 1.4610 + maxProgress: null, 1.4611 + 1.4612 + /** 1.4613 + * Initialises this install to be a staged install waiting to be applied 1.4614 + * 1.4615 + * @param aManifest 1.4616 + * The cached manifest for the staged install 1.4617 + */ 1.4618 + initStagedInstall: function AI_initStagedInstall(aManifest) { 1.4619 + this.name = aManifest.name; 1.4620 + this.type = aManifest.type; 1.4621 + this.version = aManifest.version; 1.4622 + this.icons = aManifest.icons; 1.4623 + this.releaseNotesURI = aManifest.releaseNotesURI ? 1.4624 + NetUtil.newURI(aManifest.releaseNotesURI) : 1.4625 + null 1.4626 + this.sourceURI = aManifest.sourceURI ? 1.4627 + NetUtil.newURI(aManifest.sourceURI) : 1.4628 + null; 1.4629 + this.file = null; 1.4630 + this.addon = aManifest; 1.4631 + 1.4632 + this.state = AddonManager.STATE_INSTALLED; 1.4633 + 1.4634 + XPIProvider.installs.push(this); 1.4635 + }, 1.4636 + 1.4637 + /** 1.4638 + * Initialises this install to be an install from a local file. 1.4639 + * 1.4640 + * @param aCallback 1.4641 + * The callback to pass the initialised AddonInstall to 1.4642 + */ 1.4643 + initLocalInstall: function AI_initLocalInstall(aCallback) { 1.4644 + aCallback = makeSafe(aCallback); 1.4645 + this.file = this.sourceURI.QueryInterface(Ci.nsIFileURL).file; 1.4646 + 1.4647 + if (!this.file.exists()) { 1.4648 + logger.warn("XPI file " + this.file.path + " does not exist"); 1.4649 + this.state = AddonManager.STATE_DOWNLOAD_FAILED; 1.4650 + this.error = AddonManager.ERROR_NETWORK_FAILURE; 1.4651 + aCallback(this); 1.4652 + return; 1.4653 + } 1.4654 + 1.4655 + this.state = AddonManager.STATE_DOWNLOADED; 1.4656 + this.progress = this.file.fileSize; 1.4657 + this.maxProgress = this.file.fileSize; 1.4658 + 1.4659 + if (this.hash) { 1.4660 + let crypto = Cc["@mozilla.org/security/hash;1"]. 1.4661 + createInstance(Ci.nsICryptoHash); 1.4662 + try { 1.4663 + crypto.initWithString(this.hash.algorithm); 1.4664 + } 1.4665 + catch (e) { 1.4666 + logger.warn("Unknown hash algorithm '" + this.hash.algorithm + "' for addon " + this.sourceURI.spec, e); 1.4667 + this.state = AddonManager.STATE_DOWNLOAD_FAILED; 1.4668 + this.error = AddonManager.ERROR_INCORRECT_HASH; 1.4669 + aCallback(this); 1.4670 + return; 1.4671 + } 1.4672 + 1.4673 + let fis = Cc["@mozilla.org/network/file-input-stream;1"]. 1.4674 + createInstance(Ci.nsIFileInputStream); 1.4675 + fis.init(this.file, -1, -1, false); 1.4676 + crypto.updateFromStream(fis, this.file.fileSize); 1.4677 + let calculatedHash = getHashStringForCrypto(crypto); 1.4678 + if (calculatedHash != this.hash.data) { 1.4679 + logger.warn("File hash (" + calculatedHash + ") did not match provided hash (" + 1.4680 + this.hash.data + ")"); 1.4681 + this.state = AddonManager.STATE_DOWNLOAD_FAILED; 1.4682 + this.error = AddonManager.ERROR_INCORRECT_HASH; 1.4683 + aCallback(this); 1.4684 + return; 1.4685 + } 1.4686 + } 1.4687 + 1.4688 + try { 1.4689 + let self = this; 1.4690 + this.loadManifest(function initLocalInstall_loadManifest() { 1.4691 + XPIDatabase.getVisibleAddonForID(self.addon.id, function initLocalInstall_getVisibleAddon(aAddon) { 1.4692 + self.existingAddon = aAddon; 1.4693 + if (aAddon) 1.4694 + applyBlocklistChanges(aAddon, self.addon); 1.4695 + self.addon.updateDate = Date.now(); 1.4696 + self.addon.installDate = aAddon ? aAddon.installDate : self.addon.updateDate; 1.4697 + 1.4698 + if (!self.addon.isCompatible) { 1.4699 + // TODO Should we send some event here? 1.4700 + self.state = AddonManager.STATE_CHECKING; 1.4701 + new UpdateChecker(self.addon, { 1.4702 + onUpdateFinished: function updateChecker_onUpdateFinished(aAddon) { 1.4703 + self.state = AddonManager.STATE_DOWNLOADED; 1.4704 + XPIProvider.installs.push(self); 1.4705 + AddonManagerPrivate.callInstallListeners("onNewInstall", 1.4706 + self.listeners, 1.4707 + self.wrapper); 1.4708 + 1.4709 + aCallback(self); 1.4710 + } 1.4711 + }, AddonManager.UPDATE_WHEN_ADDON_INSTALLED); 1.4712 + } 1.4713 + else { 1.4714 + XPIProvider.installs.push(self); 1.4715 + AddonManagerPrivate.callInstallListeners("onNewInstall", 1.4716 + self.listeners, 1.4717 + self.wrapper); 1.4718 + 1.4719 + aCallback(self); 1.4720 + } 1.4721 + }); 1.4722 + }); 1.4723 + } 1.4724 + catch (e) { 1.4725 + logger.warn("Invalid XPI", e); 1.4726 + this.state = AddonManager.STATE_DOWNLOAD_FAILED; 1.4727 + this.error = AddonManager.ERROR_CORRUPT_FILE; 1.4728 + aCallback(this); 1.4729 + return; 1.4730 + } 1.4731 + }, 1.4732 + 1.4733 + /** 1.4734 + * Initialises this install to be a download from a remote url. 1.4735 + * 1.4736 + * @param aCallback 1.4737 + * The callback to pass the initialised AddonInstall to 1.4738 + * @param aName 1.4739 + * An optional name for the add-on 1.4740 + * @param aType 1.4741 + * An optional type for the add-on 1.4742 + * @param aIcons 1.4743 + * Optional icons for the add-on 1.4744 + * @param aVersion 1.4745 + * An optional version for the add-on 1.4746 + */ 1.4747 + initAvailableDownload: function AI_initAvailableDownload(aName, aType, aIcons, aVersion, aCallback) { 1.4748 + this.state = AddonManager.STATE_AVAILABLE; 1.4749 + this.name = aName; 1.4750 + this.type = aType; 1.4751 + this.version = aVersion; 1.4752 + this.icons = aIcons; 1.4753 + this.progress = 0; 1.4754 + this.maxProgress = -1; 1.4755 + 1.4756 + XPIProvider.installs.push(this); 1.4757 + AddonManagerPrivate.callInstallListeners("onNewInstall", this.listeners, 1.4758 + this.wrapper); 1.4759 + 1.4760 + makeSafe(aCallback)(this); 1.4761 + }, 1.4762 + 1.4763 + /** 1.4764 + * Starts installation of this add-on from whatever state it is currently at 1.4765 + * if possible. 1.4766 + * 1.4767 + * @throws if installation cannot proceed from the current state 1.4768 + */ 1.4769 + install: function AI_install() { 1.4770 + switch (this.state) { 1.4771 + case AddonManager.STATE_AVAILABLE: 1.4772 + this.startDownload(); 1.4773 + break; 1.4774 + case AddonManager.STATE_DOWNLOADED: 1.4775 + this.startInstall(); 1.4776 + break; 1.4777 + case AddonManager.STATE_DOWNLOAD_FAILED: 1.4778 + case AddonManager.STATE_INSTALL_FAILED: 1.4779 + case AddonManager.STATE_CANCELLED: 1.4780 + this.removeTemporaryFile(); 1.4781 + this.state = AddonManager.STATE_AVAILABLE; 1.4782 + this.error = 0; 1.4783 + this.progress = 0; 1.4784 + this.maxProgress = -1; 1.4785 + this.hash = this.originalHash; 1.4786 + XPIProvider.installs.push(this); 1.4787 + this.startDownload(); 1.4788 + break; 1.4789 + case AddonManager.STATE_DOWNLOADING: 1.4790 + case AddonManager.STATE_CHECKING: 1.4791 + case AddonManager.STATE_INSTALLING: 1.4792 + // Installation is already running 1.4793 + return; 1.4794 + default: 1.4795 + throw new Error("Cannot start installing from this state"); 1.4796 + } 1.4797 + }, 1.4798 + 1.4799 + /** 1.4800 + * Cancels installation of this add-on. 1.4801 + * 1.4802 + * @throws if installation cannot be cancelled from the current state 1.4803 + */ 1.4804 + cancel: function AI_cancel() { 1.4805 + switch (this.state) { 1.4806 + case AddonManager.STATE_DOWNLOADING: 1.4807 + if (this.channel) 1.4808 + this.channel.cancel(Cr.NS_BINDING_ABORTED); 1.4809 + case AddonManager.STATE_AVAILABLE: 1.4810 + case AddonManager.STATE_DOWNLOADED: 1.4811 + logger.debug("Cancelling download of " + this.sourceURI.spec); 1.4812 + this.state = AddonManager.STATE_CANCELLED; 1.4813 + XPIProvider.removeActiveInstall(this); 1.4814 + AddonManagerPrivate.callInstallListeners("onDownloadCancelled", 1.4815 + this.listeners, this.wrapper); 1.4816 + this.removeTemporaryFile(); 1.4817 + break; 1.4818 + case AddonManager.STATE_INSTALLED: 1.4819 + logger.debug("Cancelling install of " + this.addon.id); 1.4820 + let xpi = this.installLocation.getStagingDir(); 1.4821 + xpi.append(this.addon.id + ".xpi"); 1.4822 + flushJarCache(xpi); 1.4823 + this.installLocation.cleanStagingDir([this.addon.id, this.addon.id + ".xpi", 1.4824 + this.addon.id + ".json"]); 1.4825 + this.state = AddonManager.STATE_CANCELLED; 1.4826 + XPIProvider.removeActiveInstall(this); 1.4827 + 1.4828 + if (this.existingAddon) { 1.4829 + delete this.existingAddon.pendingUpgrade; 1.4830 + this.existingAddon.pendingUpgrade = null; 1.4831 + } 1.4832 + 1.4833 + AddonManagerPrivate.callAddonListeners("onOperationCancelled", createWrapper(this.addon)); 1.4834 + 1.4835 + AddonManagerPrivate.callInstallListeners("onInstallCancelled", 1.4836 + this.listeners, this.wrapper); 1.4837 + break; 1.4838 + default: 1.4839 + throw new Error("Cannot cancel install of " + this.sourceURI.spec + 1.4840 + " from this state (" + this.state + ")"); 1.4841 + } 1.4842 + }, 1.4843 + 1.4844 + /** 1.4845 + * Adds an InstallListener for this instance if the listener is not already 1.4846 + * registered. 1.4847 + * 1.4848 + * @param aListener 1.4849 + * The InstallListener to add 1.4850 + */ 1.4851 + addListener: function AI_addListener(aListener) { 1.4852 + if (!this.listeners.some(function addListener_matchListener(i) { return i == aListener; })) 1.4853 + this.listeners.push(aListener); 1.4854 + }, 1.4855 + 1.4856 + /** 1.4857 + * Removes an InstallListener for this instance if it is registered. 1.4858 + * 1.4859 + * @param aListener 1.4860 + * The InstallListener to remove 1.4861 + */ 1.4862 + removeListener: function AI_removeListener(aListener) { 1.4863 + this.listeners = this.listeners.filter(function removeListener_filterListener(i) { 1.4864 + return i != aListener; 1.4865 + }); 1.4866 + }, 1.4867 + 1.4868 + /** 1.4869 + * Removes the temporary file owned by this AddonInstall if there is one. 1.4870 + */ 1.4871 + removeTemporaryFile: function AI_removeTemporaryFile() { 1.4872 + // Only proceed if this AddonInstall owns its XPI file 1.4873 + if (!this.ownsTempFile) { 1.4874 + this.logger.debug("removeTemporaryFile: " + this.sourceURI.spec + " does not own temp file"); 1.4875 + return; 1.4876 + } 1.4877 + 1.4878 + try { 1.4879 + this.logger.debug("removeTemporaryFile: " + this.sourceURI.spec + " removing temp file " + 1.4880 + this.file.path); 1.4881 + this.file.remove(true); 1.4882 + this.ownsTempFile = false; 1.4883 + } 1.4884 + catch (e) { 1.4885 + this.logger.warn("Failed to remove temporary file " + this.file.path + " for addon " + 1.4886 + this.sourceURI.spec, 1.4887 + e); 1.4888 + } 1.4889 + }, 1.4890 + 1.4891 + /** 1.4892 + * Updates the sourceURI and releaseNotesURI values on the Addon being 1.4893 + * installed by this AddonInstall instance. 1.4894 + */ 1.4895 + updateAddonURIs: function AI_updateAddonURIs() { 1.4896 + this.addon.sourceURI = this.sourceURI.spec; 1.4897 + if (this.releaseNotesURI) 1.4898 + this.addon.releaseNotesURI = this.releaseNotesURI.spec; 1.4899 + }, 1.4900 + 1.4901 + /** 1.4902 + * Loads add-on manifests from a multi-package XPI file. Each of the 1.4903 + * XPI and JAR files contained in the XPI will be extracted. Any that 1.4904 + * do not contain valid add-ons will be ignored. The first valid add-on will 1.4905 + * be installed by this AddonInstall instance, the rest will have new 1.4906 + * AddonInstall instances created for them. 1.4907 + * 1.4908 + * @param aZipReader 1.4909 + * An open nsIZipReader for the multi-package XPI's files. This will 1.4910 + * be closed before this method returns. 1.4911 + * @param aCallback 1.4912 + * A function to call when all of the add-on manifests have been 1.4913 + * loaded. Because this loadMultipackageManifests is an internal API 1.4914 + * we don't exception-wrap this callback 1.4915 + */ 1.4916 + _loadMultipackageManifests: function AI_loadMultipackageManifests(aZipReader, 1.4917 + aCallback) { 1.4918 + let files = []; 1.4919 + let entries = aZipReader.findEntries("(*.[Xx][Pp][Ii]|*.[Jj][Aa][Rr])"); 1.4920 + while (entries.hasMore()) { 1.4921 + let entryName = entries.getNext(); 1.4922 + var target = getTemporaryFile(); 1.4923 + try { 1.4924 + aZipReader.extract(entryName, target); 1.4925 + files.push(target); 1.4926 + } 1.4927 + catch (e) { 1.4928 + logger.warn("Failed to extract " + entryName + " from multi-package " + 1.4929 + "XPI", e); 1.4930 + target.remove(false); 1.4931 + } 1.4932 + } 1.4933 + 1.4934 + aZipReader.close(); 1.4935 + 1.4936 + if (files.length == 0) { 1.4937 + throw new Error("Multi-package XPI does not contain any packages " + 1.4938 + "to install"); 1.4939 + } 1.4940 + 1.4941 + let addon = null; 1.4942 + 1.4943 + // Find the first file that has a valid install manifest and use it for 1.4944 + // the add-on that this AddonInstall instance will install. 1.4945 + while (files.length > 0) { 1.4946 + this.removeTemporaryFile(); 1.4947 + this.file = files.shift(); 1.4948 + this.ownsTempFile = true; 1.4949 + try { 1.4950 + addon = loadManifestFromZipFile(this.file); 1.4951 + break; 1.4952 + } 1.4953 + catch (e) { 1.4954 + logger.warn(this.file.leafName + " cannot be installed from multi-package " + 1.4955 + "XPI", e); 1.4956 + } 1.4957 + } 1.4958 + 1.4959 + if (!addon) { 1.4960 + // No valid add-on was found 1.4961 + aCallback(); 1.4962 + return; 1.4963 + } 1.4964 + 1.4965 + this.addon = addon; 1.4966 + 1.4967 + this.updateAddonURIs(); 1.4968 + 1.4969 + this.addon._install = this; 1.4970 + this.name = this.addon.selectedLocale.name; 1.4971 + this.type = this.addon.type; 1.4972 + this.version = this.addon.version; 1.4973 + 1.4974 + // Setting the iconURL to something inside the XPI locks the XPI and 1.4975 + // makes it impossible to delete on Windows. 1.4976 + //let newIcon = createWrapper(this.addon).iconURL; 1.4977 + //if (newIcon) 1.4978 + // this.iconURL = newIcon; 1.4979 + 1.4980 + // Create new AddonInstall instances for every remaining file 1.4981 + if (files.length > 0) { 1.4982 + this.linkedInstalls = []; 1.4983 + let count = 0; 1.4984 + let self = this; 1.4985 + files.forEach(function(file) { 1.4986 + AddonInstall.createInstall(function loadMultipackageManifests_createInstall(aInstall) { 1.4987 + // Ignore bad add-ons (createInstall will have logged the error) 1.4988 + if (aInstall.state == AddonManager.STATE_DOWNLOAD_FAILED) { 1.4989 + // Manually remove the temporary file 1.4990 + file.remove(true); 1.4991 + } 1.4992 + else { 1.4993 + // Make the new install own its temporary file 1.4994 + aInstall.ownsTempFile = true; 1.4995 + 1.4996 + self.linkedInstalls.push(aInstall) 1.4997 + 1.4998 + aInstall.sourceURI = self.sourceURI; 1.4999 + aInstall.releaseNotesURI = self.releaseNotesURI; 1.5000 + aInstall.updateAddonURIs(); 1.5001 + } 1.5002 + 1.5003 + count++; 1.5004 + if (count == files.length) 1.5005 + aCallback(); 1.5006 + }, file); 1.5007 + }, this); 1.5008 + } 1.5009 + else { 1.5010 + aCallback(); 1.5011 + } 1.5012 + }, 1.5013 + 1.5014 + /** 1.5015 + * Called after the add-on is a local file and the signature and install 1.5016 + * manifest can be read. 1.5017 + * 1.5018 + * @param aCallback 1.5019 + * A function to call when the manifest has been loaded 1.5020 + * @throws if the add-on does not contain a valid install manifest or the 1.5021 + * XPI is incorrectly signed 1.5022 + */ 1.5023 + loadManifest: function AI_loadManifest(aCallback) { 1.5024 + aCallback = makeSafe(aCallback); 1.5025 + let self = this; 1.5026 + function addRepositoryData(aAddon) { 1.5027 + // Try to load from the existing cache first 1.5028 + AddonRepository.getCachedAddonByID(aAddon.id, function loadManifest_getCachedAddonByID(aRepoAddon) { 1.5029 + if (aRepoAddon) { 1.5030 + aAddon._repositoryAddon = aRepoAddon; 1.5031 + self.name = self.name || aAddon._repositoryAddon.name; 1.5032 + aAddon.compatibilityOverrides = aRepoAddon.compatibilityOverrides; 1.5033 + aAddon.appDisabled = !isUsableAddon(aAddon); 1.5034 + aCallback(); 1.5035 + return; 1.5036 + } 1.5037 + 1.5038 + // It wasn't there so try to re-download it 1.5039 + AddonRepository.cacheAddons([aAddon.id], function loadManifest_cacheAddons() { 1.5040 + AddonRepository.getCachedAddonByID(aAddon.id, function loadManifest_getCachedAddonByID(aRepoAddon) { 1.5041 + aAddon._repositoryAddon = aRepoAddon; 1.5042 + self.name = self.name || aAddon._repositoryAddon.name; 1.5043 + aAddon.compatibilityOverrides = aRepoAddon ? 1.5044 + aRepoAddon.compatibilityOverrides : 1.5045 + null; 1.5046 + aAddon.appDisabled = !isUsableAddon(aAddon); 1.5047 + aCallback(); 1.5048 + }); 1.5049 + }); 1.5050 + }); 1.5051 + } 1.5052 + 1.5053 + let zipreader = Cc["@mozilla.org/libjar/zip-reader;1"]. 1.5054 + createInstance(Ci.nsIZipReader); 1.5055 + try { 1.5056 + zipreader.open(this.file); 1.5057 + } 1.5058 + catch (e) { 1.5059 + zipreader.close(); 1.5060 + throw e; 1.5061 + } 1.5062 + 1.5063 + let principal = zipreader.getCertificatePrincipal(null); 1.5064 + if (principal && principal.hasCertificate) { 1.5065 + logger.debug("Verifying XPI signature"); 1.5066 + if (verifyZipSigning(zipreader, principal)) { 1.5067 + let x509 = principal.certificate; 1.5068 + if (x509 instanceof Ci.nsIX509Cert) 1.5069 + this.certificate = x509; 1.5070 + if (this.certificate && this.certificate.commonName.length > 0) 1.5071 + this.certName = this.certificate.commonName; 1.5072 + else 1.5073 + this.certName = principal.prettyName; 1.5074 + } 1.5075 + else { 1.5076 + zipreader.close(); 1.5077 + throw new Error("XPI is incorrectly signed"); 1.5078 + } 1.5079 + } 1.5080 + 1.5081 + try { 1.5082 + this.addon = loadManifestFromZipReader(zipreader); 1.5083 + } 1.5084 + catch (e) { 1.5085 + zipreader.close(); 1.5086 + throw e; 1.5087 + } 1.5088 + 1.5089 + if (this.addon.type == "multipackage") { 1.5090 + this._loadMultipackageManifests(zipreader, function loadManifest_loadMultipackageManifests() { 1.5091 + addRepositoryData(self.addon); 1.5092 + }); 1.5093 + return; 1.5094 + } 1.5095 + 1.5096 + zipreader.close(); 1.5097 + 1.5098 + this.updateAddonURIs(); 1.5099 + 1.5100 + this.addon._install = this; 1.5101 + this.name = this.addon.selectedLocale.name; 1.5102 + this.type = this.addon.type; 1.5103 + this.version = this.addon.version; 1.5104 + 1.5105 + // Setting the iconURL to something inside the XPI locks the XPI and 1.5106 + // makes it impossible to delete on Windows. 1.5107 + //let newIcon = createWrapper(this.addon).iconURL; 1.5108 + //if (newIcon) 1.5109 + // this.iconURL = newIcon; 1.5110 + 1.5111 + addRepositoryData(this.addon); 1.5112 + }, 1.5113 + 1.5114 + observe: function AI_observe(aSubject, aTopic, aData) { 1.5115 + // Network is going offline 1.5116 + this.cancel(); 1.5117 + }, 1.5118 + 1.5119 + /** 1.5120 + * Starts downloading the add-on's XPI file. 1.5121 + */ 1.5122 + startDownload: function AI_startDownload() { 1.5123 + this.state = AddonManager.STATE_DOWNLOADING; 1.5124 + if (!AddonManagerPrivate.callInstallListeners("onDownloadStarted", 1.5125 + this.listeners, this.wrapper)) { 1.5126 + logger.debug("onDownloadStarted listeners cancelled installation of addon " + this.sourceURI.spec); 1.5127 + this.state = AddonManager.STATE_CANCELLED; 1.5128 + XPIProvider.removeActiveInstall(this); 1.5129 + AddonManagerPrivate.callInstallListeners("onDownloadCancelled", 1.5130 + this.listeners, this.wrapper) 1.5131 + return; 1.5132 + } 1.5133 + 1.5134 + // If a listener changed our state then do not proceed with the download 1.5135 + if (this.state != AddonManager.STATE_DOWNLOADING) 1.5136 + return; 1.5137 + 1.5138 + if (this.channel) { 1.5139 + // A previous download attempt hasn't finished cleaning up yet, signal 1.5140 + // that it should restart when complete 1.5141 + logger.debug("Waiting for previous download to complete"); 1.5142 + this.restartDownload = true; 1.5143 + return; 1.5144 + } 1.5145 + 1.5146 + this.openChannel(); 1.5147 + }, 1.5148 + 1.5149 + openChannel: function AI_openChannel() { 1.5150 + this.restartDownload = false; 1.5151 + 1.5152 + try { 1.5153 + this.file = getTemporaryFile(); 1.5154 + this.ownsTempFile = true; 1.5155 + this.stream = Cc["@mozilla.org/network/file-output-stream;1"]. 1.5156 + createInstance(Ci.nsIFileOutputStream); 1.5157 + this.stream.init(this.file, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | 1.5158 + FileUtils.MODE_TRUNCATE, FileUtils.PERMS_FILE, 0); 1.5159 + } 1.5160 + catch (e) { 1.5161 + logger.warn("Failed to start download for addon " + this.sourceURI.spec, e); 1.5162 + this.state = AddonManager.STATE_DOWNLOAD_FAILED; 1.5163 + this.error = AddonManager.ERROR_FILE_ACCESS; 1.5164 + XPIProvider.removeActiveInstall(this); 1.5165 + AddonManagerPrivate.callInstallListeners("onDownloadFailed", 1.5166 + this.listeners, this.wrapper); 1.5167 + return; 1.5168 + } 1.5169 + 1.5170 + let listener = Cc["@mozilla.org/network/stream-listener-tee;1"]. 1.5171 + createInstance(Ci.nsIStreamListenerTee); 1.5172 + listener.init(this, this.stream); 1.5173 + try { 1.5174 + Components.utils.import("resource://gre/modules/CertUtils.jsm"); 1.5175 + let requireBuiltIn = Prefs.getBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, true); 1.5176 + this.badCertHandler = new BadCertHandler(!requireBuiltIn); 1.5177 + 1.5178 + this.channel = NetUtil.newChannel(this.sourceURI); 1.5179 + this.channel.notificationCallbacks = this; 1.5180 + if (this.channel instanceof Ci.nsIHttpChannelInternal) 1.5181 + this.channel.forceAllowThirdPartyCookie = true; 1.5182 + this.channel.asyncOpen(listener, null); 1.5183 + 1.5184 + Services.obs.addObserver(this, "network:offline-about-to-go-offline", false); 1.5185 + } 1.5186 + catch (e) { 1.5187 + logger.warn("Failed to start download for addon " + this.sourceURI.spec, e); 1.5188 + this.state = AddonManager.STATE_DOWNLOAD_FAILED; 1.5189 + this.error = AddonManager.ERROR_NETWORK_FAILURE; 1.5190 + XPIProvider.removeActiveInstall(this); 1.5191 + AddonManagerPrivate.callInstallListeners("onDownloadFailed", 1.5192 + this.listeners, this.wrapper); 1.5193 + } 1.5194 + }, 1.5195 + 1.5196 + /** 1.5197 + * Update the crypto hasher with the new data and call the progress listeners. 1.5198 + * 1.5199 + * @see nsIStreamListener 1.5200 + */ 1.5201 + onDataAvailable: function AI_onDataAvailable(aRequest, aContext, aInputstream, 1.5202 + aOffset, aCount) { 1.5203 + this.crypto.updateFromStream(aInputstream, aCount); 1.5204 + this.progress += aCount; 1.5205 + if (!AddonManagerPrivate.callInstallListeners("onDownloadProgress", 1.5206 + this.listeners, this.wrapper)) { 1.5207 + // TODO cancel the download and make it available again (bug 553024) 1.5208 + } 1.5209 + }, 1.5210 + 1.5211 + /** 1.5212 + * Check the redirect response for a hash of the target XPI and verify that 1.5213 + * we don't end up on an insecure channel. 1.5214 + * 1.5215 + * @see nsIChannelEventSink 1.5216 + */ 1.5217 + asyncOnChannelRedirect: function AI_asyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, aCallback) { 1.5218 + if (!this.hash && aOldChannel.originalURI.schemeIs("https") && 1.5219 + aOldChannel instanceof Ci.nsIHttpChannel) { 1.5220 + try { 1.5221 + let hashStr = aOldChannel.getResponseHeader("X-Target-Digest"); 1.5222 + let hashSplit = hashStr.toLowerCase().split(":"); 1.5223 + this.hash = { 1.5224 + algorithm: hashSplit[0], 1.5225 + data: hashSplit[1] 1.5226 + }; 1.5227 + } 1.5228 + catch (e) { 1.5229 + } 1.5230 + } 1.5231 + 1.5232 + // Verify that we don't end up on an insecure channel if we haven't got a 1.5233 + // hash to verify with (see bug 537761 for discussion) 1.5234 + if (!this.hash) 1.5235 + this.badCertHandler.asyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, aCallback); 1.5236 + else 1.5237 + aCallback.onRedirectVerifyCallback(Cr.NS_OK); 1.5238 + 1.5239 + this.channel = aNewChannel; 1.5240 + }, 1.5241 + 1.5242 + /** 1.5243 + * This is the first chance to get at real headers on the channel. 1.5244 + * 1.5245 + * @see nsIStreamListener 1.5246 + */ 1.5247 + onStartRequest: function AI_onStartRequest(aRequest, aContext) { 1.5248 + this.crypto = Cc["@mozilla.org/security/hash;1"]. 1.5249 + createInstance(Ci.nsICryptoHash); 1.5250 + if (this.hash) { 1.5251 + try { 1.5252 + this.crypto.initWithString(this.hash.algorithm); 1.5253 + } 1.5254 + catch (e) { 1.5255 + logger.warn("Unknown hash algorithm '" + this.hash.algorithm + "' for addon " + this.sourceURI.spec, e); 1.5256 + this.state = AddonManager.STATE_DOWNLOAD_FAILED; 1.5257 + this.error = AddonManager.ERROR_INCORRECT_HASH; 1.5258 + XPIProvider.removeActiveInstall(this); 1.5259 + AddonManagerPrivate.callInstallListeners("onDownloadFailed", 1.5260 + this.listeners, this.wrapper); 1.5261 + aRequest.cancel(Cr.NS_BINDING_ABORTED); 1.5262 + return; 1.5263 + } 1.5264 + } 1.5265 + else { 1.5266 + // We always need something to consume data from the inputstream passed 1.5267 + // to onDataAvailable so just create a dummy cryptohasher to do that. 1.5268 + this.crypto.initWithString("sha1"); 1.5269 + } 1.5270 + 1.5271 + this.progress = 0; 1.5272 + if (aRequest instanceof Ci.nsIChannel) { 1.5273 + try { 1.5274 + this.maxProgress = aRequest.contentLength; 1.5275 + } 1.5276 + catch (e) { 1.5277 + } 1.5278 + logger.debug("Download started for " + this.sourceURI.spec + " to file " + 1.5279 + this.file.path); 1.5280 + } 1.5281 + }, 1.5282 + 1.5283 + /** 1.5284 + * The download is complete. 1.5285 + * 1.5286 + * @see nsIStreamListener 1.5287 + */ 1.5288 + onStopRequest: function AI_onStopRequest(aRequest, aContext, aStatus) { 1.5289 + this.stream.close(); 1.5290 + this.channel = null; 1.5291 + this.badCerthandler = null; 1.5292 + Services.obs.removeObserver(this, "network:offline-about-to-go-offline"); 1.5293 + 1.5294 + // If the download was cancelled then all events will have already been sent 1.5295 + if (aStatus == Cr.NS_BINDING_ABORTED) { 1.5296 + this.removeTemporaryFile(); 1.5297 + if (this.restartDownload) 1.5298 + this.openChannel(); 1.5299 + return; 1.5300 + } 1.5301 + 1.5302 + logger.debug("Download of " + this.sourceURI.spec + " completed."); 1.5303 + 1.5304 + if (Components.isSuccessCode(aStatus)) { 1.5305 + if (!(aRequest instanceof Ci.nsIHttpChannel) || aRequest.requestSucceeded) { 1.5306 + if (!this.hash && (aRequest instanceof Ci.nsIChannel)) { 1.5307 + try { 1.5308 + checkCert(aRequest, 1.5309 + !Prefs.getBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, true)); 1.5310 + } 1.5311 + catch (e) { 1.5312 + this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, e); 1.5313 + return; 1.5314 + } 1.5315 + } 1.5316 + 1.5317 + // convert the binary hash data to a hex string. 1.5318 + let calculatedHash = getHashStringForCrypto(this.crypto); 1.5319 + this.crypto = null; 1.5320 + if (this.hash && calculatedHash != this.hash.data) { 1.5321 + this.downloadFailed(AddonManager.ERROR_INCORRECT_HASH, 1.5322 + "Downloaded file hash (" + calculatedHash + 1.5323 + ") did not match provided hash (" + this.hash.data + ")"); 1.5324 + return; 1.5325 + } 1.5326 + try { 1.5327 + let self = this; 1.5328 + this.loadManifest(function onStopRequest_loadManifest() { 1.5329 + if (self.addon.isCompatible) { 1.5330 + self.downloadCompleted(); 1.5331 + } 1.5332 + else { 1.5333 + // TODO Should we send some event here (bug 557716)? 1.5334 + self.state = AddonManager.STATE_CHECKING; 1.5335 + new UpdateChecker(self.addon, { 1.5336 + onUpdateFinished: function onStopRequest_onUpdateFinished(aAddon) { 1.5337 + self.downloadCompleted(); 1.5338 + } 1.5339 + }, AddonManager.UPDATE_WHEN_ADDON_INSTALLED); 1.5340 + } 1.5341 + }); 1.5342 + } 1.5343 + catch (e) { 1.5344 + this.downloadFailed(AddonManager.ERROR_CORRUPT_FILE, e); 1.5345 + } 1.5346 + } 1.5347 + else { 1.5348 + if (aRequest instanceof Ci.nsIHttpChannel) 1.5349 + this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, 1.5350 + aRequest.responseStatus + " " + 1.5351 + aRequest.responseStatusText); 1.5352 + else 1.5353 + this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, aStatus); 1.5354 + } 1.5355 + } 1.5356 + else { 1.5357 + this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, aStatus); 1.5358 + } 1.5359 + }, 1.5360 + 1.5361 + /** 1.5362 + * Notify listeners that the download failed. 1.5363 + * 1.5364 + * @param aReason 1.5365 + * Something to log about the failure 1.5366 + * @param error 1.5367 + * The error code to pass to the listeners 1.5368 + */ 1.5369 + downloadFailed: function AI_downloadFailed(aReason, aError) { 1.5370 + logger.warn("Download of " + this.sourceURI.spec + " failed", aError); 1.5371 + this.state = AddonManager.STATE_DOWNLOAD_FAILED; 1.5372 + this.error = aReason; 1.5373 + XPIProvider.removeActiveInstall(this); 1.5374 + AddonManagerPrivate.callInstallListeners("onDownloadFailed", this.listeners, 1.5375 + this.wrapper); 1.5376 + 1.5377 + // If the listener hasn't restarted the download then remove any temporary 1.5378 + // file 1.5379 + if (this.state == AddonManager.STATE_DOWNLOAD_FAILED) { 1.5380 + logger.debug("downloadFailed: removing temp file for " + this.sourceURI.spec); 1.5381 + this.removeTemporaryFile(); 1.5382 + } 1.5383 + else 1.5384 + logger.debug("downloadFailed: listener changed AddonInstall state for " + 1.5385 + this.sourceURI.spec + " to " + this.state); 1.5386 + }, 1.5387 + 1.5388 + /** 1.5389 + * Notify listeners that the download completed. 1.5390 + */ 1.5391 + downloadCompleted: function AI_downloadCompleted() { 1.5392 + let self = this; 1.5393 + XPIDatabase.getVisibleAddonForID(this.addon.id, function downloadCompleted_getVisibleAddonForID(aAddon) { 1.5394 + if (aAddon) 1.5395 + self.existingAddon = aAddon; 1.5396 + 1.5397 + self.state = AddonManager.STATE_DOWNLOADED; 1.5398 + self.addon.updateDate = Date.now(); 1.5399 + 1.5400 + if (self.existingAddon) { 1.5401 + self.addon.existingAddonID = self.existingAddon.id; 1.5402 + self.addon.installDate = self.existingAddon.installDate; 1.5403 + applyBlocklistChanges(self.existingAddon, self.addon); 1.5404 + } 1.5405 + else { 1.5406 + self.addon.installDate = self.addon.updateDate; 1.5407 + } 1.5408 + 1.5409 + if (AddonManagerPrivate.callInstallListeners("onDownloadEnded", 1.5410 + self.listeners, 1.5411 + self.wrapper)) { 1.5412 + // If a listener changed our state then do not proceed with the install 1.5413 + if (self.state != AddonManager.STATE_DOWNLOADED) 1.5414 + return; 1.5415 + 1.5416 + self.install(); 1.5417 + 1.5418 + if (self.linkedInstalls) { 1.5419 + self.linkedInstalls.forEach(function(aInstall) { 1.5420 + aInstall.install(); 1.5421 + }); 1.5422 + } 1.5423 + } 1.5424 + }); 1.5425 + }, 1.5426 + 1.5427 + // TODO This relies on the assumption that we are always installing into the 1.5428 + // highest priority install location so the resulting add-on will be visible 1.5429 + // overriding any existing copy in another install location (bug 557710). 1.5430 + /** 1.5431 + * Installs the add-on into the install location. 1.5432 + */ 1.5433 + startInstall: function AI_startInstall() { 1.5434 + this.state = AddonManager.STATE_INSTALLING; 1.5435 + if (!AddonManagerPrivate.callInstallListeners("onInstallStarted", 1.5436 + this.listeners, this.wrapper)) { 1.5437 + this.state = AddonManager.STATE_DOWNLOADED; 1.5438 + XPIProvider.removeActiveInstall(this); 1.5439 + AddonManagerPrivate.callInstallListeners("onInstallCancelled", 1.5440 + this.listeners, this.wrapper) 1.5441 + return; 1.5442 + } 1.5443 + 1.5444 + // Find and cancel any pending installs for the same add-on in the same 1.5445 + // install location 1.5446 + for (let aInstall of XPIProvider.installs) { 1.5447 + if (aInstall.state == AddonManager.STATE_INSTALLED && 1.5448 + aInstall.installLocation == this.installLocation && 1.5449 + aInstall.addon.id == this.addon.id) { 1.5450 + logger.debug("Cancelling previous pending install of " + aInstall.addon.id); 1.5451 + aInstall.cancel(); 1.5452 + } 1.5453 + } 1.5454 + 1.5455 + let isUpgrade = this.existingAddon && 1.5456 + this.existingAddon._installLocation == this.installLocation; 1.5457 + let requiresRestart = XPIProvider.installRequiresRestart(this.addon); 1.5458 + 1.5459 + logger.debug("Starting install of " + this.addon.id + " from " + this.sourceURI.spec); 1.5460 + AddonManagerPrivate.callAddonListeners("onInstalling", 1.5461 + createWrapper(this.addon), 1.5462 + requiresRestart); 1.5463 + 1.5464 + let stagingDir = this.installLocation.getStagingDir(); 1.5465 + let stagedAddon = stagingDir.clone(); 1.5466 + 1.5467 + Task.spawn((function() { 1.5468 + let installedUnpacked = 0; 1.5469 + yield this.installLocation.requestStagingDir(); 1.5470 + 1.5471 + // First stage the file regardless of whether restarting is necessary 1.5472 + if (this.addon.unpack || Prefs.getBoolPref(PREF_XPI_UNPACK, false)) { 1.5473 + logger.debug("Addon " + this.addon.id + " will be installed as " + 1.5474 + "an unpacked directory"); 1.5475 + stagedAddon.append(this.addon.id); 1.5476 + yield removeAsync(stagedAddon); 1.5477 + yield OS.File.makeDir(stagedAddon.path); 1.5478 + yield ZipUtils.extractFilesAsync(this.file, stagedAddon); 1.5479 + installedUnpacked = 1; 1.5480 + } 1.5481 + else { 1.5482 + logger.debug("Addon " + this.addon.id + " will be installed as " + 1.5483 + "a packed xpi"); 1.5484 + stagedAddon.append(this.addon.id + ".xpi"); 1.5485 + yield removeAsync(stagedAddon); 1.5486 + yield OS.File.copy(this.file.path, stagedAddon.path); 1.5487 + } 1.5488 + 1.5489 + if (requiresRestart) { 1.5490 + // Point the add-on to its extracted files as the xpi may get deleted 1.5491 + this.addon._sourceBundle = stagedAddon; 1.5492 + 1.5493 + // Cache the AddonInternal as it may have updated compatibility info 1.5494 + let stagedJSON = stagedAddon.clone(); 1.5495 + stagedJSON.leafName = this.addon.id + ".json"; 1.5496 + if (stagedJSON.exists()) 1.5497 + stagedJSON.remove(true); 1.5498 + let stream = Cc["@mozilla.org/network/file-output-stream;1"]. 1.5499 + createInstance(Ci.nsIFileOutputStream); 1.5500 + let converter = Cc["@mozilla.org/intl/converter-output-stream;1"]. 1.5501 + createInstance(Ci.nsIConverterOutputStream); 1.5502 + 1.5503 + try { 1.5504 + stream.init(stagedJSON, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | 1.5505 + FileUtils.MODE_TRUNCATE, FileUtils.PERMS_FILE, 1.5506 + 0); 1.5507 + converter.init(stream, "UTF-8", 0, 0x0000); 1.5508 + converter.writeString(JSON.stringify(this.addon)); 1.5509 + } 1.5510 + finally { 1.5511 + converter.close(); 1.5512 + stream.close(); 1.5513 + } 1.5514 + 1.5515 + logger.debug("Staged install of " + this.addon.id + " from " + this.sourceURI.spec + " ready; waiting for restart."); 1.5516 + this.state = AddonManager.STATE_INSTALLED; 1.5517 + if (isUpgrade) { 1.5518 + delete this.existingAddon.pendingUpgrade; 1.5519 + this.existingAddon.pendingUpgrade = this.addon; 1.5520 + } 1.5521 + AddonManagerPrivate.callInstallListeners("onInstallEnded", 1.5522 + this.listeners, this.wrapper, 1.5523 + createWrapper(this.addon)); 1.5524 + } 1.5525 + else { 1.5526 + // The install is completed so it should be removed from the active list 1.5527 + XPIProvider.removeActiveInstall(this); 1.5528 + 1.5529 + // TODO We can probably reduce the number of DB operations going on here 1.5530 + // We probably also want to support rolling back failed upgrades etc. 1.5531 + // See bug 553015. 1.5532 + 1.5533 + // Deactivate and remove the old add-on as necessary 1.5534 + let reason = BOOTSTRAP_REASONS.ADDON_INSTALL; 1.5535 + if (this.existingAddon) { 1.5536 + if (Services.vc.compare(this.existingAddon.version, this.addon.version) < 0) 1.5537 + reason = BOOTSTRAP_REASONS.ADDON_UPGRADE; 1.5538 + else 1.5539 + reason = BOOTSTRAP_REASONS.ADDON_DOWNGRADE; 1.5540 + 1.5541 + if (this.existingAddon.bootstrap) { 1.5542 + let file = this.existingAddon._installLocation 1.5543 + .getLocationForID(this.existingAddon.id); 1.5544 + if (this.existingAddon.active) { 1.5545 + XPIProvider.callBootstrapMethod(this.existingAddon.id, 1.5546 + this.existingAddon.version, 1.5547 + this.existingAddon.type, file, 1.5548 + "shutdown", reason, 1.5549 + { newVersion: this.addon.version }); 1.5550 + } 1.5551 + 1.5552 + XPIProvider.callBootstrapMethod(this.existingAddon.id, 1.5553 + this.existingAddon.version, 1.5554 + this.existingAddon.type, file, 1.5555 + "uninstall", reason, 1.5556 + { newVersion: this.addon.version }); 1.5557 + XPIProvider.unloadBootstrapScope(this.existingAddon.id); 1.5558 + flushStartupCache(); 1.5559 + } 1.5560 + 1.5561 + if (!isUpgrade && this.existingAddon.active) { 1.5562 + XPIDatabase.updateAddonActive(this.existingAddon, false); 1.5563 + } 1.5564 + } 1.5565 + 1.5566 + // Install the new add-on into its final location 1.5567 + let existingAddonID = this.existingAddon ? this.existingAddon.id : null; 1.5568 + let file = this.installLocation.installAddon(this.addon.id, stagedAddon, 1.5569 + existingAddonID); 1.5570 + 1.5571 + // Update the metadata in the database 1.5572 + this.addon._sourceBundle = file; 1.5573 + this.addon._installLocation = this.installLocation; 1.5574 + let scanStarted = Date.now(); 1.5575 + let [, mTime, scanItems] = recursiveLastModifiedTime(file); 1.5576 + let scanTime = Date.now() - scanStarted; 1.5577 + this.addon.updateDate = mTime; 1.5578 + this.addon.visible = true; 1.5579 + if (isUpgrade) { 1.5580 + this.addon = XPIDatabase.updateAddonMetadata(this.existingAddon, this.addon, 1.5581 + file.persistentDescriptor); 1.5582 + } 1.5583 + else { 1.5584 + this.addon.installDate = this.addon.updateDate; 1.5585 + this.addon.active = (this.addon.visible && !isAddonDisabled(this.addon)) 1.5586 + this.addon = XPIDatabase.addAddonMetadata(this.addon, file.persistentDescriptor); 1.5587 + } 1.5588 + 1.5589 + let extraParams = {}; 1.5590 + if (this.existingAddon) { 1.5591 + extraParams.oldVersion = this.existingAddon.version; 1.5592 + } 1.5593 + 1.5594 + if (this.addon.bootstrap) { 1.5595 + XPIProvider.callBootstrapMethod(this.addon.id, this.addon.version, 1.5596 + this.addon.type, file, "install", 1.5597 + reason, extraParams); 1.5598 + } 1.5599 + 1.5600 + AddonManagerPrivate.callAddonListeners("onInstalled", 1.5601 + createWrapper(this.addon)); 1.5602 + 1.5603 + logger.debug("Install of " + this.sourceURI.spec + " completed."); 1.5604 + this.state = AddonManager.STATE_INSTALLED; 1.5605 + AddonManagerPrivate.callInstallListeners("onInstallEnded", 1.5606 + this.listeners, this.wrapper, 1.5607 + createWrapper(this.addon)); 1.5608 + 1.5609 + if (this.addon.bootstrap) { 1.5610 + if (this.addon.active) { 1.5611 + XPIProvider.callBootstrapMethod(this.addon.id, this.addon.version, 1.5612 + this.addon.type, file, "startup", 1.5613 + reason, extraParams); 1.5614 + } 1.5615 + else { 1.5616 + // XXX this makes it dangerous to do some things in onInstallEnded 1.5617 + // listeners because important cleanup hasn't been done yet 1.5618 + XPIProvider.unloadBootstrapScope(this.addon.id); 1.5619 + } 1.5620 + } 1.5621 + XPIProvider.setTelemetry(this.addon.id, "unpacked", installedUnpacked); 1.5622 + XPIProvider.setTelemetry(this.addon.id, "location", this.installLocation.name); 1.5623 + XPIProvider.setTelemetry(this.addon.id, "scan_MS", scanTime); 1.5624 + XPIProvider.setTelemetry(this.addon.id, "scan_items", scanItems); 1.5625 + let loc = this.addon.defaultLocale; 1.5626 + if (loc) { 1.5627 + XPIProvider.setTelemetry(this.addon.id, "name", loc.name); 1.5628 + XPIProvider.setTelemetry(this.addon.id, "creator", loc.creator); 1.5629 + } 1.5630 + } 1.5631 + }).bind(this)).then(null, (e) => { 1.5632 + logger.warn("Failed to install " + this.file.path + " from " + this.sourceURI.spec, e); 1.5633 + if (stagedAddon.exists()) 1.5634 + recursiveRemove(stagedAddon); 1.5635 + this.state = AddonManager.STATE_INSTALL_FAILED; 1.5636 + this.error = AddonManager.ERROR_FILE_ACCESS; 1.5637 + XPIProvider.removeActiveInstall(this); 1.5638 + AddonManagerPrivate.callAddonListeners("onOperationCancelled", 1.5639 + createWrapper(this.addon)); 1.5640 + AddonManagerPrivate.callInstallListeners("onInstallFailed", 1.5641 + this.listeners, 1.5642 + this.wrapper); 1.5643 + }).then(() => { 1.5644 + this.removeTemporaryFile(); 1.5645 + return this.installLocation.releaseStagingDir(); 1.5646 + }); 1.5647 + }, 1.5648 + 1.5649 + getInterface: function AI_getInterface(iid) { 1.5650 + if (iid.equals(Ci.nsIAuthPrompt2)) { 1.5651 + var factory = Cc["@mozilla.org/prompter;1"]. 1.5652 + getService(Ci.nsIPromptFactory); 1.5653 + return factory.getPrompt(this.window, Ci.nsIAuthPrompt); 1.5654 + } 1.5655 + else if (iid.equals(Ci.nsIChannelEventSink)) { 1.5656 + return this; 1.5657 + } 1.5658 + 1.5659 + return this.badCertHandler.getInterface(iid); 1.5660 + } 1.5661 +} 1.5662 + 1.5663 +/** 1.5664 + * Creates a new AddonInstall for an already staged install. Used when 1.5665 + * installing the staged install failed for some reason. 1.5666 + * 1.5667 + * @param aDir 1.5668 + * The directory holding the staged install 1.5669 + * @param aManifest 1.5670 + * The cached manifest for the install 1.5671 + */ 1.5672 +AddonInstall.createStagedInstall = function AI_createStagedInstall(aInstallLocation, aDir, aManifest) { 1.5673 + let url = Services.io.newFileURI(aDir); 1.5674 + 1.5675 + let install = new AddonInstall(aInstallLocation, aDir); 1.5676 + install.initStagedInstall(aManifest); 1.5677 +}; 1.5678 + 1.5679 +/** 1.5680 + * Creates a new AddonInstall to install an add-on from a local file. Installs 1.5681 + * always go into the profile install location. 1.5682 + * 1.5683 + * @param aCallback 1.5684 + * The callback to pass the new AddonInstall to 1.5685 + * @param aFile 1.5686 + * The file to install 1.5687 + */ 1.5688 +AddonInstall.createInstall = function AI_createInstall(aCallback, aFile) { 1.5689 + let location = XPIProvider.installLocationsByName[KEY_APP_PROFILE]; 1.5690 + let url = Services.io.newFileURI(aFile); 1.5691 + 1.5692 + try { 1.5693 + let install = new AddonInstall(location, url); 1.5694 + install.initLocalInstall(aCallback); 1.5695 + } 1.5696 + catch(e) { 1.5697 + logger.error("Error creating install", e); 1.5698 + makeSafe(aCallback)(null); 1.5699 + } 1.5700 +}; 1.5701 + 1.5702 +/** 1.5703 + * Creates a new AddonInstall to download and install a URL. 1.5704 + * 1.5705 + * @param aCallback 1.5706 + * The callback to pass the new AddonInstall to 1.5707 + * @param aUri 1.5708 + * The URI to download 1.5709 + * @param aHash 1.5710 + * A hash for the add-on 1.5711 + * @param aName 1.5712 + * A name for the add-on 1.5713 + * @param aIcons 1.5714 + * An icon URLs for the add-on 1.5715 + * @param aVersion 1.5716 + * A version for the add-on 1.5717 + * @param aLoadGroup 1.5718 + * An nsILoadGroup to associate the download with 1.5719 + */ 1.5720 +AddonInstall.createDownload = function AI_createDownload(aCallback, aUri, aHash, aName, aIcons, 1.5721 + aVersion, aLoadGroup) { 1.5722 + let location = XPIProvider.installLocationsByName[KEY_APP_PROFILE]; 1.5723 + let url = NetUtil.newURI(aUri); 1.5724 + 1.5725 + let install = new AddonInstall(location, url, aHash, null, null, aLoadGroup); 1.5726 + if (url instanceof Ci.nsIFileURL) 1.5727 + install.initLocalInstall(aCallback); 1.5728 + else 1.5729 + install.initAvailableDownload(aName, null, aIcons, aVersion, aCallback); 1.5730 +}; 1.5731 + 1.5732 +/** 1.5733 + * Creates a new AddonInstall for an update. 1.5734 + * 1.5735 + * @param aCallback 1.5736 + * The callback to pass the new AddonInstall to 1.5737 + * @param aAddon 1.5738 + * The add-on being updated 1.5739 + * @param aUpdate 1.5740 + * The metadata about the new version from the update manifest 1.5741 + */ 1.5742 +AddonInstall.createUpdate = function AI_createUpdate(aCallback, aAddon, aUpdate) { 1.5743 + let url = NetUtil.newURI(aUpdate.updateURL); 1.5744 + let releaseNotesURI = null; 1.5745 + try { 1.5746 + if (aUpdate.updateInfoURL) 1.5747 + releaseNotesURI = NetUtil.newURI(escapeAddonURI(aAddon, aUpdate.updateInfoURL)); 1.5748 + } 1.5749 + catch (e) { 1.5750 + // If the releaseNotesURI cannot be parsed then just ignore it. 1.5751 + } 1.5752 + 1.5753 + let install = new AddonInstall(aAddon._installLocation, url, 1.5754 + aUpdate.updateHash, releaseNotesURI, aAddon); 1.5755 + if (url instanceof Ci.nsIFileURL) { 1.5756 + install.initLocalInstall(aCallback); 1.5757 + } 1.5758 + else { 1.5759 + install.initAvailableDownload(aAddon.selectedLocale.name, aAddon.type, 1.5760 + aAddon.icons, aUpdate.version, aCallback); 1.5761 + } 1.5762 +}; 1.5763 + 1.5764 +/** 1.5765 + * Creates a wrapper for an AddonInstall that only exposes the public API 1.5766 + * 1.5767 + * @param install 1.5768 + * The AddonInstall to create a wrapper for 1.5769 + */ 1.5770 +function AddonInstallWrapper(aInstall) { 1.5771 +#ifdef MOZ_EM_DEBUG 1.5772 + this.__defineGetter__("__AddonInstallInternal__", function AIW_debugGetter() { 1.5773 + return aInstall; 1.5774 + }); 1.5775 +#endif 1.5776 + 1.5777 + ["name", "type", "version", "icons", "releaseNotesURI", "file", "state", "error", 1.5778 + "progress", "maxProgress", "certificate", "certName"].forEach(function(aProp) { 1.5779 + this.__defineGetter__(aProp, function AIW_propertyGetter() aInstall[aProp]); 1.5780 + }, this); 1.5781 + 1.5782 + this.__defineGetter__("iconURL", function AIW_iconURL() aInstall.icons[32]); 1.5783 + 1.5784 + this.__defineGetter__("existingAddon", function AIW_existingAddonGetter() { 1.5785 + return createWrapper(aInstall.existingAddon); 1.5786 + }); 1.5787 + this.__defineGetter__("addon", function AIW_addonGetter() createWrapper(aInstall.addon)); 1.5788 + this.__defineGetter__("sourceURI", function AIW_sourceURIGetter() aInstall.sourceURI); 1.5789 + 1.5790 + this.__defineGetter__("linkedInstalls", function AIW_linkedInstallsGetter() { 1.5791 + if (!aInstall.linkedInstalls) 1.5792 + return null; 1.5793 + return [i.wrapper for each (i in aInstall.linkedInstalls)]; 1.5794 + }); 1.5795 + 1.5796 + this.install = function AIW_install() { 1.5797 + aInstall.install(); 1.5798 + } 1.5799 + 1.5800 + this.cancel = function AIW_cancel() { 1.5801 + aInstall.cancel(); 1.5802 + } 1.5803 + 1.5804 + this.addListener = function AIW_addListener(listener) { 1.5805 + aInstall.addListener(listener); 1.5806 + } 1.5807 + 1.5808 + this.removeListener = function AIW_removeListener(listener) { 1.5809 + aInstall.removeListener(listener); 1.5810 + } 1.5811 +} 1.5812 + 1.5813 +AddonInstallWrapper.prototype = {}; 1.5814 + 1.5815 +/** 1.5816 + * Creates a new update checker. 1.5817 + * 1.5818 + * @param aAddon 1.5819 + * The add-on to check for updates 1.5820 + * @param aListener 1.5821 + * An UpdateListener to notify of updates 1.5822 + * @param aReason 1.5823 + * The reason for the update check 1.5824 + * @param aAppVersion 1.5825 + * An optional application version to check for updates for 1.5826 + * @param aPlatformVersion 1.5827 + * An optional platform version to check for updates for 1.5828 + * @throws if the aListener or aReason arguments are not valid 1.5829 + */ 1.5830 +function UpdateChecker(aAddon, aListener, aReason, aAppVersion, aPlatformVersion) { 1.5831 + if (!aListener || !aReason) 1.5832 + throw Cr.NS_ERROR_INVALID_ARG; 1.5833 + 1.5834 + Components.utils.import("resource://gre/modules/addons/AddonUpdateChecker.jsm"); 1.5835 + 1.5836 + this.addon = aAddon; 1.5837 + aAddon._updateCheck = this; 1.5838 + XPIProvider.doing(this); 1.5839 + this.listener = aListener; 1.5840 + this.appVersion = aAppVersion; 1.5841 + this.platformVersion = aPlatformVersion; 1.5842 + this.syncCompatibility = (aReason == AddonManager.UPDATE_WHEN_NEW_APP_INSTALLED); 1.5843 + 1.5844 + let updateURL = aAddon.updateURL; 1.5845 + if (!updateURL) { 1.5846 + if (aReason == AddonManager.UPDATE_WHEN_PERIODIC_UPDATE && 1.5847 + Services.prefs.getPrefType(PREF_EM_UPDATE_BACKGROUND_URL) == Services.prefs.PREF_STRING) { 1.5848 + updateURL = Services.prefs.getCharPref(PREF_EM_UPDATE_BACKGROUND_URL); 1.5849 + } else { 1.5850 + updateURL = Services.prefs.getCharPref(PREF_EM_UPDATE_URL); 1.5851 + } 1.5852 + } 1.5853 + 1.5854 + const UPDATE_TYPE_COMPATIBILITY = 32; 1.5855 + const UPDATE_TYPE_NEWVERSION = 64; 1.5856 + 1.5857 + aReason |= UPDATE_TYPE_COMPATIBILITY; 1.5858 + if ("onUpdateAvailable" in this.listener) 1.5859 + aReason |= UPDATE_TYPE_NEWVERSION; 1.5860 + 1.5861 + let url = escapeAddonURI(aAddon, updateURL, aReason, aAppVersion); 1.5862 + this._parser = AddonUpdateChecker.checkForUpdates(aAddon.id, aAddon.updateKey, 1.5863 + url, this); 1.5864 +} 1.5865 + 1.5866 +UpdateChecker.prototype = { 1.5867 + addon: null, 1.5868 + listener: null, 1.5869 + appVersion: null, 1.5870 + platformVersion: null, 1.5871 + syncCompatibility: null, 1.5872 + 1.5873 + /** 1.5874 + * Calls a method on the listener passing any number of arguments and 1.5875 + * consuming any exceptions. 1.5876 + * 1.5877 + * @param aMethod 1.5878 + * The method to call on the listener 1.5879 + */ 1.5880 + callListener: function UC_callListener(aMethod, ...aArgs) { 1.5881 + if (!(aMethod in this.listener)) 1.5882 + return; 1.5883 + 1.5884 + try { 1.5885 + this.listener[aMethod].apply(this.listener, aArgs); 1.5886 + } 1.5887 + catch (e) { 1.5888 + logger.warn("Exception calling UpdateListener method " + aMethod, e); 1.5889 + } 1.5890 + }, 1.5891 + 1.5892 + /** 1.5893 + * Called when AddonUpdateChecker completes the update check 1.5894 + * 1.5895 + * @param updates 1.5896 + * The list of update details for the add-on 1.5897 + */ 1.5898 + onUpdateCheckComplete: function UC_onUpdateCheckComplete(aUpdates) { 1.5899 + XPIProvider.done(this.addon._updateCheck); 1.5900 + this.addon._updateCheck = null; 1.5901 + let AUC = AddonUpdateChecker; 1.5902 + 1.5903 + let ignoreMaxVersion = false; 1.5904 + let ignoreStrictCompat = false; 1.5905 + if (!AddonManager.checkCompatibility) { 1.5906 + ignoreMaxVersion = true; 1.5907 + ignoreStrictCompat = true; 1.5908 + } else if (this.addon.type in COMPATIBLE_BY_DEFAULT_TYPES && 1.5909 + !AddonManager.strictCompatibility && 1.5910 + !this.addon.strictCompatibility && 1.5911 + !this.addon.hasBinaryComponents) { 1.5912 + ignoreMaxVersion = true; 1.5913 + } 1.5914 + 1.5915 + // Always apply any compatibility update for the current version 1.5916 + let compatUpdate = AUC.getCompatibilityUpdate(aUpdates, this.addon.version, 1.5917 + this.syncCompatibility, 1.5918 + null, null, 1.5919 + ignoreMaxVersion, 1.5920 + ignoreStrictCompat); 1.5921 + // Apply the compatibility update to the database 1.5922 + if (compatUpdate) 1.5923 + this.addon.applyCompatibilityUpdate(compatUpdate, this.syncCompatibility); 1.5924 + 1.5925 + // If the request is for an application or platform version that is 1.5926 + // different to the current application or platform version then look for a 1.5927 + // compatibility update for those versions. 1.5928 + if ((this.appVersion && 1.5929 + Services.vc.compare(this.appVersion, Services.appinfo.version) != 0) || 1.5930 + (this.platformVersion && 1.5931 + Services.vc.compare(this.platformVersion, Services.appinfo.platformVersion) != 0)) { 1.5932 + compatUpdate = AUC.getCompatibilityUpdate(aUpdates, this.addon.version, 1.5933 + false, this.appVersion, 1.5934 + this.platformVersion, 1.5935 + ignoreMaxVersion, 1.5936 + ignoreStrictCompat); 1.5937 + } 1.5938 + 1.5939 + if (compatUpdate) 1.5940 + this.callListener("onCompatibilityUpdateAvailable", createWrapper(this.addon)); 1.5941 + else 1.5942 + this.callListener("onNoCompatibilityUpdateAvailable", createWrapper(this.addon)); 1.5943 + 1.5944 + function sendUpdateAvailableMessages(aSelf, aInstall) { 1.5945 + if (aInstall) { 1.5946 + aSelf.callListener("onUpdateAvailable", createWrapper(aSelf.addon), 1.5947 + aInstall.wrapper); 1.5948 + } 1.5949 + else { 1.5950 + aSelf.callListener("onNoUpdateAvailable", createWrapper(aSelf.addon)); 1.5951 + } 1.5952 + aSelf.callListener("onUpdateFinished", createWrapper(aSelf.addon), 1.5953 + AddonManager.UPDATE_STATUS_NO_ERROR); 1.5954 + } 1.5955 + 1.5956 + let compatOverrides = AddonManager.strictCompatibility ? 1.5957 + null : 1.5958 + this.addon.compatibilityOverrides; 1.5959 + 1.5960 + let update = AUC.getNewestCompatibleUpdate(aUpdates, 1.5961 + this.appVersion, 1.5962 + this.platformVersion, 1.5963 + ignoreMaxVersion, 1.5964 + ignoreStrictCompat, 1.5965 + compatOverrides); 1.5966 + 1.5967 + if (update && Services.vc.compare(this.addon.version, update.version) < 0) { 1.5968 + for (let currentInstall of XPIProvider.installs) { 1.5969 + // Skip installs that don't match the available update 1.5970 + if (currentInstall.existingAddon != this.addon || 1.5971 + currentInstall.version != update.version) 1.5972 + continue; 1.5973 + 1.5974 + // If the existing install has not yet started downloading then send an 1.5975 + // available update notification. If it is already downloading then 1.5976 + // don't send any available update notification 1.5977 + if (currentInstall.state == AddonManager.STATE_AVAILABLE) { 1.5978 + logger.debug("Found an existing AddonInstall for " + this.addon.id); 1.5979 + sendUpdateAvailableMessages(this, currentInstall); 1.5980 + } 1.5981 + else 1.5982 + sendUpdateAvailableMessages(this, null); 1.5983 + return; 1.5984 + } 1.5985 + 1.5986 + let self = this; 1.5987 + AddonInstall.createUpdate(function onUpdateCheckComplete_createUpdate(aInstall) { 1.5988 + sendUpdateAvailableMessages(self, aInstall); 1.5989 + }, this.addon, update); 1.5990 + } 1.5991 + else { 1.5992 + sendUpdateAvailableMessages(this, null); 1.5993 + } 1.5994 + }, 1.5995 + 1.5996 + /** 1.5997 + * Called when AddonUpdateChecker fails the update check 1.5998 + * 1.5999 + * @param aError 1.6000 + * An error status 1.6001 + */ 1.6002 + onUpdateCheckError: function UC_onUpdateCheckError(aError) { 1.6003 + XPIProvider.done(this.addon._updateCheck); 1.6004 + this.addon._updateCheck = null; 1.6005 + this.callListener("onNoCompatibilityUpdateAvailable", createWrapper(this.addon)); 1.6006 + this.callListener("onNoUpdateAvailable", createWrapper(this.addon)); 1.6007 + this.callListener("onUpdateFinished", createWrapper(this.addon), aError); 1.6008 + }, 1.6009 + 1.6010 + /** 1.6011 + * Called to cancel an in-progress update check 1.6012 + */ 1.6013 + cancel: function UC_cancel() { 1.6014 + let parser = this._parser; 1.6015 + if (parser) { 1.6016 + this._parser = null; 1.6017 + // This will call back to onUpdateCheckError with a CANCELLED error 1.6018 + parser.cancel(); 1.6019 + } 1.6020 + } 1.6021 +}; 1.6022 + 1.6023 +/** 1.6024 + * The AddonInternal is an internal only representation of add-ons. It may 1.6025 + * have come from the database (see DBAddonInternal in XPIProviderUtils.jsm) 1.6026 + * or an install manifest. 1.6027 + */ 1.6028 +function AddonInternal() { 1.6029 +} 1.6030 + 1.6031 +AddonInternal.prototype = { 1.6032 + _selectedLocale: null, 1.6033 + active: false, 1.6034 + visible: false, 1.6035 + userDisabled: false, 1.6036 + appDisabled: false, 1.6037 + softDisabled: false, 1.6038 + sourceURI: null, 1.6039 + releaseNotesURI: null, 1.6040 + foreignInstall: false, 1.6041 + 1.6042 + get selectedLocale() { 1.6043 + if (this._selectedLocale) 1.6044 + return this._selectedLocale; 1.6045 + let locale = findClosestLocale(this.locales); 1.6046 + this._selectedLocale = locale ? locale : this.defaultLocale; 1.6047 + return this._selectedLocale; 1.6048 + }, 1.6049 + 1.6050 + get providesUpdatesSecurely() { 1.6051 + return !!(this.updateKey || !this.updateURL || 1.6052 + this.updateURL.substring(0, 6) == "https:"); 1.6053 + }, 1.6054 + 1.6055 + get isCompatible() { 1.6056 + return this.isCompatibleWith(); 1.6057 + }, 1.6058 + 1.6059 + get isPlatformCompatible() { 1.6060 + if (this.targetPlatforms.length == 0) 1.6061 + return true; 1.6062 + 1.6063 + let matchedOS = false; 1.6064 + 1.6065 + // If any targetPlatform matches the OS and contains an ABI then we will 1.6066 + // only match a targetPlatform that contains both the current OS and ABI 1.6067 + let needsABI = false; 1.6068 + 1.6069 + // Some platforms do not specify an ABI, test against null in that case. 1.6070 + let abi = null; 1.6071 + try { 1.6072 + abi = Services.appinfo.XPCOMABI; 1.6073 + } 1.6074 + catch (e) { } 1.6075 + 1.6076 + for (let platform of this.targetPlatforms) { 1.6077 + if (platform.os == Services.appinfo.OS) { 1.6078 + if (platform.abi) { 1.6079 + needsABI = true; 1.6080 + if (platform.abi === abi) 1.6081 + return true; 1.6082 + } 1.6083 + else { 1.6084 + matchedOS = true; 1.6085 + } 1.6086 + } 1.6087 + } 1.6088 + 1.6089 + return matchedOS && !needsABI; 1.6090 + }, 1.6091 + 1.6092 + isCompatibleWith: function AddonInternal_isCompatibleWith(aAppVersion, aPlatformVersion) { 1.6093 + // Experiments are installed through an external mechanism that 1.6094 + // limits target audience to compatible clients. We trust it knows what 1.6095 + // it's doing and skip compatibility checks. 1.6096 + // 1.6097 + // This decision does forfeit defense in depth. If the experiments system 1.6098 + // is ever wrong about targeting an add-on to a specific application 1.6099 + // or platform, the client will likely see errors. 1.6100 + if (this.type == "experiment") { 1.6101 + return true; 1.6102 + } 1.6103 + 1.6104 + let app = this.matchingTargetApplication; 1.6105 + if (!app) 1.6106 + return false; 1.6107 + 1.6108 + if (!aAppVersion) 1.6109 + aAppVersion = Services.appinfo.version; 1.6110 + if (!aPlatformVersion) 1.6111 + aPlatformVersion = Services.appinfo.platformVersion; 1.6112 + 1.6113 + let version; 1.6114 + if (app.id == Services.appinfo.ID) 1.6115 + version = aAppVersion; 1.6116 + else if (app.id == TOOLKIT_ID) 1.6117 + version = aPlatformVersion 1.6118 + 1.6119 + // Only extensions and dictionaries can be compatible by default; themes 1.6120 + // and language packs always use strict compatibility checking. 1.6121 + if (this.type in COMPATIBLE_BY_DEFAULT_TYPES && 1.6122 + !AddonManager.strictCompatibility && !this.strictCompatibility && 1.6123 + !this.hasBinaryComponents) { 1.6124 + 1.6125 + // The repository can specify compatibility overrides. 1.6126 + // Note: For now, only blacklisting is supported by overrides. 1.6127 + if (this._repositoryAddon && 1.6128 + this._repositoryAddon.compatibilityOverrides) { 1.6129 + let overrides = this._repositoryAddon.compatibilityOverrides; 1.6130 + let override = AddonRepository.findMatchingCompatOverride(this.version, 1.6131 + overrides); 1.6132 + if (override && override.type == "incompatible") 1.6133 + return false; 1.6134 + } 1.6135 + 1.6136 + // Extremely old extensions should not be compatible by default. 1.6137 + let minCompatVersion; 1.6138 + if (app.id == Services.appinfo.ID) 1.6139 + minCompatVersion = XPIProvider.minCompatibleAppVersion; 1.6140 + else if (app.id == TOOLKIT_ID) 1.6141 + minCompatVersion = XPIProvider.minCompatiblePlatformVersion; 1.6142 + 1.6143 + if (minCompatVersion && 1.6144 + Services.vc.compare(minCompatVersion, app.maxVersion) > 0) 1.6145 + return false; 1.6146 + 1.6147 + return Services.vc.compare(version, app.minVersion) >= 0; 1.6148 + } 1.6149 + 1.6150 + return (Services.vc.compare(version, app.minVersion) >= 0) && 1.6151 + (Services.vc.compare(version, app.maxVersion) <= 0) 1.6152 + }, 1.6153 + 1.6154 + get matchingTargetApplication() { 1.6155 + let app = null; 1.6156 + for (let targetApp of this.targetApplications) { 1.6157 + if (targetApp.id == Services.appinfo.ID) 1.6158 + return targetApp; 1.6159 + if (targetApp.id == TOOLKIT_ID) 1.6160 + app = targetApp; 1.6161 + } 1.6162 + return app; 1.6163 + }, 1.6164 + 1.6165 + get blocklistState() { 1.6166 + let staticItem = findMatchingStaticBlocklistItem(this); 1.6167 + if (staticItem) 1.6168 + return staticItem.level; 1.6169 + 1.6170 + let bs = Cc["@mozilla.org/extensions/blocklist;1"]. 1.6171 + getService(Ci.nsIBlocklistService); 1.6172 + return bs.getAddonBlocklistState(createWrapper(this)); 1.6173 + }, 1.6174 + 1.6175 + get blocklistURL() { 1.6176 + let staticItem = findMatchingStaticBlocklistItem(this); 1.6177 + if (staticItem) { 1.6178 + let url = Services.urlFormatter.formatURLPref("extensions.blocklist.itemURL"); 1.6179 + return url.replace(/%blockID%/g, staticItem.blockID); 1.6180 + } 1.6181 + 1.6182 + let bs = Cc["@mozilla.org/extensions/blocklist;1"]. 1.6183 + getService(Ci.nsIBlocklistService); 1.6184 + return bs.getAddonBlocklistURL(createWrapper(this)); 1.6185 + }, 1.6186 + 1.6187 + applyCompatibilityUpdate: function AddonInternal_applyCompatibilityUpdate(aUpdate, aSyncCompatibility) { 1.6188 + this.targetApplications.forEach(function(aTargetApp) { 1.6189 + aUpdate.targetApplications.forEach(function(aUpdateTarget) { 1.6190 + if (aTargetApp.id == aUpdateTarget.id && (aSyncCompatibility || 1.6191 + Services.vc.compare(aTargetApp.maxVersion, aUpdateTarget.maxVersion) < 0)) { 1.6192 + aTargetApp.minVersion = aUpdateTarget.minVersion; 1.6193 + aTargetApp.maxVersion = aUpdateTarget.maxVersion; 1.6194 + } 1.6195 + }); 1.6196 + }); 1.6197 + this.appDisabled = !isUsableAddon(this); 1.6198 + }, 1.6199 + 1.6200 + /** 1.6201 + * toJSON is called by JSON.stringify in order to create a filtered version 1.6202 + * of this object to be serialized to a JSON file. A new object is returned 1.6203 + * with copies of all non-private properties. Functions, getters and setters 1.6204 + * are not copied. 1.6205 + * 1.6206 + * @param aKey 1.6207 + * The key that this object is being serialized as in the JSON. 1.6208 + * Unused here since this is always the main object serialized 1.6209 + * 1.6210 + * @return an object containing copies of the properties of this object 1.6211 + * ignoring private properties, functions, getters and setters 1.6212 + */ 1.6213 + toJSON: function AddonInternal_toJSON(aKey) { 1.6214 + let obj = {}; 1.6215 + for (let prop in this) { 1.6216 + // Ignore private properties 1.6217 + if (prop.substring(0, 1) == "_") 1.6218 + continue; 1.6219 + 1.6220 + // Ignore getters 1.6221 + if (this.__lookupGetter__(prop)) 1.6222 + continue; 1.6223 + 1.6224 + // Ignore setters 1.6225 + if (this.__lookupSetter__(prop)) 1.6226 + continue; 1.6227 + 1.6228 + // Ignore functions 1.6229 + if (typeof this[prop] == "function") 1.6230 + continue; 1.6231 + 1.6232 + obj[prop] = this[prop]; 1.6233 + } 1.6234 + 1.6235 + return obj; 1.6236 + }, 1.6237 + 1.6238 + /** 1.6239 + * When an add-on install is pending its metadata will be cached in a file. 1.6240 + * This method reads particular properties of that metadata that may be newer 1.6241 + * than that in the install manifest, like compatibility information. 1.6242 + * 1.6243 + * @param aObj 1.6244 + * A JS object containing the cached metadata 1.6245 + */ 1.6246 + importMetadata: function AddonInternal_importMetaData(aObj) { 1.6247 + PENDING_INSTALL_METADATA.forEach(function(aProp) { 1.6248 + if (!(aProp in aObj)) 1.6249 + return; 1.6250 + 1.6251 + this[aProp] = aObj[aProp]; 1.6252 + }, this); 1.6253 + 1.6254 + // Compatibility info may have changed so update appDisabled 1.6255 + this.appDisabled = !isUsableAddon(this); 1.6256 + } 1.6257 +}; 1.6258 + 1.6259 +/** 1.6260 + * Creates an AddonWrapper for an AddonInternal. 1.6261 + * 1.6262 + * @param addon 1.6263 + * The AddonInternal to wrap 1.6264 + * @return an AddonWrapper or null if addon was null 1.6265 + */ 1.6266 +function createWrapper(aAddon) { 1.6267 + if (!aAddon) 1.6268 + return null; 1.6269 + if (!aAddon._wrapper) { 1.6270 + aAddon._hasResourceCache = new Map(); 1.6271 + aAddon._wrapper = new AddonWrapper(aAddon); 1.6272 + } 1.6273 + return aAddon._wrapper; 1.6274 +} 1.6275 + 1.6276 +/** 1.6277 + * The AddonWrapper wraps an Addon to provide the data visible to consumers of 1.6278 + * the public API. 1.6279 + */ 1.6280 +function AddonWrapper(aAddon) { 1.6281 +#ifdef MOZ_EM_DEBUG 1.6282 + this.__defineGetter__("__AddonInternal__", function AW_debugGetter() { 1.6283 + return aAddon; 1.6284 + }); 1.6285 +#endif 1.6286 + 1.6287 + function chooseValue(aObj, aProp) { 1.6288 + let repositoryAddon = aAddon._repositoryAddon; 1.6289 + let objValue = aObj[aProp]; 1.6290 + 1.6291 + if (repositoryAddon && (aProp in repositoryAddon) && 1.6292 + (objValue === undefined || objValue === null)) { 1.6293 + return [repositoryAddon[aProp], true]; 1.6294 + } 1.6295 + 1.6296 + return [objValue, false]; 1.6297 + } 1.6298 + 1.6299 + ["id", "syncGUID", "version", "type", "isCompatible", "isPlatformCompatible", 1.6300 + "providesUpdatesSecurely", "blocklistState", "blocklistURL", "appDisabled", 1.6301 + "softDisabled", "skinnable", "size", "foreignInstall", "hasBinaryComponents", 1.6302 + "strictCompatibility", "compatibilityOverrides", "updateURL"].forEach(function(aProp) { 1.6303 + this.__defineGetter__(aProp, function AddonWrapper_propertyGetter() aAddon[aProp]); 1.6304 + }, this); 1.6305 + 1.6306 + ["fullDescription", "developerComments", "eula", "supportURL", 1.6307 + "contributionURL", "contributionAmount", "averageRating", "reviewCount", 1.6308 + "reviewURL", "totalDownloads", "weeklyDownloads", "dailyUsers", 1.6309 + "repositoryStatus"].forEach(function(aProp) { 1.6310 + this.__defineGetter__(aProp, function AddonWrapper_repoPropertyGetter() { 1.6311 + if (aAddon._repositoryAddon) 1.6312 + return aAddon._repositoryAddon[aProp]; 1.6313 + 1.6314 + return null; 1.6315 + }); 1.6316 + }, this); 1.6317 + 1.6318 + this.__defineGetter__("aboutURL", function AddonWrapper_aboutURLGetter() { 1.6319 + return this.isActive ? aAddon["aboutURL"] : null; 1.6320 + }); 1.6321 + 1.6322 + ["installDate", "updateDate"].forEach(function(aProp) { 1.6323 + this.__defineGetter__(aProp, function AddonWrapper_datePropertyGetter() new Date(aAddon[aProp])); 1.6324 + }, this); 1.6325 + 1.6326 + ["sourceURI", "releaseNotesURI"].forEach(function(aProp) { 1.6327 + this.__defineGetter__(aProp, function AddonWrapper_URIPropertyGetter() { 1.6328 + let [target, fromRepo] = chooseValue(aAddon, aProp); 1.6329 + if (!target) 1.6330 + return null; 1.6331 + if (fromRepo) 1.6332 + return target; 1.6333 + return NetUtil.newURI(target); 1.6334 + }); 1.6335 + }, this); 1.6336 + 1.6337 + this.__defineGetter__("optionsURL", function AddonWrapper_optionsURLGetter() { 1.6338 + if (this.isActive && aAddon.optionsURL) 1.6339 + return aAddon.optionsURL; 1.6340 + 1.6341 + if (this.isActive && this.hasResource("options.xul")) 1.6342 + return this.getResourceURI("options.xul").spec; 1.6343 + 1.6344 + return null; 1.6345 + }, this); 1.6346 + 1.6347 + this.__defineGetter__("optionsType", function AddonWrapper_optionsTypeGetter() { 1.6348 + if (!this.isActive) 1.6349 + return null; 1.6350 + 1.6351 + let hasOptionsXUL = this.hasResource("options.xul"); 1.6352 + let hasOptionsURL = !!this.optionsURL; 1.6353 + 1.6354 + if (aAddon.optionsType) { 1.6355 + switch (parseInt(aAddon.optionsType, 10)) { 1.6356 + case AddonManager.OPTIONS_TYPE_DIALOG: 1.6357 + case AddonManager.OPTIONS_TYPE_TAB: 1.6358 + return hasOptionsURL ? aAddon.optionsType : null; 1.6359 + case AddonManager.OPTIONS_TYPE_INLINE: 1.6360 + case AddonManager.OPTIONS_TYPE_INLINE_INFO: 1.6361 + return (hasOptionsXUL || hasOptionsURL) ? aAddon.optionsType : null; 1.6362 + } 1.6363 + return null; 1.6364 + } 1.6365 + 1.6366 + if (hasOptionsXUL) 1.6367 + return AddonManager.OPTIONS_TYPE_INLINE; 1.6368 + 1.6369 + if (hasOptionsURL) 1.6370 + return AddonManager.OPTIONS_TYPE_DIALOG; 1.6371 + 1.6372 + return null; 1.6373 + }, this); 1.6374 + 1.6375 + this.__defineGetter__("iconURL", function AddonWrapper_iconURLGetter() { 1.6376 + return this.icons[32]; 1.6377 + }, this); 1.6378 + 1.6379 + this.__defineGetter__("icon64URL", function AddonWrapper_icon64URLGetter() { 1.6380 + return this.icons[64]; 1.6381 + }, this); 1.6382 + 1.6383 + this.__defineGetter__("icons", function AddonWrapper_iconsGetter() { 1.6384 + let icons = {}; 1.6385 + if (aAddon._repositoryAddon) { 1.6386 + for (let size in aAddon._repositoryAddon.icons) { 1.6387 + icons[size] = aAddon._repositoryAddon.icons[size]; 1.6388 + } 1.6389 + } 1.6390 + if (this.isActive && aAddon.iconURL) { 1.6391 + icons[32] = aAddon.iconURL; 1.6392 + } else if (this.hasResource("icon.png")) { 1.6393 + icons[32] = this.getResourceURI("icon.png").spec; 1.6394 + } 1.6395 + if (this.isActive && aAddon.icon64URL) { 1.6396 + icons[64] = aAddon.icon64URL; 1.6397 + } else if (this.hasResource("icon64.png")) { 1.6398 + icons[64] = this.getResourceURI("icon64.png").spec; 1.6399 + } 1.6400 + Object.freeze(icons); 1.6401 + return icons; 1.6402 + }, this); 1.6403 + 1.6404 + PROP_LOCALE_SINGLE.forEach(function(aProp) { 1.6405 + this.__defineGetter__(aProp, function AddonWrapper_singleLocaleGetter() { 1.6406 + // Override XPI creator if repository creator is defined 1.6407 + if (aProp == "creator" && 1.6408 + aAddon._repositoryAddon && aAddon._repositoryAddon.creator) { 1.6409 + return aAddon._repositoryAddon.creator; 1.6410 + } 1.6411 + 1.6412 + let result = null; 1.6413 + 1.6414 + if (aAddon.active) { 1.6415 + try { 1.6416 + let pref = PREF_EM_EXTENSION_FORMAT + aAddon.id + "." + aProp; 1.6417 + let value = Services.prefs.getComplexValue(pref, 1.6418 + Ci.nsIPrefLocalizedString); 1.6419 + if (value.data) 1.6420 + result = value.data; 1.6421 + } 1.6422 + catch (e) { 1.6423 + } 1.6424 + } 1.6425 + 1.6426 + if (result == null) 1.6427 + [result, ] = chooseValue(aAddon.selectedLocale, aProp); 1.6428 + 1.6429 + if (aProp == "creator") 1.6430 + return result ? new AddonManagerPrivate.AddonAuthor(result) : null; 1.6431 + 1.6432 + return result; 1.6433 + }); 1.6434 + }, this); 1.6435 + 1.6436 + PROP_LOCALE_MULTI.forEach(function(aProp) { 1.6437 + this.__defineGetter__(aProp, function AddonWrapper_multiLocaleGetter() { 1.6438 + let results = null; 1.6439 + let usedRepository = false; 1.6440 + 1.6441 + if (aAddon.active) { 1.6442 + let pref = PREF_EM_EXTENSION_FORMAT + aAddon.id + "." + 1.6443 + aProp.substring(0, aProp.length - 1); 1.6444 + let list = Services.prefs.getChildList(pref, {}); 1.6445 + if (list.length > 0) { 1.6446 + list.sort(); 1.6447 + results = []; 1.6448 + list.forEach(function(aPref) { 1.6449 + let value = Services.prefs.getComplexValue(aPref, 1.6450 + Ci.nsIPrefLocalizedString); 1.6451 + if (value.data) 1.6452 + results.push(value.data); 1.6453 + }); 1.6454 + } 1.6455 + } 1.6456 + 1.6457 + if (results == null) 1.6458 + [results, usedRepository] = chooseValue(aAddon.selectedLocale, aProp); 1.6459 + 1.6460 + if (results && !usedRepository) { 1.6461 + results = results.map(function mapResult(aResult) { 1.6462 + return new AddonManagerPrivate.AddonAuthor(aResult); 1.6463 + }); 1.6464 + } 1.6465 + 1.6466 + return results; 1.6467 + }); 1.6468 + }, this); 1.6469 + 1.6470 + this.__defineGetter__("screenshots", function AddonWrapper_screenshotsGetter() { 1.6471 + let repositoryAddon = aAddon._repositoryAddon; 1.6472 + if (repositoryAddon && ("screenshots" in repositoryAddon)) { 1.6473 + let repositoryScreenshots = repositoryAddon.screenshots; 1.6474 + if (repositoryScreenshots && repositoryScreenshots.length > 0) 1.6475 + return repositoryScreenshots; 1.6476 + } 1.6477 + 1.6478 + if (aAddon.type == "theme" && this.hasResource("preview.png")) { 1.6479 + let url = this.getResourceURI("preview.png").spec; 1.6480 + return [new AddonManagerPrivate.AddonScreenshot(url)]; 1.6481 + } 1.6482 + 1.6483 + return null; 1.6484 + }); 1.6485 + 1.6486 + this.__defineGetter__("applyBackgroundUpdates", function AddonWrapper_applyBackgroundUpdatesGetter() { 1.6487 + return aAddon.applyBackgroundUpdates; 1.6488 + }); 1.6489 + this.__defineSetter__("applyBackgroundUpdates", function AddonWrapper_applyBackgroundUpdatesSetter(val) { 1.6490 + if (this.type == "experiment") { 1.6491 + logger.warn("Setting applyBackgroundUpdates on an experiment is not supported."); 1.6492 + return; 1.6493 + } 1.6494 + 1.6495 + if (val != AddonManager.AUTOUPDATE_DEFAULT && 1.6496 + val != AddonManager.AUTOUPDATE_DISABLE && 1.6497 + val != AddonManager.AUTOUPDATE_ENABLE) { 1.6498 + val = val ? AddonManager.AUTOUPDATE_DEFAULT : 1.6499 + AddonManager.AUTOUPDATE_DISABLE; 1.6500 + } 1.6501 + 1.6502 + if (val == aAddon.applyBackgroundUpdates) 1.6503 + return val; 1.6504 + 1.6505 + XPIDatabase.setAddonProperties(aAddon, { 1.6506 + applyBackgroundUpdates: val 1.6507 + }); 1.6508 + AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, ["applyBackgroundUpdates"]); 1.6509 + 1.6510 + return val; 1.6511 + }); 1.6512 + 1.6513 + this.__defineSetter__("syncGUID", function AddonWrapper_syncGUIDGetter(val) { 1.6514 + if (aAddon.syncGUID == val) 1.6515 + return val; 1.6516 + 1.6517 + if (aAddon.inDatabase) 1.6518 + XPIDatabase.setAddonSyncGUID(aAddon, val); 1.6519 + 1.6520 + aAddon.syncGUID = val; 1.6521 + 1.6522 + return val; 1.6523 + }); 1.6524 + 1.6525 + this.__defineGetter__("install", function AddonWrapper_installGetter() { 1.6526 + if (!("_install" in aAddon) || !aAddon._install) 1.6527 + return null; 1.6528 + return aAddon._install.wrapper; 1.6529 + }); 1.6530 + 1.6531 + this.__defineGetter__("pendingUpgrade", function AddonWrapper_pendingUpgradeGetter() { 1.6532 + return createWrapper(aAddon.pendingUpgrade); 1.6533 + }); 1.6534 + 1.6535 + this.__defineGetter__("scope", function AddonWrapper_scopeGetter() { 1.6536 + if (aAddon._installLocation) 1.6537 + return aAddon._installLocation.scope; 1.6538 + 1.6539 + return AddonManager.SCOPE_PROFILE; 1.6540 + }); 1.6541 + 1.6542 + this.__defineGetter__("pendingOperations", function AddonWrapper_pendingOperationsGetter() { 1.6543 + let pending = 0; 1.6544 + if (!(aAddon.inDatabase)) { 1.6545 + // Add-on is pending install if there is no associated install (shouldn't 1.6546 + // happen here) or if the install is in the process of or has successfully 1.6547 + // completed the install. If an add-on is pending install then we ignore 1.6548 + // any other pending operations. 1.6549 + if (!aAddon._install || aAddon._install.state == AddonManager.STATE_INSTALLING || 1.6550 + aAddon._install.state == AddonManager.STATE_INSTALLED) 1.6551 + return AddonManager.PENDING_INSTALL; 1.6552 + } 1.6553 + else if (aAddon.pendingUninstall) { 1.6554 + // If an add-on is pending uninstall then we ignore any other pending 1.6555 + // operations 1.6556 + return AddonManager.PENDING_UNINSTALL; 1.6557 + } 1.6558 + 1.6559 + if (aAddon.active && isAddonDisabled(aAddon)) 1.6560 + pending |= AddonManager.PENDING_DISABLE; 1.6561 + else if (!aAddon.active && !isAddonDisabled(aAddon)) 1.6562 + pending |= AddonManager.PENDING_ENABLE; 1.6563 + 1.6564 + if (aAddon.pendingUpgrade) 1.6565 + pending |= AddonManager.PENDING_UPGRADE; 1.6566 + 1.6567 + return pending; 1.6568 + }); 1.6569 + 1.6570 + this.__defineGetter__("operationsRequiringRestart", function AddonWrapper_operationsRequiringRestartGetter() { 1.6571 + let ops = 0; 1.6572 + if (XPIProvider.installRequiresRestart(aAddon)) 1.6573 + ops |= AddonManager.OP_NEEDS_RESTART_INSTALL; 1.6574 + if (XPIProvider.uninstallRequiresRestart(aAddon)) 1.6575 + ops |= AddonManager.OP_NEEDS_RESTART_UNINSTALL; 1.6576 + if (XPIProvider.enableRequiresRestart(aAddon)) 1.6577 + ops |= AddonManager.OP_NEEDS_RESTART_ENABLE; 1.6578 + if (XPIProvider.disableRequiresRestart(aAddon)) 1.6579 + ops |= AddonManager.OP_NEEDS_RESTART_DISABLE; 1.6580 + 1.6581 + return ops; 1.6582 + }); 1.6583 + 1.6584 + this.__defineGetter__("isDebuggable", function AddonWrapper_isDebuggable() { 1.6585 + return this.isActive && aAddon.bootstrap; 1.6586 + }); 1.6587 + 1.6588 + this.__defineGetter__("permissions", function AddonWrapper_permisionsGetter() { 1.6589 + let permissions = 0; 1.6590 + 1.6591 + // Add-ons that aren't installed cannot be modified in any way 1.6592 + if (!(aAddon.inDatabase)) 1.6593 + return permissions; 1.6594 + 1.6595 + // Experiments can only be uninstalled. An uninstall reflects the user 1.6596 + // intent of "disable this experiment." This is partially managed by the 1.6597 + // experiments manager. 1.6598 + if (aAddon.type == "experiment") { 1.6599 + return AddonManager.PERM_CAN_UNINSTALL; 1.6600 + } 1.6601 + 1.6602 + if (!aAddon.appDisabled) { 1.6603 + if (this.userDisabled) { 1.6604 + permissions |= AddonManager.PERM_CAN_ENABLE; 1.6605 + } 1.6606 + else if (aAddon.type != "theme") { 1.6607 + permissions |= AddonManager.PERM_CAN_DISABLE; 1.6608 + } 1.6609 + } 1.6610 + 1.6611 + // Add-ons that are in locked install locations, or are pending uninstall 1.6612 + // cannot be upgraded or uninstalled 1.6613 + if (!aAddon._installLocation.locked && !aAddon.pendingUninstall) { 1.6614 + // Add-ons that are installed by a file link cannot be upgraded 1.6615 + if (!aAddon._installLocation.isLinkedAddon(aAddon.id)) { 1.6616 + permissions |= AddonManager.PERM_CAN_UPGRADE; 1.6617 + } 1.6618 + 1.6619 + permissions |= AddonManager.PERM_CAN_UNINSTALL; 1.6620 + } 1.6621 + 1.6622 + return permissions; 1.6623 + }); 1.6624 + 1.6625 + this.__defineGetter__("isActive", function AddonWrapper_isActiveGetter() { 1.6626 + if (Services.appinfo.inSafeMode) 1.6627 + return false; 1.6628 + return aAddon.active; 1.6629 + }); 1.6630 + 1.6631 + this.__defineGetter__("userDisabled", function AddonWrapper_userDisabledGetter() { 1.6632 + if (XPIProvider._enabledExperiments.has(aAddon.id)) { 1.6633 + return false; 1.6634 + } 1.6635 + 1.6636 + return aAddon.softDisabled || aAddon.userDisabled; 1.6637 + }); 1.6638 + this.__defineSetter__("userDisabled", function AddonWrapper_userDisabledSetter(val) { 1.6639 + if (val == this.userDisabled) { 1.6640 + return val; 1.6641 + } 1.6642 + 1.6643 + if (aAddon.type == "experiment") { 1.6644 + if (val) { 1.6645 + XPIProvider._enabledExperiments.delete(aAddon.id); 1.6646 + } else { 1.6647 + XPIProvider._enabledExperiments.add(aAddon.id); 1.6648 + } 1.6649 + } 1.6650 + 1.6651 + if (aAddon.inDatabase) { 1.6652 + if (aAddon.type == "theme" && val) { 1.6653 + if (aAddon.internalName == XPIProvider.defaultSkin) 1.6654 + throw new Error("Cannot disable the default theme"); 1.6655 + XPIProvider.enableDefaultTheme(); 1.6656 + } 1.6657 + else { 1.6658 + XPIProvider.updateAddonDisabledState(aAddon, val); 1.6659 + } 1.6660 + } 1.6661 + else { 1.6662 + aAddon.userDisabled = val; 1.6663 + // When enabling remove the softDisabled flag 1.6664 + if (!val) 1.6665 + aAddon.softDisabled = false; 1.6666 + } 1.6667 + 1.6668 + return val; 1.6669 + }); 1.6670 + 1.6671 + this.__defineSetter__("softDisabled", function AddonWrapper_softDisabledSetter(val) { 1.6672 + if (val == aAddon.softDisabled) 1.6673 + return val; 1.6674 + 1.6675 + if (aAddon.inDatabase) { 1.6676 + // When softDisabling a theme just enable the active theme 1.6677 + if (aAddon.type == "theme" && val && !aAddon.userDisabled) { 1.6678 + if (aAddon.internalName == XPIProvider.defaultSkin) 1.6679 + throw new Error("Cannot disable the default theme"); 1.6680 + XPIProvider.enableDefaultTheme(); 1.6681 + } 1.6682 + else { 1.6683 + XPIProvider.updateAddonDisabledState(aAddon, undefined, val); 1.6684 + } 1.6685 + } 1.6686 + else { 1.6687 + // Only set softDisabled if not already disabled 1.6688 + if (!aAddon.userDisabled) 1.6689 + aAddon.softDisabled = val; 1.6690 + } 1.6691 + 1.6692 + return val; 1.6693 + }); 1.6694 + 1.6695 + this.isCompatibleWith = function AddonWrapper_isCompatiblewith(aAppVersion, aPlatformVersion) { 1.6696 + return aAddon.isCompatibleWith(aAppVersion, aPlatformVersion); 1.6697 + }; 1.6698 + 1.6699 + this.uninstall = function AddonWrapper_uninstall() { 1.6700 + if (!(aAddon.inDatabase)) 1.6701 + throw new Error("Cannot uninstall an add-on that isn't installed"); 1.6702 + if (aAddon.pendingUninstall) 1.6703 + throw new Error("Add-on is already marked to be uninstalled"); 1.6704 + XPIProvider.uninstallAddon(aAddon); 1.6705 + }; 1.6706 + 1.6707 + this.cancelUninstall = function AddonWrapper_cancelUninstall() { 1.6708 + if (!(aAddon.inDatabase)) 1.6709 + throw new Error("Cannot cancel uninstall for an add-on that isn't installed"); 1.6710 + if (!aAddon.pendingUninstall) 1.6711 + throw new Error("Add-on is not marked to be uninstalled"); 1.6712 + XPIProvider.cancelUninstallAddon(aAddon); 1.6713 + }; 1.6714 + 1.6715 + this.findUpdates = function AddonWrapper_findUpdates(aListener, aReason, aAppVersion, aPlatformVersion) { 1.6716 + // Short-circuit updates for experiments because updates are handled 1.6717 + // through the Experiments Manager. 1.6718 + if (this.type == "experiment") { 1.6719 + AddonManagerPrivate.callNoUpdateListeners(this, aListener, aReason, 1.6720 + aAppVersion, aPlatformVersion); 1.6721 + return; 1.6722 + } 1.6723 + 1.6724 + new UpdateChecker(aAddon, aListener, aReason, aAppVersion, aPlatformVersion); 1.6725 + }; 1.6726 + 1.6727 + // Returns true if there was an update in progress, false if there was no update to cancel 1.6728 + this.cancelUpdate = function AddonWrapper_cancelUpdate() { 1.6729 + if (aAddon._updateCheck) { 1.6730 + aAddon._updateCheck.cancel(); 1.6731 + return true; 1.6732 + } 1.6733 + return false; 1.6734 + }; 1.6735 + 1.6736 + this.hasResource = function AddonWrapper_hasResource(aPath) { 1.6737 + if (aAddon._hasResourceCache.has(aPath)) 1.6738 + return aAddon._hasResourceCache.get(aPath); 1.6739 + 1.6740 + let bundle = aAddon._sourceBundle.clone(); 1.6741 + 1.6742 + // Bundle may not exist any more if the addon has just been uninstalled, 1.6743 + // but explicitly first checking .exists() results in unneeded file I/O. 1.6744 + try { 1.6745 + var isDir = bundle.isDirectory(); 1.6746 + } catch (e) { 1.6747 + aAddon._hasResourceCache.set(aPath, false); 1.6748 + return false; 1.6749 + } 1.6750 + 1.6751 + if (isDir) { 1.6752 + if (aPath) { 1.6753 + aPath.split("/").forEach(function(aPart) { 1.6754 + bundle.append(aPart); 1.6755 + }); 1.6756 + } 1.6757 + let result = bundle.exists(); 1.6758 + aAddon._hasResourceCache.set(aPath, result); 1.6759 + return result; 1.6760 + } 1.6761 + 1.6762 + let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"]. 1.6763 + createInstance(Ci.nsIZipReader); 1.6764 + try { 1.6765 + zipReader.open(bundle); 1.6766 + let result = zipReader.hasEntry(aPath); 1.6767 + aAddon._hasResourceCache.set(aPath, result); 1.6768 + return result; 1.6769 + } 1.6770 + catch (e) { 1.6771 + aAddon._hasResourceCache.set(aPath, false); 1.6772 + return false; 1.6773 + } 1.6774 + finally { 1.6775 + zipReader.close(); 1.6776 + } 1.6777 + }, 1.6778 + 1.6779 + /** 1.6780 + * Returns a URI to the selected resource or to the add-on bundle if aPath 1.6781 + * is null. URIs to the bundle will always be file: URIs. URIs to resources 1.6782 + * will be file: URIs if the add-on is unpacked or jar: URIs if the add-on is 1.6783 + * still an XPI file. 1.6784 + * 1.6785 + * @param aPath 1.6786 + * The path in the add-on to get the URI for or null to get a URI to 1.6787 + * the file or directory the add-on is installed as. 1.6788 + * @return an nsIURI 1.6789 + */ 1.6790 + this.getResourceURI = function AddonWrapper_getResourceURI(aPath) { 1.6791 + if (!aPath) 1.6792 + return NetUtil.newURI(aAddon._sourceBundle); 1.6793 + 1.6794 + return getURIForResourceInFile(aAddon._sourceBundle, aPath); 1.6795 + } 1.6796 +} 1.6797 + 1.6798 +/** 1.6799 + * An object which identifies a directory install location for add-ons. The 1.6800 + * location consists of a directory which contains the add-ons installed in the 1.6801 + * location. 1.6802 + * 1.6803 + * Each add-on installed in the location is either a directory containing the 1.6804 + * add-on's files or a text file containing an absolute path to the directory 1.6805 + * containing the add-ons files. The directory or text file must have the same 1.6806 + * name as the add-on's ID. 1.6807 + * 1.6808 + * There may also a special directory named "staged" which can contain 1.6809 + * directories with the same name as an add-on ID. If the directory is empty 1.6810 + * then it means the add-on will be uninstalled from this location during the 1.6811 + * next startup. If the directory contains the add-on's files then they will be 1.6812 + * installed during the next startup. 1.6813 + * 1.6814 + * @param aName 1.6815 + * The string identifier for the install location 1.6816 + * @param aDirectory 1.6817 + * The nsIFile directory for the install location 1.6818 + * @param aScope 1.6819 + * The scope of add-ons installed in this location 1.6820 + * @param aLocked 1.6821 + * true if add-ons cannot be installed, uninstalled or upgraded in the 1.6822 + * install location 1.6823 + */ 1.6824 +function DirectoryInstallLocation(aName, aDirectory, aScope, aLocked) { 1.6825 + this._name = aName; 1.6826 + this.locked = aLocked; 1.6827 + this._directory = aDirectory; 1.6828 + this._scope = aScope 1.6829 + this._IDToFileMap = {}; 1.6830 + this._FileToIDMap = {}; 1.6831 + this._linkedAddons = []; 1.6832 + this._stagingDirLock = 0; 1.6833 + 1.6834 + if (!aDirectory.exists()) 1.6835 + return; 1.6836 + if (!aDirectory.isDirectory()) 1.6837 + throw new Error("Location must be a directory."); 1.6838 + 1.6839 + this._readAddons(); 1.6840 +} 1.6841 + 1.6842 +DirectoryInstallLocation.prototype = { 1.6843 + _name : "", 1.6844 + _directory : null, 1.6845 + _IDToFileMap : null, // mapping from add-on ID to nsIFile 1.6846 + _FileToIDMap : null, // mapping from add-on path to add-on ID 1.6847 + 1.6848 + /** 1.6849 + * Reads a directory linked to in a file. 1.6850 + * 1.6851 + * @param file 1.6852 + * The file containing the directory path 1.6853 + * @return An nsIFile object representing the linked directory. 1.6854 + */ 1.6855 + _readDirectoryFromFile: function DirInstallLocation__readDirectoryFromFile(aFile) { 1.6856 + let fis = Cc["@mozilla.org/network/file-input-stream;1"]. 1.6857 + createInstance(Ci.nsIFileInputStream); 1.6858 + fis.init(aFile, -1, -1, false); 1.6859 + let line = { value: "" }; 1.6860 + if (fis instanceof Ci.nsILineInputStream) 1.6861 + fis.readLine(line); 1.6862 + fis.close(); 1.6863 + if (line.value) { 1.6864 + let linkedDirectory = Cc["@mozilla.org/file/local;1"]. 1.6865 + createInstance(Ci.nsIFile); 1.6866 + 1.6867 + try { 1.6868 + linkedDirectory.initWithPath(line.value); 1.6869 + } 1.6870 + catch (e) { 1.6871 + linkedDirectory.setRelativeDescriptor(aFile.parent, line.value); 1.6872 + } 1.6873 + 1.6874 + if (!linkedDirectory.exists()) { 1.6875 + logger.warn("File pointer " + aFile.path + " points to " + linkedDirectory.path + 1.6876 + " which does not exist"); 1.6877 + return null; 1.6878 + } 1.6879 + 1.6880 + if (!linkedDirectory.isDirectory()) { 1.6881 + logger.warn("File pointer " + aFile.path + " points to " + linkedDirectory.path + 1.6882 + " which is not a directory"); 1.6883 + return null; 1.6884 + } 1.6885 + 1.6886 + return linkedDirectory; 1.6887 + } 1.6888 + 1.6889 + logger.warn("File pointer " + aFile.path + " does not contain a path"); 1.6890 + return null; 1.6891 + }, 1.6892 + 1.6893 + /** 1.6894 + * Finds all the add-ons installed in this location. 1.6895 + */ 1.6896 + _readAddons: function DirInstallLocation__readAddons() { 1.6897 + // Use a snapshot of the directory contents to avoid possible issues with 1.6898 + // iterating over a directory while removing files from it (the YAFFS2 1.6899 + // embedded filesystem has this issue, see bug 772238). 1.6900 + let entries = getDirectoryEntries(this._directory); 1.6901 + for (let entry of entries) { 1.6902 + let id = entry.leafName; 1.6903 + 1.6904 + if (id == DIR_STAGE || id == DIR_XPI_STAGE || id == DIR_TRASH) 1.6905 + continue; 1.6906 + 1.6907 + let directLoad = false; 1.6908 + if (entry.isFile() && 1.6909 + id.substring(id.length - 4).toLowerCase() == ".xpi") { 1.6910 + directLoad = true; 1.6911 + id = id.substring(0, id.length - 4); 1.6912 + } 1.6913 + 1.6914 + if (!gIDTest.test(id)) { 1.6915 + logger.debug("Ignoring file entry whose name is not a valid add-on ID: " + 1.6916 + entry.path); 1.6917 + continue; 1.6918 + } 1.6919 + 1.6920 + if (entry.isFile() && !directLoad) { 1.6921 + let newEntry = this._readDirectoryFromFile(entry); 1.6922 + if (!newEntry) { 1.6923 + logger.debug("Deleting stale pointer file " + entry.path); 1.6924 + try { 1.6925 + entry.remove(true); 1.6926 + } 1.6927 + catch (e) { 1.6928 + logger.warn("Failed to remove stale pointer file " + entry.path, e); 1.6929 + // Failing to remove the stale pointer file is ignorable 1.6930 + } 1.6931 + continue; 1.6932 + } 1.6933 + 1.6934 + entry = newEntry; 1.6935 + this._linkedAddons.push(id); 1.6936 + } 1.6937 + 1.6938 + this._IDToFileMap[id] = entry; 1.6939 + this._FileToIDMap[entry.path] = id; 1.6940 + } 1.6941 + }, 1.6942 + 1.6943 + /** 1.6944 + * Gets the name of this install location. 1.6945 + */ 1.6946 + get name() { 1.6947 + return this._name; 1.6948 + }, 1.6949 + 1.6950 + /** 1.6951 + * Gets the scope of this install location. 1.6952 + */ 1.6953 + get scope() { 1.6954 + return this._scope; 1.6955 + }, 1.6956 + 1.6957 + /** 1.6958 + * Gets an array of nsIFiles for add-ons installed in this location. 1.6959 + */ 1.6960 + get addonLocations() { 1.6961 + let locations = []; 1.6962 + for (let id in this._IDToFileMap) { 1.6963 + locations.push(this._IDToFileMap[id].clone()); 1.6964 + } 1.6965 + return locations; 1.6966 + }, 1.6967 + 1.6968 + /** 1.6969 + * Gets the staging directory to put add-ons that are pending install and 1.6970 + * uninstall into. 1.6971 + * 1.6972 + * @return an nsIFile 1.6973 + */ 1.6974 + getStagingDir: function DirInstallLocation_getStagingDir() { 1.6975 + let dir = this._directory.clone(); 1.6976 + dir.append(DIR_STAGE); 1.6977 + return dir; 1.6978 + }, 1.6979 + 1.6980 + requestStagingDir: function() { 1.6981 + this._stagingDirLock++; 1.6982 + 1.6983 + if (this._stagingDirPromise) 1.6984 + return this._stagingDirPromise; 1.6985 + 1.6986 + OS.File.makeDir(this._directory.path); 1.6987 + let stagepath = OS.Path.join(this._directory.path, DIR_STAGE); 1.6988 + return this._stagingDirPromise = OS.File.makeDir(stagepath).then(null, (e) => { 1.6989 + if (e instanceof OS.File.Error && e.becauseExists) 1.6990 + return; 1.6991 + logger.error("Failed to create staging directory", e); 1.6992 + throw e; 1.6993 + }); 1.6994 + }, 1.6995 + 1.6996 + releaseStagingDir: function() { 1.6997 + this._stagingDirLock--; 1.6998 + 1.6999 + if (this._stagingDirLock == 0) { 1.7000 + this._stagingDirPromise = null; 1.7001 + this.cleanStagingDir(); 1.7002 + } 1.7003 + 1.7004 + return Promise.resolve(); 1.7005 + }, 1.7006 + 1.7007 + /** 1.7008 + * Removes the specified files or directories in the staging directory and 1.7009 + * then if the staging directory is empty attempts to remove it. 1.7010 + * 1.7011 + * @param aLeafNames 1.7012 + * An array of file or directory to remove from the directory, the 1.7013 + * array may be empty 1.7014 + */ 1.7015 + cleanStagingDir: function(aLeafNames = []) { 1.7016 + let dir = this.getStagingDir(); 1.7017 + 1.7018 + for (let name of aLeafNames) { 1.7019 + let file = dir.clone(); 1.7020 + file.append(name); 1.7021 + recursiveRemove(file); 1.7022 + } 1.7023 + 1.7024 + if (this._stagingDirLock > 0) 1.7025 + return; 1.7026 + 1.7027 + let dirEntries = dir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator); 1.7028 + try { 1.7029 + if (dirEntries.nextFile) 1.7030 + return; 1.7031 + } 1.7032 + finally { 1.7033 + dirEntries.close(); 1.7034 + } 1.7035 + 1.7036 + try { 1.7037 + setFilePermissions(dir, FileUtils.PERMS_DIRECTORY); 1.7038 + dir.remove(false); 1.7039 + } 1.7040 + catch (e) { 1.7041 + logger.warn("Failed to remove staging dir", e); 1.7042 + // Failing to remove the staging directory is ignorable 1.7043 + } 1.7044 + }, 1.7045 + 1.7046 + /** 1.7047 + * Gets the directory used by old versions for staging XPI and JAR files ready 1.7048 + * to be installed. 1.7049 + * 1.7050 + * @return an nsIFile 1.7051 + */ 1.7052 + getXPIStagingDir: function DirInstallLocation_getXPIStagingDir() { 1.7053 + let dir = this._directory.clone(); 1.7054 + dir.append(DIR_XPI_STAGE); 1.7055 + return dir; 1.7056 + }, 1.7057 + 1.7058 + /** 1.7059 + * Returns a directory that is normally on the same filesystem as the rest of 1.7060 + * the install location and can be used for temporarily storing files during 1.7061 + * safe move operations. Calling this method will delete the existing trash 1.7062 + * directory and its contents. 1.7063 + * 1.7064 + * @return an nsIFile 1.7065 + */ 1.7066 + getTrashDir: function DirInstallLocation_getTrashDir() { 1.7067 + let trashDir = this._directory.clone(); 1.7068 + trashDir.append(DIR_TRASH); 1.7069 + if (trashDir.exists()) 1.7070 + recursiveRemove(trashDir); 1.7071 + trashDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); 1.7072 + return trashDir; 1.7073 + }, 1.7074 + 1.7075 + /** 1.7076 + * Installs an add-on into the install location. 1.7077 + * 1.7078 + * @param aId 1.7079 + * The ID of the add-on to install 1.7080 + * @param aSource 1.7081 + * The source nsIFile to install from 1.7082 + * @param aExistingAddonID 1.7083 + * The ID of an existing add-on to uninstall at the same time 1.7084 + * @param aCopy 1.7085 + * If false the source files will be moved to the new location, 1.7086 + * otherwise they will only be copied 1.7087 + * @return an nsIFile indicating where the add-on was installed to 1.7088 + */ 1.7089 + installAddon: function DirInstallLocation_installAddon(aId, aSource, 1.7090 + aExistingAddonID, 1.7091 + aCopy) { 1.7092 + let trashDir = this.getTrashDir(); 1.7093 + 1.7094 + let transaction = new SafeInstallOperation(); 1.7095 + 1.7096 + let self = this; 1.7097 + function moveOldAddon(aId) { 1.7098 + let file = self._directory.clone(); 1.7099 + file.append(aId); 1.7100 + 1.7101 + if (file.exists()) 1.7102 + transaction.move(file, trashDir); 1.7103 + 1.7104 + file = self._directory.clone(); 1.7105 + file.append(aId + ".xpi"); 1.7106 + if (file.exists()) { 1.7107 + flushJarCache(file); 1.7108 + transaction.move(file, trashDir); 1.7109 + } 1.7110 + } 1.7111 + 1.7112 + // If any of these operations fails the finally block will clean up the 1.7113 + // temporary directory 1.7114 + try { 1.7115 + moveOldAddon(aId); 1.7116 + if (aExistingAddonID && aExistingAddonID != aId) 1.7117 + moveOldAddon(aExistingAddonID); 1.7118 + 1.7119 + if (aCopy) { 1.7120 + transaction.copy(aSource, this._directory); 1.7121 + } 1.7122 + else { 1.7123 + if (aSource.isFile()) 1.7124 + flushJarCache(aSource); 1.7125 + 1.7126 + transaction.move(aSource, this._directory); 1.7127 + } 1.7128 + } 1.7129 + finally { 1.7130 + // It isn't ideal if this cleanup fails but it isn't worth rolling back 1.7131 + // the install because of it. 1.7132 + try { 1.7133 + recursiveRemove(trashDir); 1.7134 + } 1.7135 + catch (e) { 1.7136 + logger.warn("Failed to remove trash directory when installing " + aId, e); 1.7137 + } 1.7138 + } 1.7139 + 1.7140 + let newFile = this._directory.clone(); 1.7141 + newFile.append(aSource.leafName); 1.7142 + try { 1.7143 + newFile.lastModifiedTime = Date.now(); 1.7144 + } catch (e) { 1.7145 + logger.warn("failed to set lastModifiedTime on " + newFile.path, e); 1.7146 + } 1.7147 + this._FileToIDMap[newFile.path] = aId; 1.7148 + this._IDToFileMap[aId] = newFile; 1.7149 + 1.7150 + if (aExistingAddonID && aExistingAddonID != aId && 1.7151 + aExistingAddonID in this._IDToFileMap) { 1.7152 + delete this._FileToIDMap[this._IDToFileMap[aExistingAddonID]]; 1.7153 + delete this._IDToFileMap[aExistingAddonID]; 1.7154 + } 1.7155 + 1.7156 + return newFile; 1.7157 + }, 1.7158 + 1.7159 + /** 1.7160 + * Uninstalls an add-on from this location. 1.7161 + * 1.7162 + * @param aId 1.7163 + * The ID of the add-on to uninstall 1.7164 + * @throws if the ID does not match any of the add-ons installed 1.7165 + */ 1.7166 + uninstallAddon: function DirInstallLocation_uninstallAddon(aId) { 1.7167 + let file = this._IDToFileMap[aId]; 1.7168 + if (!file) { 1.7169 + logger.warn("Attempted to remove " + aId + " from " + 1.7170 + this._name + " but it was already gone"); 1.7171 + return; 1.7172 + } 1.7173 + 1.7174 + file = this._directory.clone(); 1.7175 + file.append(aId); 1.7176 + if (!file.exists()) 1.7177 + file.leafName += ".xpi"; 1.7178 + 1.7179 + if (!file.exists()) { 1.7180 + logger.warn("Attempted to remove " + aId + " from " + 1.7181 + this._name + " but it was already gone"); 1.7182 + 1.7183 + delete this._FileToIDMap[file.path]; 1.7184 + delete this._IDToFileMap[aId]; 1.7185 + return; 1.7186 + } 1.7187 + 1.7188 + let trashDir = this.getTrashDir(); 1.7189 + 1.7190 + if (file.leafName != aId) { 1.7191 + logger.debug("uninstallAddon: flushing jar cache " + file.path + " for addon " + aId); 1.7192 + flushJarCache(file); 1.7193 + } 1.7194 + 1.7195 + let transaction = new SafeInstallOperation(); 1.7196 + 1.7197 + try { 1.7198 + transaction.move(file, trashDir); 1.7199 + } 1.7200 + finally { 1.7201 + // It isn't ideal if this cleanup fails, but it is probably better than 1.7202 + // rolling back the uninstall at this point 1.7203 + try { 1.7204 + recursiveRemove(trashDir); 1.7205 + } 1.7206 + catch (e) { 1.7207 + logger.warn("Failed to remove trash directory when uninstalling " + aId, e); 1.7208 + } 1.7209 + } 1.7210 + 1.7211 + delete this._FileToIDMap[file.path]; 1.7212 + delete this._IDToFileMap[aId]; 1.7213 + }, 1.7214 + 1.7215 + /** 1.7216 + * Gets the ID of the add-on installed in the given nsIFile. 1.7217 + * 1.7218 + * @param aFile 1.7219 + * The nsIFile to look in 1.7220 + * @return the ID 1.7221 + * @throws if the file does not represent an installed add-on 1.7222 + */ 1.7223 + getIDForLocation: function DirInstallLocation_getIDForLocation(aFile) { 1.7224 + if (aFile.path in this._FileToIDMap) 1.7225 + return this._FileToIDMap[aFile.path]; 1.7226 + throw new Error("Unknown add-on location " + aFile.path); 1.7227 + }, 1.7228 + 1.7229 + /** 1.7230 + * Gets the directory that the add-on with the given ID is installed in. 1.7231 + * 1.7232 + * @param aId 1.7233 + * The ID of the add-on 1.7234 + * @return The nsIFile 1.7235 + * @throws if the ID does not match any of the add-ons installed 1.7236 + */ 1.7237 + getLocationForID: function DirInstallLocation_getLocationForID(aId) { 1.7238 + if (aId in this._IDToFileMap) 1.7239 + return this._IDToFileMap[aId].clone(); 1.7240 + throw new Error("Unknown add-on ID " + aId); 1.7241 + }, 1.7242 + 1.7243 + /** 1.7244 + * Returns true if the given addon was installed in this location by a text 1.7245 + * file pointing to its real path. 1.7246 + * 1.7247 + * @param aId 1.7248 + * The ID of the addon 1.7249 + */ 1.7250 + isLinkedAddon: function DirInstallLocation__isLinkedAddon(aId) { 1.7251 + return this._linkedAddons.indexOf(aId) != -1; 1.7252 + } 1.7253 +}; 1.7254 + 1.7255 +#ifdef XP_WIN 1.7256 +/** 1.7257 + * An object that identifies a registry install location for add-ons. The location 1.7258 + * consists of a registry key which contains string values mapping ID to the 1.7259 + * path where an add-on is installed 1.7260 + * 1.7261 + * @param aName 1.7262 + * The string identifier of this Install Location. 1.7263 + * @param aRootKey 1.7264 + * The root key (one of the ROOT_KEY_ values from nsIWindowsRegKey). 1.7265 + * @param scope 1.7266 + * The scope of add-ons installed in this location 1.7267 + */ 1.7268 +function WinRegInstallLocation(aName, aRootKey, aScope) { 1.7269 + this.locked = true; 1.7270 + this._name = aName; 1.7271 + this._rootKey = aRootKey; 1.7272 + this._scope = aScope; 1.7273 + this._IDToFileMap = {}; 1.7274 + this._FileToIDMap = {}; 1.7275 + 1.7276 + let path = this._appKeyPath + "\\Extensions"; 1.7277 + let key = Cc["@mozilla.org/windows-registry-key;1"]. 1.7278 + createInstance(Ci.nsIWindowsRegKey); 1.7279 + 1.7280 + // Reading the registry may throw an exception, and that's ok. In error 1.7281 + // cases, we just leave ourselves in the empty state. 1.7282 + try { 1.7283 + key.open(this._rootKey, path, Ci.nsIWindowsRegKey.ACCESS_READ); 1.7284 + } 1.7285 + catch (e) { 1.7286 + return; 1.7287 + } 1.7288 + 1.7289 + this._readAddons(key); 1.7290 + key.close(); 1.7291 +} 1.7292 + 1.7293 +WinRegInstallLocation.prototype = { 1.7294 + _name : "", 1.7295 + _rootKey : null, 1.7296 + _scope : null, 1.7297 + _IDToFileMap : null, // mapping from ID to nsIFile 1.7298 + _FileToIDMap : null, // mapping from path to ID 1.7299 + 1.7300 + /** 1.7301 + * Retrieves the path of this Application's data key in the registry. 1.7302 + */ 1.7303 + get _appKeyPath() { 1.7304 + let appVendor = Services.appinfo.vendor; 1.7305 + let appName = Services.appinfo.name; 1.7306 + 1.7307 +#ifdef MOZ_THUNDERBIRD 1.7308 + // XXX Thunderbird doesn't specify a vendor string 1.7309 + if (appVendor == "") 1.7310 + appVendor = "Mozilla"; 1.7311 +#endif 1.7312 + 1.7313 + // XULRunner-based apps may intentionally not specify a vendor 1.7314 + if (appVendor != "") 1.7315 + appVendor += "\\"; 1.7316 + 1.7317 + return "SOFTWARE\\" + appVendor + appName; 1.7318 + }, 1.7319 + 1.7320 + /** 1.7321 + * Read the registry and build a mapping between ID and path for each 1.7322 + * installed add-on. 1.7323 + * 1.7324 + * @param key 1.7325 + * The key that contains the ID to path mapping 1.7326 + */ 1.7327 + _readAddons: function RegInstallLocation__readAddons(aKey) { 1.7328 + let count = aKey.valueCount; 1.7329 + for (let i = 0; i < count; ++i) { 1.7330 + let id = aKey.getValueName(i); 1.7331 + 1.7332 + let file = Cc["@mozilla.org/file/local;1"]. 1.7333 + createInstance(Ci.nsIFile); 1.7334 + file.initWithPath(aKey.readStringValue(id)); 1.7335 + 1.7336 + if (!file.exists()) { 1.7337 + logger.warn("Ignoring missing add-on in " + file.path); 1.7338 + continue; 1.7339 + } 1.7340 + 1.7341 + this._IDToFileMap[id] = file; 1.7342 + this._FileToIDMap[file.path] = id; 1.7343 + } 1.7344 + }, 1.7345 + 1.7346 + /** 1.7347 + * Gets the name of this install location. 1.7348 + */ 1.7349 + get name() { 1.7350 + return this._name; 1.7351 + }, 1.7352 + 1.7353 + /** 1.7354 + * Gets the scope of this install location. 1.7355 + */ 1.7356 + get scope() { 1.7357 + return this._scope; 1.7358 + }, 1.7359 + 1.7360 + /** 1.7361 + * Gets an array of nsIFiles for add-ons installed in this location. 1.7362 + */ 1.7363 + get addonLocations() { 1.7364 + let locations = []; 1.7365 + for (let id in this._IDToFileMap) { 1.7366 + locations.push(this._IDToFileMap[id].clone()); 1.7367 + } 1.7368 + return locations; 1.7369 + }, 1.7370 + 1.7371 + /** 1.7372 + * Gets the ID of the add-on installed in the given nsIFile. 1.7373 + * 1.7374 + * @param aFile 1.7375 + * The nsIFile to look in 1.7376 + * @return the ID 1.7377 + * @throws if the file does not represent an installed add-on 1.7378 + */ 1.7379 + getIDForLocation: function RegInstallLocation_getIDForLocation(aFile) { 1.7380 + if (aFile.path in this._FileToIDMap) 1.7381 + return this._FileToIDMap[aFile.path]; 1.7382 + throw new Error("Unknown add-on location"); 1.7383 + }, 1.7384 + 1.7385 + /** 1.7386 + * Gets the nsIFile that the add-on with the given ID is installed in. 1.7387 + * 1.7388 + * @param aId 1.7389 + * The ID of the add-on 1.7390 + * @return the nsIFile 1.7391 + */ 1.7392 + getLocationForID: function RegInstallLocation_getLocationForID(aId) { 1.7393 + if (aId in this._IDToFileMap) 1.7394 + return this._IDToFileMap[aId].clone(); 1.7395 + throw new Error("Unknown add-on ID"); 1.7396 + }, 1.7397 + 1.7398 + /** 1.7399 + * @see DirectoryInstallLocation 1.7400 + */ 1.7401 + isLinkedAddon: function RegInstallLocation_isLinkedAddon(aId) { 1.7402 + return true; 1.7403 + } 1.7404 +}; 1.7405 +#endif 1.7406 + 1.7407 +let addonTypes = [ 1.7408 + new AddonManagerPrivate.AddonType("extension", URI_EXTENSION_STRINGS, 1.7409 + STRING_TYPE_NAME, 1.7410 + AddonManager.VIEW_TYPE_LIST, 4000), 1.7411 + new AddonManagerPrivate.AddonType("theme", URI_EXTENSION_STRINGS, 1.7412 + STRING_TYPE_NAME, 1.7413 + AddonManager.VIEW_TYPE_LIST, 5000), 1.7414 + new AddonManagerPrivate.AddonType("dictionary", URI_EXTENSION_STRINGS, 1.7415 + STRING_TYPE_NAME, 1.7416 + AddonManager.VIEW_TYPE_LIST, 7000, 1.7417 + AddonManager.TYPE_UI_HIDE_EMPTY), 1.7418 + new AddonManagerPrivate.AddonType("locale", URI_EXTENSION_STRINGS, 1.7419 + STRING_TYPE_NAME, 1.7420 + AddonManager.VIEW_TYPE_LIST, 8000, 1.7421 + AddonManager.TYPE_UI_HIDE_EMPTY), 1.7422 +]; 1.7423 + 1.7424 +// We only register experiments support if the application supports them. 1.7425 +// Ideally, we would install an observer to watch the pref. Installing 1.7426 +// an observer for this pref is not necessary here and may be buggy with 1.7427 +// regards to registering this XPIProvider twice. 1.7428 +if (Prefs.getBoolPref("experiments.supported", false)) { 1.7429 + addonTypes.push( 1.7430 + new AddonManagerPrivate.AddonType("experiment", 1.7431 + URI_EXTENSION_STRINGS, 1.7432 + STRING_TYPE_NAME, 1.7433 + AddonManager.VIEW_TYPE_LIST, 11000, 1.7434 + AddonManager.TYPE_UI_HIDE_EMPTY)); 1.7435 +} 1.7436 + 1.7437 +AddonManagerPrivate.registerProvider(XPIProvider, addonTypes);