Sat, 03 Jan 2015 20:18:00 +0100
Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.
michael@0 | 1 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 4 | |
michael@0 | 5 | "use strict"; |
michael@0 | 6 | |
michael@0 | 7 | const Cc = Components.classes; |
michael@0 | 8 | const Ci = Components.interfaces; |
michael@0 | 9 | const Cr = Components.results; |
michael@0 | 10 | const Cu = Components.utils; |
michael@0 | 11 | |
michael@0 | 12 | this.EXPORTED_SYMBOLS = ["XPIProvider"]; |
michael@0 | 13 | |
michael@0 | 14 | Components.utils.import("resource://gre/modules/Services.jsm"); |
michael@0 | 15 | Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); |
michael@0 | 16 | Components.utils.import("resource://gre/modules/AddonManager.jsm"); |
michael@0 | 17 | |
michael@0 | 18 | XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository", |
michael@0 | 19 | "resource://gre/modules/addons/AddonRepository.jsm"); |
michael@0 | 20 | XPCOMUtils.defineLazyModuleGetter(this, "ChromeManifestParser", |
michael@0 | 21 | "resource://gre/modules/ChromeManifestParser.jsm"); |
michael@0 | 22 | XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager", |
michael@0 | 23 | "resource://gre/modules/LightweightThemeManager.jsm"); |
michael@0 | 24 | XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", |
michael@0 | 25 | "resource://gre/modules/FileUtils.jsm"); |
michael@0 | 26 | XPCOMUtils.defineLazyModuleGetter(this, "ZipUtils", |
michael@0 | 27 | "resource://gre/modules/ZipUtils.jsm"); |
michael@0 | 28 | XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", |
michael@0 | 29 | "resource://gre/modules/NetUtil.jsm"); |
michael@0 | 30 | XPCOMUtils.defineLazyModuleGetter(this, "PermissionsUtils", |
michael@0 | 31 | "resource://gre/modules/PermissionsUtils.jsm"); |
michael@0 | 32 | XPCOMUtils.defineLazyModuleGetter(this, "Promise", |
michael@0 | 33 | "resource://gre/modules/Promise.jsm"); |
michael@0 | 34 | XPCOMUtils.defineLazyModuleGetter(this, "Task", |
michael@0 | 35 | "resource://gre/modules/Task.jsm"); |
michael@0 | 36 | XPCOMUtils.defineLazyModuleGetter(this, "OS", |
michael@0 | 37 | "resource://gre/modules/osfile.jsm"); |
michael@0 | 38 | XPCOMUtils.defineLazyModuleGetter(this, "BrowserToolboxProcess", |
michael@0 | 39 | "resource:///modules/devtools/ToolboxProcess.jsm"); |
michael@0 | 40 | |
michael@0 | 41 | XPCOMUtils.defineLazyServiceGetter(this, |
michael@0 | 42 | "ChromeRegistry", |
michael@0 | 43 | "@mozilla.org/chrome/chrome-registry;1", |
michael@0 | 44 | "nsIChromeRegistry"); |
michael@0 | 45 | XPCOMUtils.defineLazyServiceGetter(this, |
michael@0 | 46 | "ResProtocolHandler", |
michael@0 | 47 | "@mozilla.org/network/protocol;1?name=resource", |
michael@0 | 48 | "nsIResProtocolHandler"); |
michael@0 | 49 | |
michael@0 | 50 | const nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile", |
michael@0 | 51 | "initWithPath"); |
michael@0 | 52 | |
michael@0 | 53 | const PREF_DB_SCHEMA = "extensions.databaseSchema"; |
michael@0 | 54 | const PREF_INSTALL_CACHE = "extensions.installCache"; |
michael@0 | 55 | const PREF_BOOTSTRAP_ADDONS = "extensions.bootstrappedAddons"; |
michael@0 | 56 | const PREF_PENDING_OPERATIONS = "extensions.pendingOperations"; |
michael@0 | 57 | const PREF_MATCH_OS_LOCALE = "intl.locale.matchOS"; |
michael@0 | 58 | const PREF_SELECTED_LOCALE = "general.useragent.locale"; |
michael@0 | 59 | const PREF_EM_DSS_ENABLED = "extensions.dss.enabled"; |
michael@0 | 60 | const PREF_DSS_SWITCHPENDING = "extensions.dss.switchPending"; |
michael@0 | 61 | const PREF_DSS_SKIN_TO_SELECT = "extensions.lastSelectedSkin"; |
michael@0 | 62 | const PREF_GENERAL_SKINS_SELECTEDSKIN = "general.skins.selectedSkin"; |
michael@0 | 63 | const PREF_EM_UPDATE_URL = "extensions.update.url"; |
michael@0 | 64 | const PREF_EM_UPDATE_BACKGROUND_URL = "extensions.update.background.url"; |
michael@0 | 65 | const PREF_EM_ENABLED_ADDONS = "extensions.enabledAddons"; |
michael@0 | 66 | const PREF_EM_EXTENSION_FORMAT = "extensions."; |
michael@0 | 67 | const PREF_EM_ENABLED_SCOPES = "extensions.enabledScopes"; |
michael@0 | 68 | const PREF_EM_AUTO_DISABLED_SCOPES = "extensions.autoDisableScopes"; |
michael@0 | 69 | const PREF_EM_SHOW_MISMATCH_UI = "extensions.showMismatchUI"; |
michael@0 | 70 | const PREF_XPI_ENABLED = "xpinstall.enabled"; |
michael@0 | 71 | const PREF_XPI_WHITELIST_REQUIRED = "xpinstall.whitelist.required"; |
michael@0 | 72 | const PREF_XPI_DIRECT_WHITELISTED = "xpinstall.whitelist.directRequest"; |
michael@0 | 73 | const PREF_XPI_FILE_WHITELISTED = "xpinstall.whitelist.fileRequest"; |
michael@0 | 74 | const PREF_XPI_PERMISSIONS_BRANCH = "xpinstall."; |
michael@0 | 75 | const PREF_XPI_UNPACK = "extensions.alwaysUnpack"; |
michael@0 | 76 | const PREF_INSTALL_REQUIREBUILTINCERTS = "extensions.install.requireBuiltInCerts"; |
michael@0 | 77 | const PREF_INSTALL_DISTRO_ADDONS = "extensions.installDistroAddons"; |
michael@0 | 78 | const PREF_BRANCH_INSTALLED_ADDON = "extensions.installedDistroAddon."; |
michael@0 | 79 | const PREF_SHOWN_SELECTION_UI = "extensions.shownSelectionUI"; |
michael@0 | 80 | |
michael@0 | 81 | const PREF_EM_MIN_COMPAT_APP_VERSION = "extensions.minCompatibleAppVersion"; |
michael@0 | 82 | const PREF_EM_MIN_COMPAT_PLATFORM_VERSION = "extensions.minCompatiblePlatformVersion"; |
michael@0 | 83 | |
michael@0 | 84 | const PREF_CHECKCOMAT_THEMEOVERRIDE = "extensions.checkCompatibility.temporaryThemeOverride_minAppVersion"; |
michael@0 | 85 | |
michael@0 | 86 | const URI_EXTENSION_SELECT_DIALOG = "chrome://mozapps/content/extensions/selectAddons.xul"; |
michael@0 | 87 | const URI_EXTENSION_UPDATE_DIALOG = "chrome://mozapps/content/extensions/update.xul"; |
michael@0 | 88 | const URI_EXTENSION_STRINGS = "chrome://mozapps/locale/extensions/extensions.properties"; |
michael@0 | 89 | |
michael@0 | 90 | const STRING_TYPE_NAME = "type.%ID%.name"; |
michael@0 | 91 | |
michael@0 | 92 | const DIR_EXTENSIONS = "extensions"; |
michael@0 | 93 | const DIR_STAGE = "staged"; |
michael@0 | 94 | const DIR_XPI_STAGE = "staged-xpis"; |
michael@0 | 95 | const DIR_TRASH = "trash"; |
michael@0 | 96 | |
michael@0 | 97 | const FILE_DATABASE = "extensions.json"; |
michael@0 | 98 | const FILE_OLD_CACHE = "extensions.cache"; |
michael@0 | 99 | const FILE_INSTALL_MANIFEST = "install.rdf"; |
michael@0 | 100 | const FILE_XPI_ADDONS_LIST = "extensions.ini"; |
michael@0 | 101 | |
michael@0 | 102 | const KEY_PROFILEDIR = "ProfD"; |
michael@0 | 103 | const KEY_APPDIR = "XCurProcD"; |
michael@0 | 104 | const KEY_TEMPDIR = "TmpD"; |
michael@0 | 105 | const KEY_APP_DISTRIBUTION = "XREAppDist"; |
michael@0 | 106 | |
michael@0 | 107 | const KEY_APP_PROFILE = "app-profile"; |
michael@0 | 108 | const KEY_APP_GLOBAL = "app-global"; |
michael@0 | 109 | const KEY_APP_SYSTEM_LOCAL = "app-system-local"; |
michael@0 | 110 | const KEY_APP_SYSTEM_SHARE = "app-system-share"; |
michael@0 | 111 | const KEY_APP_SYSTEM_USER = "app-system-user"; |
michael@0 | 112 | |
michael@0 | 113 | const NOTIFICATION_FLUSH_PERMISSIONS = "flush-pending-permissions"; |
michael@0 | 114 | const XPI_PERMISSION = "install"; |
michael@0 | 115 | |
michael@0 | 116 | const RDFURI_INSTALL_MANIFEST_ROOT = "urn:mozilla:install-manifest"; |
michael@0 | 117 | const PREFIX_NS_EM = "http://www.mozilla.org/2004/em-rdf#"; |
michael@0 | 118 | |
michael@0 | 119 | const TOOLKIT_ID = "toolkit@mozilla.org"; |
michael@0 | 120 | |
michael@0 | 121 | // The value for this is in Makefile.in |
michael@0 | 122 | #expand const DB_SCHEMA = __MOZ_EXTENSIONS_DB_SCHEMA__; |
michael@0 | 123 | |
michael@0 | 124 | // Properties that exist in the install manifest |
michael@0 | 125 | const PROP_METADATA = ["id", "version", "type", "internalName", "updateURL", |
michael@0 | 126 | "updateKey", "optionsURL", "optionsType", "aboutURL", |
michael@0 | 127 | "iconURL", "icon64URL"]; |
michael@0 | 128 | const PROP_LOCALE_SINGLE = ["name", "description", "creator", "homepageURL"]; |
michael@0 | 129 | const PROP_LOCALE_MULTI = ["developers", "translators", "contributors"]; |
michael@0 | 130 | const PROP_TARGETAPP = ["id", "minVersion", "maxVersion"]; |
michael@0 | 131 | |
michael@0 | 132 | // Properties that should be migrated where possible from an old database. These |
michael@0 | 133 | // shouldn't include properties that can be read directly from install.rdf files |
michael@0 | 134 | // or calculated |
michael@0 | 135 | const DB_MIGRATE_METADATA= ["installDate", "userDisabled", "softDisabled", |
michael@0 | 136 | "sourceURI", "applyBackgroundUpdates", |
michael@0 | 137 | "releaseNotesURI", "foreignInstall", "syncGUID"]; |
michael@0 | 138 | // Properties to cache and reload when an addon installation is pending |
michael@0 | 139 | const PENDING_INSTALL_METADATA = |
michael@0 | 140 | ["syncGUID", "targetApplications", "userDisabled", "softDisabled", |
michael@0 | 141 | "existingAddonID", "sourceURI", "releaseNotesURI", "installDate", |
michael@0 | 142 | "updateDate", "applyBackgroundUpdates", "compatibilityOverrides"]; |
michael@0 | 143 | |
michael@0 | 144 | // Note: When adding/changing/removing items here, remember to change the |
michael@0 | 145 | // DB schema version to ensure changes are picked up ASAP. |
michael@0 | 146 | const STATIC_BLOCKLIST_PATTERNS = [ |
michael@0 | 147 | { creator: "Mozilla Corp.", |
michael@0 | 148 | level: Ci.nsIBlocklistService.STATE_BLOCKED, |
michael@0 | 149 | blockID: "i162" }, |
michael@0 | 150 | { creator: "Mozilla.org", |
michael@0 | 151 | level: Ci.nsIBlocklistService.STATE_BLOCKED, |
michael@0 | 152 | blockID: "i162" } |
michael@0 | 153 | ]; |
michael@0 | 154 | |
michael@0 | 155 | |
michael@0 | 156 | const BOOTSTRAP_REASONS = { |
michael@0 | 157 | APP_STARTUP : 1, |
michael@0 | 158 | APP_SHUTDOWN : 2, |
michael@0 | 159 | ADDON_ENABLE : 3, |
michael@0 | 160 | ADDON_DISABLE : 4, |
michael@0 | 161 | ADDON_INSTALL : 5, |
michael@0 | 162 | ADDON_UNINSTALL : 6, |
michael@0 | 163 | ADDON_UPGRADE : 7, |
michael@0 | 164 | ADDON_DOWNGRADE : 8 |
michael@0 | 165 | }; |
michael@0 | 166 | |
michael@0 | 167 | // Map new string type identifiers to old style nsIUpdateItem types |
michael@0 | 168 | const TYPES = { |
michael@0 | 169 | extension: 2, |
michael@0 | 170 | theme: 4, |
michael@0 | 171 | locale: 8, |
michael@0 | 172 | multipackage: 32, |
michael@0 | 173 | dictionary: 64, |
michael@0 | 174 | experiment: 128, |
michael@0 | 175 | }; |
michael@0 | 176 | |
michael@0 | 177 | const RESTARTLESS_TYPES = new Set([ |
michael@0 | 178 | "dictionary", |
michael@0 | 179 | "experiment", |
michael@0 | 180 | "locale", |
michael@0 | 181 | ]); |
michael@0 | 182 | |
michael@0 | 183 | // Keep track of where we are in startup for telemetry |
michael@0 | 184 | // event happened during XPIDatabase.startup() |
michael@0 | 185 | const XPI_STARTING = "XPIStarting"; |
michael@0 | 186 | // event happened after startup() but before the final-ui-startup event |
michael@0 | 187 | const XPI_BEFORE_UI_STARTUP = "BeforeFinalUIStartup"; |
michael@0 | 188 | // event happened after final-ui-startup |
michael@0 | 189 | const XPI_AFTER_UI_STARTUP = "AfterFinalUIStartup"; |
michael@0 | 190 | |
michael@0 | 191 | const COMPATIBLE_BY_DEFAULT_TYPES = { |
michael@0 | 192 | extension: true, |
michael@0 | 193 | dictionary: true |
michael@0 | 194 | }; |
michael@0 | 195 | |
michael@0 | 196 | const MSG_JAR_FLUSH = "AddonJarFlush"; |
michael@0 | 197 | |
michael@0 | 198 | var gGlobalScope = this; |
michael@0 | 199 | |
michael@0 | 200 | /** |
michael@0 | 201 | * Valid IDs fit this pattern. |
michael@0 | 202 | */ |
michael@0 | 203 | var gIDTest = /^(\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}|[a-z0-9-\._]*\@[a-z0-9-\._]+)$/i; |
michael@0 | 204 | |
michael@0 | 205 | Cu.import("resource://gre/modules/Log.jsm"); |
michael@0 | 206 | const LOGGER_ID = "addons.xpi"; |
michael@0 | 207 | |
michael@0 | 208 | // Create a new logger for use by all objects in this Addons XPI Provider module |
michael@0 | 209 | // (Requires AddonManager.jsm) |
michael@0 | 210 | let logger = Log.repository.getLogger(LOGGER_ID); |
michael@0 | 211 | |
michael@0 | 212 | const LAZY_OBJECTS = ["XPIDatabase"]; |
michael@0 | 213 | |
michael@0 | 214 | var gLazyObjectsLoaded = false; |
michael@0 | 215 | |
michael@0 | 216 | function loadLazyObjects() { |
michael@0 | 217 | let scope = {}; |
michael@0 | 218 | scope.AddonInternal = AddonInternal; |
michael@0 | 219 | scope.XPIProvider = XPIProvider; |
michael@0 | 220 | Services.scriptloader.loadSubScript("resource://gre/modules/addons/XPIProviderUtils.js", |
michael@0 | 221 | scope); |
michael@0 | 222 | |
michael@0 | 223 | for (let name of LAZY_OBJECTS) { |
michael@0 | 224 | delete gGlobalScope[name]; |
michael@0 | 225 | gGlobalScope[name] = scope[name]; |
michael@0 | 226 | } |
michael@0 | 227 | gLazyObjectsLoaded = true; |
michael@0 | 228 | return scope; |
michael@0 | 229 | } |
michael@0 | 230 | |
michael@0 | 231 | for (let name of LAZY_OBJECTS) { |
michael@0 | 232 | Object.defineProperty(gGlobalScope, name, { |
michael@0 | 233 | get: function lazyObjectGetter() { |
michael@0 | 234 | let objs = loadLazyObjects(); |
michael@0 | 235 | return objs[name]; |
michael@0 | 236 | }, |
michael@0 | 237 | configurable: true |
michael@0 | 238 | }); |
michael@0 | 239 | } |
michael@0 | 240 | |
michael@0 | 241 | |
michael@0 | 242 | function findMatchingStaticBlocklistItem(aAddon) { |
michael@0 | 243 | for (let item of STATIC_BLOCKLIST_PATTERNS) { |
michael@0 | 244 | if ("creator" in item && typeof item.creator == "string") { |
michael@0 | 245 | if ((aAddon.defaultLocale && aAddon.defaultLocale.creator == item.creator) || |
michael@0 | 246 | (aAddon.selectedLocale && aAddon.selectedLocale.creator == item.creator)) { |
michael@0 | 247 | return item; |
michael@0 | 248 | } |
michael@0 | 249 | } |
michael@0 | 250 | } |
michael@0 | 251 | return null; |
michael@0 | 252 | } |
michael@0 | 253 | |
michael@0 | 254 | |
michael@0 | 255 | /** |
michael@0 | 256 | * Sets permissions on a file |
michael@0 | 257 | * |
michael@0 | 258 | * @param aFile |
michael@0 | 259 | * The file or directory to operate on. |
michael@0 | 260 | * @param aPermissions |
michael@0 | 261 | * The permisions to set |
michael@0 | 262 | */ |
michael@0 | 263 | function setFilePermissions(aFile, aPermissions) { |
michael@0 | 264 | try { |
michael@0 | 265 | aFile.permissions = aPermissions; |
michael@0 | 266 | } |
michael@0 | 267 | catch (e) { |
michael@0 | 268 | logger.warn("Failed to set permissions " + aPermissions.toString(8) + " on " + |
michael@0 | 269 | aFile.path, e); |
michael@0 | 270 | } |
michael@0 | 271 | } |
michael@0 | 272 | |
michael@0 | 273 | /** |
michael@0 | 274 | * A safe way to install a file or the contents of a directory to a new |
michael@0 | 275 | * directory. The file or directory is moved or copied recursively and if |
michael@0 | 276 | * anything fails an attempt is made to rollback the entire operation. The |
michael@0 | 277 | * operation may also be rolled back to its original state after it has |
michael@0 | 278 | * completed by calling the rollback method. |
michael@0 | 279 | * |
michael@0 | 280 | * Operations can be chained. Calling move or copy multiple times will remember |
michael@0 | 281 | * the whole set and if one fails all of the operations will be rolled back. |
michael@0 | 282 | */ |
michael@0 | 283 | function SafeInstallOperation() { |
michael@0 | 284 | this._installedFiles = []; |
michael@0 | 285 | this._createdDirs = []; |
michael@0 | 286 | } |
michael@0 | 287 | |
michael@0 | 288 | SafeInstallOperation.prototype = { |
michael@0 | 289 | _installedFiles: null, |
michael@0 | 290 | _createdDirs: null, |
michael@0 | 291 | |
michael@0 | 292 | _installFile: function SIO_installFile(aFile, aTargetDirectory, aCopy) { |
michael@0 | 293 | let oldFile = aCopy ? null : aFile.clone(); |
michael@0 | 294 | let newFile = aFile.clone(); |
michael@0 | 295 | try { |
michael@0 | 296 | if (aCopy) |
michael@0 | 297 | newFile.copyTo(aTargetDirectory, null); |
michael@0 | 298 | else |
michael@0 | 299 | newFile.moveTo(aTargetDirectory, null); |
michael@0 | 300 | } |
michael@0 | 301 | catch (e) { |
michael@0 | 302 | logger.error("Failed to " + (aCopy ? "copy" : "move") + " file " + aFile.path + |
michael@0 | 303 | " to " + aTargetDirectory.path, e); |
michael@0 | 304 | throw e; |
michael@0 | 305 | } |
michael@0 | 306 | this._installedFiles.push({ oldFile: oldFile, newFile: newFile }); |
michael@0 | 307 | }, |
michael@0 | 308 | |
michael@0 | 309 | _installDirectory: function SIO_installDirectory(aDirectory, aTargetDirectory, aCopy) { |
michael@0 | 310 | let newDir = aTargetDirectory.clone(); |
michael@0 | 311 | newDir.append(aDirectory.leafName); |
michael@0 | 312 | try { |
michael@0 | 313 | newDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); |
michael@0 | 314 | } |
michael@0 | 315 | catch (e) { |
michael@0 | 316 | logger.error("Failed to create directory " + newDir.path, e); |
michael@0 | 317 | throw e; |
michael@0 | 318 | } |
michael@0 | 319 | this._createdDirs.push(newDir); |
michael@0 | 320 | |
michael@0 | 321 | // Use a snapshot of the directory contents to avoid possible issues with |
michael@0 | 322 | // iterating over a directory while removing files from it (the YAFFS2 |
michael@0 | 323 | // embedded filesystem has this issue, see bug 772238), and to remove |
michael@0 | 324 | // normal files before their resource forks on OSX (see bug 733436). |
michael@0 | 325 | let entries = getDirectoryEntries(aDirectory, true); |
michael@0 | 326 | entries.forEach(function(aEntry) { |
michael@0 | 327 | try { |
michael@0 | 328 | this._installDirEntry(aEntry, newDir, aCopy); |
michael@0 | 329 | } |
michael@0 | 330 | catch (e) { |
michael@0 | 331 | logger.error("Failed to " + (aCopy ? "copy" : "move") + " entry " + |
michael@0 | 332 | aEntry.path, e); |
michael@0 | 333 | throw e; |
michael@0 | 334 | } |
michael@0 | 335 | }, this); |
michael@0 | 336 | |
michael@0 | 337 | // If this is only a copy operation then there is nothing else to do |
michael@0 | 338 | if (aCopy) |
michael@0 | 339 | return; |
michael@0 | 340 | |
michael@0 | 341 | // The directory should be empty by this point. If it isn't this will throw |
michael@0 | 342 | // and all of the operations will be rolled back |
michael@0 | 343 | try { |
michael@0 | 344 | setFilePermissions(aDirectory, FileUtils.PERMS_DIRECTORY); |
michael@0 | 345 | aDirectory.remove(false); |
michael@0 | 346 | } |
michael@0 | 347 | catch (e) { |
michael@0 | 348 | logger.error("Failed to remove directory " + aDirectory.path, e); |
michael@0 | 349 | throw e; |
michael@0 | 350 | } |
michael@0 | 351 | |
michael@0 | 352 | // Note we put the directory move in after all the file moves so the |
michael@0 | 353 | // directory is recreated before all the files are moved back |
michael@0 | 354 | this._installedFiles.push({ oldFile: aDirectory, newFile: newDir }); |
michael@0 | 355 | }, |
michael@0 | 356 | |
michael@0 | 357 | _installDirEntry: function SIO_installDirEntry(aDirEntry, aTargetDirectory, aCopy) { |
michael@0 | 358 | let isDir = null; |
michael@0 | 359 | |
michael@0 | 360 | try { |
michael@0 | 361 | isDir = aDirEntry.isDirectory(); |
michael@0 | 362 | } |
michael@0 | 363 | catch (e) { |
michael@0 | 364 | // If the file has already gone away then don't worry about it, this can |
michael@0 | 365 | // happen on OSX where the resource fork is automatically moved with the |
michael@0 | 366 | // data fork for the file. See bug 733436. |
michael@0 | 367 | if (e.result == Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) |
michael@0 | 368 | return; |
michael@0 | 369 | |
michael@0 | 370 | logger.error("Failure " + (aCopy ? "copying" : "moving") + " " + aDirEntry.path + |
michael@0 | 371 | " to " + aTargetDirectory.path); |
michael@0 | 372 | throw e; |
michael@0 | 373 | } |
michael@0 | 374 | |
michael@0 | 375 | try { |
michael@0 | 376 | if (isDir) |
michael@0 | 377 | this._installDirectory(aDirEntry, aTargetDirectory, aCopy); |
michael@0 | 378 | else |
michael@0 | 379 | this._installFile(aDirEntry, aTargetDirectory, aCopy); |
michael@0 | 380 | } |
michael@0 | 381 | catch (e) { |
michael@0 | 382 | logger.error("Failure " + (aCopy ? "copying" : "moving") + " " + aDirEntry.path + |
michael@0 | 383 | " to " + aTargetDirectory.path); |
michael@0 | 384 | throw e; |
michael@0 | 385 | } |
michael@0 | 386 | }, |
michael@0 | 387 | |
michael@0 | 388 | /** |
michael@0 | 389 | * Moves a file or directory into a new directory. If an error occurs then all |
michael@0 | 390 | * files that have been moved will be moved back to their original location. |
michael@0 | 391 | * |
michael@0 | 392 | * @param aFile |
michael@0 | 393 | * The file or directory to be moved. |
michael@0 | 394 | * @param aTargetDirectory |
michael@0 | 395 | * The directory to move into, this is expected to be an empty |
michael@0 | 396 | * directory. |
michael@0 | 397 | */ |
michael@0 | 398 | move: function SIO_move(aFile, aTargetDirectory) { |
michael@0 | 399 | try { |
michael@0 | 400 | this._installDirEntry(aFile, aTargetDirectory, false); |
michael@0 | 401 | } |
michael@0 | 402 | catch (e) { |
michael@0 | 403 | this.rollback(); |
michael@0 | 404 | throw e; |
michael@0 | 405 | } |
michael@0 | 406 | }, |
michael@0 | 407 | |
michael@0 | 408 | /** |
michael@0 | 409 | * Copies a file or directory into a new directory. If an error occurs then |
michael@0 | 410 | * all new files that have been created will be removed. |
michael@0 | 411 | * |
michael@0 | 412 | * @param aFile |
michael@0 | 413 | * The file or directory to be copied. |
michael@0 | 414 | * @param aTargetDirectory |
michael@0 | 415 | * The directory to copy into, this is expected to be an empty |
michael@0 | 416 | * directory. |
michael@0 | 417 | */ |
michael@0 | 418 | copy: function SIO_copy(aFile, aTargetDirectory) { |
michael@0 | 419 | try { |
michael@0 | 420 | this._installDirEntry(aFile, aTargetDirectory, true); |
michael@0 | 421 | } |
michael@0 | 422 | catch (e) { |
michael@0 | 423 | this.rollback(); |
michael@0 | 424 | throw e; |
michael@0 | 425 | } |
michael@0 | 426 | }, |
michael@0 | 427 | |
michael@0 | 428 | /** |
michael@0 | 429 | * Rolls back all the moves that this operation performed. If an exception |
michael@0 | 430 | * occurs here then both old and new directories are left in an indeterminate |
michael@0 | 431 | * state |
michael@0 | 432 | */ |
michael@0 | 433 | rollback: function SIO_rollback() { |
michael@0 | 434 | while (this._installedFiles.length > 0) { |
michael@0 | 435 | let move = this._installedFiles.pop(); |
michael@0 | 436 | if (move.newFile.isDirectory()) { |
michael@0 | 437 | let oldDir = move.oldFile.parent.clone(); |
michael@0 | 438 | oldDir.append(move.oldFile.leafName); |
michael@0 | 439 | oldDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); |
michael@0 | 440 | } |
michael@0 | 441 | else if (!move.oldFile) { |
michael@0 | 442 | // No old file means this was a copied file |
michael@0 | 443 | move.newFile.remove(true); |
michael@0 | 444 | } |
michael@0 | 445 | else { |
michael@0 | 446 | move.newFile.moveTo(move.oldFile.parent, null); |
michael@0 | 447 | } |
michael@0 | 448 | } |
michael@0 | 449 | |
michael@0 | 450 | while (this._createdDirs.length > 0) |
michael@0 | 451 | recursiveRemove(this._createdDirs.pop()); |
michael@0 | 452 | } |
michael@0 | 453 | }; |
michael@0 | 454 | |
michael@0 | 455 | /** |
michael@0 | 456 | * Gets the currently selected locale for display. |
michael@0 | 457 | * @return the selected locale or "en-US" if none is selected |
michael@0 | 458 | */ |
michael@0 | 459 | function getLocale() { |
michael@0 | 460 | if (Prefs.getBoolPref(PREF_MATCH_OS_LOCALE, false)) |
michael@0 | 461 | return Services.locale.getLocaleComponentForUserAgent(); |
michael@0 | 462 | let locale = Prefs.getComplexValue(PREF_SELECTED_LOCALE, Ci.nsIPrefLocalizedString); |
michael@0 | 463 | if (locale) |
michael@0 | 464 | return locale; |
michael@0 | 465 | return Prefs.getCharPref(PREF_SELECTED_LOCALE, "en-US"); |
michael@0 | 466 | } |
michael@0 | 467 | |
michael@0 | 468 | /** |
michael@0 | 469 | * Selects the closest matching locale from a list of locales. |
michael@0 | 470 | * |
michael@0 | 471 | * @param aLocales |
michael@0 | 472 | * An array of locales |
michael@0 | 473 | * @return the best match for the currently selected locale |
michael@0 | 474 | */ |
michael@0 | 475 | function findClosestLocale(aLocales) { |
michael@0 | 476 | let appLocale = getLocale(); |
michael@0 | 477 | |
michael@0 | 478 | // Holds the best matching localized resource |
michael@0 | 479 | var bestmatch = null; |
michael@0 | 480 | // The number of locale parts it matched with |
michael@0 | 481 | var bestmatchcount = 0; |
michael@0 | 482 | // The number of locale parts in the match |
michael@0 | 483 | var bestpartcount = 0; |
michael@0 | 484 | |
michael@0 | 485 | var matchLocales = [appLocale.toLowerCase()]; |
michael@0 | 486 | /* If the current locale is English then it will find a match if there is |
michael@0 | 487 | a valid match for en-US so no point searching that locale too. */ |
michael@0 | 488 | if (matchLocales[0].substring(0, 3) != "en-") |
michael@0 | 489 | matchLocales.push("en-us"); |
michael@0 | 490 | |
michael@0 | 491 | for each (var locale in matchLocales) { |
michael@0 | 492 | var lparts = locale.split("-"); |
michael@0 | 493 | for each (var localized in aLocales) { |
michael@0 | 494 | for each (let found in localized.locales) { |
michael@0 | 495 | found = found.toLowerCase(); |
michael@0 | 496 | // Exact match is returned immediately |
michael@0 | 497 | if (locale == found) |
michael@0 | 498 | return localized; |
michael@0 | 499 | |
michael@0 | 500 | var fparts = found.split("-"); |
michael@0 | 501 | /* If we have found a possible match and this one isn't any longer |
michael@0 | 502 | then we dont need to check further. */ |
michael@0 | 503 | if (bestmatch && fparts.length < bestmatchcount) |
michael@0 | 504 | continue; |
michael@0 | 505 | |
michael@0 | 506 | // Count the number of parts that match |
michael@0 | 507 | var maxmatchcount = Math.min(fparts.length, lparts.length); |
michael@0 | 508 | var matchcount = 0; |
michael@0 | 509 | while (matchcount < maxmatchcount && |
michael@0 | 510 | fparts[matchcount] == lparts[matchcount]) |
michael@0 | 511 | matchcount++; |
michael@0 | 512 | |
michael@0 | 513 | /* If we matched more than the last best match or matched the same and |
michael@0 | 514 | this locale is less specific than the last best match. */ |
michael@0 | 515 | if (matchcount > bestmatchcount || |
michael@0 | 516 | (matchcount == bestmatchcount && fparts.length < bestpartcount)) { |
michael@0 | 517 | bestmatch = localized; |
michael@0 | 518 | bestmatchcount = matchcount; |
michael@0 | 519 | bestpartcount = fparts.length; |
michael@0 | 520 | } |
michael@0 | 521 | } |
michael@0 | 522 | } |
michael@0 | 523 | // If we found a valid match for this locale return it |
michael@0 | 524 | if (bestmatch) |
michael@0 | 525 | return bestmatch; |
michael@0 | 526 | } |
michael@0 | 527 | return null; |
michael@0 | 528 | } |
michael@0 | 529 | |
michael@0 | 530 | /** |
michael@0 | 531 | * Sets the userDisabled and softDisabled properties of an add-on based on what |
michael@0 | 532 | * values those properties had for a previous instance of the add-on. The |
michael@0 | 533 | * previous instance may be a previous install or in the case of an application |
michael@0 | 534 | * version change the same add-on. |
michael@0 | 535 | * |
michael@0 | 536 | * NOTE: this may modify aNewAddon in place; callers should save the database if |
michael@0 | 537 | * necessary |
michael@0 | 538 | * |
michael@0 | 539 | * @param aOldAddon |
michael@0 | 540 | * The previous instance of the add-on |
michael@0 | 541 | * @param aNewAddon |
michael@0 | 542 | * The new instance of the add-on |
michael@0 | 543 | * @param aAppVersion |
michael@0 | 544 | * The optional application version to use when checking the blocklist |
michael@0 | 545 | * or undefined to use the current application |
michael@0 | 546 | * @param aPlatformVersion |
michael@0 | 547 | * The optional platform version to use when checking the blocklist or |
michael@0 | 548 | * undefined to use the current platform |
michael@0 | 549 | */ |
michael@0 | 550 | function applyBlocklistChanges(aOldAddon, aNewAddon, aOldAppVersion, |
michael@0 | 551 | aOldPlatformVersion) { |
michael@0 | 552 | // Copy the properties by default |
michael@0 | 553 | aNewAddon.userDisabled = aOldAddon.userDisabled; |
michael@0 | 554 | aNewAddon.softDisabled = aOldAddon.softDisabled; |
michael@0 | 555 | |
michael@0 | 556 | let bs = Cc["@mozilla.org/extensions/blocklist;1"]. |
michael@0 | 557 | getService(Ci.nsIBlocklistService); |
michael@0 | 558 | |
michael@0 | 559 | let oldBlocklistState = bs.getAddonBlocklistState(createWrapper(aOldAddon), |
michael@0 | 560 | aOldAppVersion, |
michael@0 | 561 | aOldPlatformVersion); |
michael@0 | 562 | let newBlocklistState = bs.getAddonBlocklistState(createWrapper(aNewAddon)); |
michael@0 | 563 | |
michael@0 | 564 | // If the blocklist state hasn't changed then the properties don't need to |
michael@0 | 565 | // change |
michael@0 | 566 | if (newBlocklistState == oldBlocklistState) |
michael@0 | 567 | return; |
michael@0 | 568 | |
michael@0 | 569 | if (newBlocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED) { |
michael@0 | 570 | if (aNewAddon.type != "theme") { |
michael@0 | 571 | // The add-on has become softblocked, set softDisabled if it isn't already |
michael@0 | 572 | // userDisabled |
michael@0 | 573 | aNewAddon.softDisabled = !aNewAddon.userDisabled; |
michael@0 | 574 | } |
michael@0 | 575 | else { |
michael@0 | 576 | // Themes just get userDisabled to switch back to the default theme |
michael@0 | 577 | aNewAddon.userDisabled = true; |
michael@0 | 578 | } |
michael@0 | 579 | } |
michael@0 | 580 | else { |
michael@0 | 581 | // If the new add-on is not softblocked then it cannot be softDisabled |
michael@0 | 582 | aNewAddon.softDisabled = false; |
michael@0 | 583 | } |
michael@0 | 584 | } |
michael@0 | 585 | |
michael@0 | 586 | /** |
michael@0 | 587 | * Calculates whether an add-on should be appDisabled or not. |
michael@0 | 588 | * |
michael@0 | 589 | * @param aAddon |
michael@0 | 590 | * The add-on to check |
michael@0 | 591 | * @return true if the add-on should not be appDisabled |
michael@0 | 592 | */ |
michael@0 | 593 | function isUsableAddon(aAddon) { |
michael@0 | 594 | // Hack to ensure the default theme is always usable |
michael@0 | 595 | if (aAddon.type == "theme" && aAddon.internalName == XPIProvider.defaultSkin) |
michael@0 | 596 | return true; |
michael@0 | 597 | |
michael@0 | 598 | if (aAddon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) |
michael@0 | 599 | return false; |
michael@0 | 600 | |
michael@0 | 601 | if (AddonManager.checkUpdateSecurity && !aAddon.providesUpdatesSecurely) |
michael@0 | 602 | return false; |
michael@0 | 603 | |
michael@0 | 604 | if (!aAddon.isPlatformCompatible) |
michael@0 | 605 | return false; |
michael@0 | 606 | |
michael@0 | 607 | if (AddonManager.checkCompatibility) { |
michael@0 | 608 | if (!aAddon.isCompatible) |
michael@0 | 609 | return false; |
michael@0 | 610 | } |
michael@0 | 611 | else { |
michael@0 | 612 | let app = aAddon.matchingTargetApplication; |
michael@0 | 613 | if (!app) |
michael@0 | 614 | return false; |
michael@0 | 615 | |
michael@0 | 616 | // XXX Temporary solution to let applications opt-in to make themes safer |
michael@0 | 617 | // following significant UI changes even if checkCompatibility=false has |
michael@0 | 618 | // been set, until we get bug 962001. |
michael@0 | 619 | if (aAddon.type == "theme" && app.id == Services.appinfo.ID) { |
michael@0 | 620 | try { |
michael@0 | 621 | let minCompatVersion = Services.prefs.getCharPref(PREF_CHECKCOMAT_THEMEOVERRIDE); |
michael@0 | 622 | if (minCompatVersion && |
michael@0 | 623 | Services.vc.compare(minCompatVersion, app.maxVersion) > 0) { |
michael@0 | 624 | return false; |
michael@0 | 625 | } |
michael@0 | 626 | } catch (e) {} |
michael@0 | 627 | } |
michael@0 | 628 | } |
michael@0 | 629 | |
michael@0 | 630 | return true; |
michael@0 | 631 | } |
michael@0 | 632 | |
michael@0 | 633 | function isAddonDisabled(aAddon) { |
michael@0 | 634 | return aAddon.appDisabled || aAddon.softDisabled || aAddon.userDisabled; |
michael@0 | 635 | } |
michael@0 | 636 | |
michael@0 | 637 | XPCOMUtils.defineLazyServiceGetter(this, "gRDF", "@mozilla.org/rdf/rdf-service;1", |
michael@0 | 638 | Ci.nsIRDFService); |
michael@0 | 639 | |
michael@0 | 640 | function EM_R(aProperty) { |
michael@0 | 641 | return gRDF.GetResource(PREFIX_NS_EM + aProperty); |
michael@0 | 642 | } |
michael@0 | 643 | |
michael@0 | 644 | /** |
michael@0 | 645 | * Converts an RDF literal, resource or integer into a string. |
michael@0 | 646 | * |
michael@0 | 647 | * @param aLiteral |
michael@0 | 648 | * The RDF object to convert |
michael@0 | 649 | * @return a string if the object could be converted or null |
michael@0 | 650 | */ |
michael@0 | 651 | function getRDFValue(aLiteral) { |
michael@0 | 652 | if (aLiteral instanceof Ci.nsIRDFLiteral) |
michael@0 | 653 | return aLiteral.Value; |
michael@0 | 654 | if (aLiteral instanceof Ci.nsIRDFResource) |
michael@0 | 655 | return aLiteral.Value; |
michael@0 | 656 | if (aLiteral instanceof Ci.nsIRDFInt) |
michael@0 | 657 | return aLiteral.Value; |
michael@0 | 658 | return null; |
michael@0 | 659 | } |
michael@0 | 660 | |
michael@0 | 661 | /** |
michael@0 | 662 | * Gets an RDF property as a string |
michael@0 | 663 | * |
michael@0 | 664 | * @param aDs |
michael@0 | 665 | * The RDF datasource to read the property from |
michael@0 | 666 | * @param aResource |
michael@0 | 667 | * The RDF resource to read the property from |
michael@0 | 668 | * @param aProperty |
michael@0 | 669 | * The property to read |
michael@0 | 670 | * @return a string if the property existed or null |
michael@0 | 671 | */ |
michael@0 | 672 | function getRDFProperty(aDs, aResource, aProperty) { |
michael@0 | 673 | return getRDFValue(aDs.GetTarget(aResource, EM_R(aProperty), true)); |
michael@0 | 674 | } |
michael@0 | 675 | |
michael@0 | 676 | /** |
michael@0 | 677 | * Reads an AddonInternal object from an RDF stream. |
michael@0 | 678 | * |
michael@0 | 679 | * @param aUri |
michael@0 | 680 | * The URI that the manifest is being read from |
michael@0 | 681 | * @param aStream |
michael@0 | 682 | * An open stream to read the RDF from |
michael@0 | 683 | * @return an AddonInternal object |
michael@0 | 684 | * @throws if the install manifest in the RDF stream is corrupt or could not |
michael@0 | 685 | * be read |
michael@0 | 686 | */ |
michael@0 | 687 | function loadManifestFromRDF(aUri, aStream) { |
michael@0 | 688 | function getPropertyArray(aDs, aSource, aProperty) { |
michael@0 | 689 | let values = []; |
michael@0 | 690 | let targets = aDs.GetTargets(aSource, EM_R(aProperty), true); |
michael@0 | 691 | while (targets.hasMoreElements()) |
michael@0 | 692 | values.push(getRDFValue(targets.getNext())); |
michael@0 | 693 | |
michael@0 | 694 | return values; |
michael@0 | 695 | } |
michael@0 | 696 | |
michael@0 | 697 | /** |
michael@0 | 698 | * Reads locale properties from either the main install manifest root or |
michael@0 | 699 | * an em:localized section in the install manifest. |
michael@0 | 700 | * |
michael@0 | 701 | * @param aDs |
michael@0 | 702 | * The nsIRDFDatasource to read from |
michael@0 | 703 | * @param aSource |
michael@0 | 704 | * The nsIRDFResource to read the properties from |
michael@0 | 705 | * @param isDefault |
michael@0 | 706 | * True if the locale is to be read from the main install manifest |
michael@0 | 707 | * root |
michael@0 | 708 | * @param aSeenLocales |
michael@0 | 709 | * An array of locale names already seen for this install manifest. |
michael@0 | 710 | * Any locale names seen as a part of this function will be added to |
michael@0 | 711 | * this array |
michael@0 | 712 | * @return an object containing the locale properties |
michael@0 | 713 | */ |
michael@0 | 714 | function readLocale(aDs, aSource, isDefault, aSeenLocales) { |
michael@0 | 715 | let locale = { }; |
michael@0 | 716 | if (!isDefault) { |
michael@0 | 717 | locale.locales = []; |
michael@0 | 718 | let targets = ds.GetTargets(aSource, EM_R("locale"), true); |
michael@0 | 719 | while (targets.hasMoreElements()) { |
michael@0 | 720 | let localeName = getRDFValue(targets.getNext()); |
michael@0 | 721 | if (!localeName) { |
michael@0 | 722 | logger.warn("Ignoring empty locale in localized properties"); |
michael@0 | 723 | continue; |
michael@0 | 724 | } |
michael@0 | 725 | if (aSeenLocales.indexOf(localeName) != -1) { |
michael@0 | 726 | logger.warn("Ignoring duplicate locale in localized properties"); |
michael@0 | 727 | continue; |
michael@0 | 728 | } |
michael@0 | 729 | aSeenLocales.push(localeName); |
michael@0 | 730 | locale.locales.push(localeName); |
michael@0 | 731 | } |
michael@0 | 732 | |
michael@0 | 733 | if (locale.locales.length == 0) { |
michael@0 | 734 | logger.warn("Ignoring localized properties with no listed locales"); |
michael@0 | 735 | return null; |
michael@0 | 736 | } |
michael@0 | 737 | } |
michael@0 | 738 | |
michael@0 | 739 | PROP_LOCALE_SINGLE.forEach(function(aProp) { |
michael@0 | 740 | locale[aProp] = getRDFProperty(aDs, aSource, aProp); |
michael@0 | 741 | }); |
michael@0 | 742 | |
michael@0 | 743 | PROP_LOCALE_MULTI.forEach(function(aProp) { |
michael@0 | 744 | // Don't store empty arrays |
michael@0 | 745 | let props = getPropertyArray(aDs, aSource, |
michael@0 | 746 | aProp.substring(0, aProp.length - 1)); |
michael@0 | 747 | if (props.length > 0) |
michael@0 | 748 | locale[aProp] = props; |
michael@0 | 749 | }); |
michael@0 | 750 | |
michael@0 | 751 | return locale; |
michael@0 | 752 | } |
michael@0 | 753 | |
michael@0 | 754 | let rdfParser = Cc["@mozilla.org/rdf/xml-parser;1"]. |
michael@0 | 755 | createInstance(Ci.nsIRDFXMLParser) |
michael@0 | 756 | let ds = Cc["@mozilla.org/rdf/datasource;1?name=in-memory-datasource"]. |
michael@0 | 757 | createInstance(Ci.nsIRDFDataSource); |
michael@0 | 758 | let listener = rdfParser.parseAsync(ds, aUri); |
michael@0 | 759 | let channel = Cc["@mozilla.org/network/input-stream-channel;1"]. |
michael@0 | 760 | createInstance(Ci.nsIInputStreamChannel); |
michael@0 | 761 | channel.setURI(aUri); |
michael@0 | 762 | channel.contentStream = aStream; |
michael@0 | 763 | channel.QueryInterface(Ci.nsIChannel); |
michael@0 | 764 | channel.contentType = "text/xml"; |
michael@0 | 765 | |
michael@0 | 766 | listener.onStartRequest(channel, null); |
michael@0 | 767 | |
michael@0 | 768 | try { |
michael@0 | 769 | let pos = 0; |
michael@0 | 770 | let count = aStream.available(); |
michael@0 | 771 | while (count > 0) { |
michael@0 | 772 | listener.onDataAvailable(channel, null, aStream, pos, count); |
michael@0 | 773 | pos += count; |
michael@0 | 774 | count = aStream.available(); |
michael@0 | 775 | } |
michael@0 | 776 | listener.onStopRequest(channel, null, Components.results.NS_OK); |
michael@0 | 777 | } |
michael@0 | 778 | catch (e) { |
michael@0 | 779 | listener.onStopRequest(channel, null, e.result); |
michael@0 | 780 | throw e; |
michael@0 | 781 | } |
michael@0 | 782 | |
michael@0 | 783 | let root = gRDF.GetResource(RDFURI_INSTALL_MANIFEST_ROOT); |
michael@0 | 784 | let addon = new AddonInternal(); |
michael@0 | 785 | PROP_METADATA.forEach(function(aProp) { |
michael@0 | 786 | addon[aProp] = getRDFProperty(ds, root, aProp); |
michael@0 | 787 | }); |
michael@0 | 788 | addon.unpack = getRDFProperty(ds, root, "unpack") == "true"; |
michael@0 | 789 | |
michael@0 | 790 | if (!addon.type) { |
michael@0 | 791 | addon.type = addon.internalName ? "theme" : "extension"; |
michael@0 | 792 | } |
michael@0 | 793 | else { |
michael@0 | 794 | let type = addon.type; |
michael@0 | 795 | addon.type = null; |
michael@0 | 796 | for (let name in TYPES) { |
michael@0 | 797 | if (TYPES[name] == type) { |
michael@0 | 798 | addon.type = name; |
michael@0 | 799 | break; |
michael@0 | 800 | } |
michael@0 | 801 | } |
michael@0 | 802 | } |
michael@0 | 803 | |
michael@0 | 804 | if (!(addon.type in TYPES)) |
michael@0 | 805 | throw new Error("Install manifest specifies unknown type: " + addon.type); |
michael@0 | 806 | |
michael@0 | 807 | if (addon.type != "multipackage") { |
michael@0 | 808 | if (!addon.id) |
michael@0 | 809 | throw new Error("No ID in install manifest"); |
michael@0 | 810 | if (!gIDTest.test(addon.id)) |
michael@0 | 811 | throw new Error("Illegal add-on ID " + addon.id); |
michael@0 | 812 | if (!addon.version) |
michael@0 | 813 | throw new Error("No version in install manifest"); |
michael@0 | 814 | } |
michael@0 | 815 | |
michael@0 | 816 | addon.strictCompatibility = !(addon.type in COMPATIBLE_BY_DEFAULT_TYPES) || |
michael@0 | 817 | getRDFProperty(ds, root, "strictCompatibility") == "true"; |
michael@0 | 818 | |
michael@0 | 819 | // Only read the bootstrap property for extensions. |
michael@0 | 820 | if (addon.type == "extension") { |
michael@0 | 821 | addon.bootstrap = getRDFProperty(ds, root, "bootstrap") == "true"; |
michael@0 | 822 | if (addon.optionsType && |
michael@0 | 823 | addon.optionsType != AddonManager.OPTIONS_TYPE_DIALOG && |
michael@0 | 824 | addon.optionsType != AddonManager.OPTIONS_TYPE_INLINE && |
michael@0 | 825 | addon.optionsType != AddonManager.OPTIONS_TYPE_TAB && |
michael@0 | 826 | addon.optionsType != AddonManager.OPTIONS_TYPE_INLINE_INFO) { |
michael@0 | 827 | throw new Error("Install manifest specifies unknown type: " + addon.optionsType); |
michael@0 | 828 | } |
michael@0 | 829 | } |
michael@0 | 830 | else { |
michael@0 | 831 | // Some add-on types are always restartless. |
michael@0 | 832 | if (RESTARTLESS_TYPES.has(addon.type)) { |
michael@0 | 833 | addon.bootstrap = true; |
michael@0 | 834 | } |
michael@0 | 835 | |
michael@0 | 836 | // Only extensions are allowed to provide an optionsURL, optionsType or aboutURL. For |
michael@0 | 837 | // all other types they are silently ignored |
michael@0 | 838 | addon.optionsURL = null; |
michael@0 | 839 | addon.optionsType = null; |
michael@0 | 840 | addon.aboutURL = null; |
michael@0 | 841 | |
michael@0 | 842 | if (addon.type == "theme") { |
michael@0 | 843 | if (!addon.internalName) |
michael@0 | 844 | throw new Error("Themes must include an internalName property"); |
michael@0 | 845 | addon.skinnable = getRDFProperty(ds, root, "skinnable") == "true"; |
michael@0 | 846 | } |
michael@0 | 847 | } |
michael@0 | 848 | |
michael@0 | 849 | addon.defaultLocale = readLocale(ds, root, true); |
michael@0 | 850 | |
michael@0 | 851 | let seenLocales = []; |
michael@0 | 852 | addon.locales = []; |
michael@0 | 853 | let targets = ds.GetTargets(root, EM_R("localized"), true); |
michael@0 | 854 | while (targets.hasMoreElements()) { |
michael@0 | 855 | let target = targets.getNext().QueryInterface(Ci.nsIRDFResource); |
michael@0 | 856 | let locale = readLocale(ds, target, false, seenLocales); |
michael@0 | 857 | if (locale) |
michael@0 | 858 | addon.locales.push(locale); |
michael@0 | 859 | } |
michael@0 | 860 | |
michael@0 | 861 | let seenApplications = []; |
michael@0 | 862 | addon.targetApplications = []; |
michael@0 | 863 | targets = ds.GetTargets(root, EM_R("targetApplication"), true); |
michael@0 | 864 | while (targets.hasMoreElements()) { |
michael@0 | 865 | let target = targets.getNext().QueryInterface(Ci.nsIRDFResource); |
michael@0 | 866 | let targetAppInfo = {}; |
michael@0 | 867 | PROP_TARGETAPP.forEach(function(aProp) { |
michael@0 | 868 | targetAppInfo[aProp] = getRDFProperty(ds, target, aProp); |
michael@0 | 869 | }); |
michael@0 | 870 | if (!targetAppInfo.id || !targetAppInfo.minVersion || |
michael@0 | 871 | !targetAppInfo.maxVersion) { |
michael@0 | 872 | logger.warn("Ignoring invalid targetApplication entry in install manifest"); |
michael@0 | 873 | continue; |
michael@0 | 874 | } |
michael@0 | 875 | if (seenApplications.indexOf(targetAppInfo.id) != -1) { |
michael@0 | 876 | logger.warn("Ignoring duplicate targetApplication entry for " + targetAppInfo.id + |
michael@0 | 877 | " in install manifest"); |
michael@0 | 878 | continue; |
michael@0 | 879 | } |
michael@0 | 880 | seenApplications.push(targetAppInfo.id); |
michael@0 | 881 | addon.targetApplications.push(targetAppInfo); |
michael@0 | 882 | } |
michael@0 | 883 | |
michael@0 | 884 | // Note that we don't need to check for duplicate targetPlatform entries since |
michael@0 | 885 | // the RDF service coalesces them for us. |
michael@0 | 886 | let targetPlatforms = getPropertyArray(ds, root, "targetPlatform"); |
michael@0 | 887 | addon.targetPlatforms = []; |
michael@0 | 888 | targetPlatforms.forEach(function(aPlatform) { |
michael@0 | 889 | let platform = { |
michael@0 | 890 | os: null, |
michael@0 | 891 | abi: null |
michael@0 | 892 | }; |
michael@0 | 893 | |
michael@0 | 894 | let pos = aPlatform.indexOf("_"); |
michael@0 | 895 | if (pos != -1) { |
michael@0 | 896 | platform.os = aPlatform.substring(0, pos); |
michael@0 | 897 | platform.abi = aPlatform.substring(pos + 1); |
michael@0 | 898 | } |
michael@0 | 899 | else { |
michael@0 | 900 | platform.os = aPlatform; |
michael@0 | 901 | } |
michael@0 | 902 | |
michael@0 | 903 | addon.targetPlatforms.push(platform); |
michael@0 | 904 | }); |
michael@0 | 905 | |
michael@0 | 906 | // A theme's userDisabled value is true if the theme is not the selected skin |
michael@0 | 907 | // or if there is an active lightweight theme. We ignore whether softblocking |
michael@0 | 908 | // is in effect since it would change the active theme. |
michael@0 | 909 | if (addon.type == "theme") { |
michael@0 | 910 | addon.userDisabled = !!LightweightThemeManager.currentTheme || |
michael@0 | 911 | addon.internalName != XPIProvider.selectedSkin; |
michael@0 | 912 | } |
michael@0 | 913 | // Experiments are disabled by default. It is up to the Experiments Manager |
michael@0 | 914 | // to enable them (it drives installation). |
michael@0 | 915 | else if (addon.type == "experiment") { |
michael@0 | 916 | addon.userDisabled = true; |
michael@0 | 917 | } |
michael@0 | 918 | else { |
michael@0 | 919 | addon.userDisabled = false; |
michael@0 | 920 | addon.softDisabled = addon.blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED; |
michael@0 | 921 | } |
michael@0 | 922 | |
michael@0 | 923 | addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT; |
michael@0 | 924 | |
michael@0 | 925 | // Experiments are managed and updated through an external "experiments |
michael@0 | 926 | // manager." So disable some built-in mechanisms. |
michael@0 | 927 | if (addon.type == "experiment") { |
michael@0 | 928 | addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DISABLE; |
michael@0 | 929 | addon.updateURL = null; |
michael@0 | 930 | addon.updateKey = null; |
michael@0 | 931 | |
michael@0 | 932 | addon.targetApplications = []; |
michael@0 | 933 | addon.targetPlatforms = []; |
michael@0 | 934 | } |
michael@0 | 935 | |
michael@0 | 936 | // Load the storage service before NSS (nsIRandomGenerator), |
michael@0 | 937 | // to avoid a SQLite initialization error (bug 717904). |
michael@0 | 938 | let storage = Services.storage; |
michael@0 | 939 | |
michael@0 | 940 | // Generate random GUID used for Sync. |
michael@0 | 941 | // This was lifted from util.js:makeGUID() from services-sync. |
michael@0 | 942 | let rng = Cc["@mozilla.org/security/random-generator;1"]. |
michael@0 | 943 | createInstance(Ci.nsIRandomGenerator); |
michael@0 | 944 | let bytes = rng.generateRandomBytes(9); |
michael@0 | 945 | let byte_string = [String.fromCharCode(byte) for each (byte in bytes)] |
michael@0 | 946 | .join(""); |
michael@0 | 947 | // Base64 encode |
michael@0 | 948 | addon.syncGUID = btoa(byte_string).replace(/\+/g, '-') |
michael@0 | 949 | .replace(/\//g, '_'); |
michael@0 | 950 | |
michael@0 | 951 | return addon; |
michael@0 | 952 | } |
michael@0 | 953 | |
michael@0 | 954 | /** |
michael@0 | 955 | * Loads an AddonInternal object from an add-on extracted in a directory. |
michael@0 | 956 | * |
michael@0 | 957 | * @param aDir |
michael@0 | 958 | * The nsIFile directory holding the add-on |
michael@0 | 959 | * @return an AddonInternal object |
michael@0 | 960 | * @throws if the directory does not contain a valid install manifest |
michael@0 | 961 | */ |
michael@0 | 962 | function loadManifestFromDir(aDir) { |
michael@0 | 963 | function getFileSize(aFile) { |
michael@0 | 964 | if (aFile.isSymlink()) |
michael@0 | 965 | return 0; |
michael@0 | 966 | |
michael@0 | 967 | if (!aFile.isDirectory()) |
michael@0 | 968 | return aFile.fileSize; |
michael@0 | 969 | |
michael@0 | 970 | let size = 0; |
michael@0 | 971 | let entries = aFile.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator); |
michael@0 | 972 | let entry; |
michael@0 | 973 | while ((entry = entries.nextFile)) |
michael@0 | 974 | size += getFileSize(entry); |
michael@0 | 975 | entries.close(); |
michael@0 | 976 | return size; |
michael@0 | 977 | } |
michael@0 | 978 | |
michael@0 | 979 | let file = aDir.clone(); |
michael@0 | 980 | file.append(FILE_INSTALL_MANIFEST); |
michael@0 | 981 | if (!file.exists() || !file.isFile()) |
michael@0 | 982 | throw new Error("Directory " + aDir.path + " does not contain a valid " + |
michael@0 | 983 | "install manifest"); |
michael@0 | 984 | |
michael@0 | 985 | let fis = Cc["@mozilla.org/network/file-input-stream;1"]. |
michael@0 | 986 | createInstance(Ci.nsIFileInputStream); |
michael@0 | 987 | fis.init(file, -1, -1, false); |
michael@0 | 988 | let bis = Cc["@mozilla.org/network/buffered-input-stream;1"]. |
michael@0 | 989 | createInstance(Ci.nsIBufferedInputStream); |
michael@0 | 990 | bis.init(fis, 4096); |
michael@0 | 991 | |
michael@0 | 992 | try { |
michael@0 | 993 | let addon = loadManifestFromRDF(Services.io.newFileURI(file), bis); |
michael@0 | 994 | addon._sourceBundle = aDir.clone(); |
michael@0 | 995 | addon.size = getFileSize(aDir); |
michael@0 | 996 | |
michael@0 | 997 | file = aDir.clone(); |
michael@0 | 998 | file.append("chrome.manifest"); |
michael@0 | 999 | let chromeManifest = ChromeManifestParser.parseSync(Services.io.newFileURI(file)); |
michael@0 | 1000 | addon.hasBinaryComponents = ChromeManifestParser.hasType(chromeManifest, |
michael@0 | 1001 | "binary-component"); |
michael@0 | 1002 | |
michael@0 | 1003 | addon.appDisabled = !isUsableAddon(addon); |
michael@0 | 1004 | return addon; |
michael@0 | 1005 | } |
michael@0 | 1006 | finally { |
michael@0 | 1007 | bis.close(); |
michael@0 | 1008 | fis.close(); |
michael@0 | 1009 | } |
michael@0 | 1010 | } |
michael@0 | 1011 | |
michael@0 | 1012 | /** |
michael@0 | 1013 | * Loads an AddonInternal object from an nsIZipReader for an add-on. |
michael@0 | 1014 | * |
michael@0 | 1015 | * @param aZipReader |
michael@0 | 1016 | * An open nsIZipReader for the add-on's files |
michael@0 | 1017 | * @return an AddonInternal object |
michael@0 | 1018 | * @throws if the XPI file does not contain a valid install manifest |
michael@0 | 1019 | */ |
michael@0 | 1020 | function loadManifestFromZipReader(aZipReader) { |
michael@0 | 1021 | let zis = aZipReader.getInputStream(FILE_INSTALL_MANIFEST); |
michael@0 | 1022 | let bis = Cc["@mozilla.org/network/buffered-input-stream;1"]. |
michael@0 | 1023 | createInstance(Ci.nsIBufferedInputStream); |
michael@0 | 1024 | bis.init(zis, 4096); |
michael@0 | 1025 | |
michael@0 | 1026 | try { |
michael@0 | 1027 | let uri = buildJarURI(aZipReader.file, FILE_INSTALL_MANIFEST); |
michael@0 | 1028 | let addon = loadManifestFromRDF(uri, bis); |
michael@0 | 1029 | addon._sourceBundle = aZipReader.file; |
michael@0 | 1030 | |
michael@0 | 1031 | addon.size = 0; |
michael@0 | 1032 | let entries = aZipReader.findEntries(null); |
michael@0 | 1033 | while (entries.hasMore()) |
michael@0 | 1034 | addon.size += aZipReader.getEntry(entries.getNext()).realSize; |
michael@0 | 1035 | |
michael@0 | 1036 | // Binary components can only be loaded from unpacked addons. |
michael@0 | 1037 | if (addon.unpack) { |
michael@0 | 1038 | uri = buildJarURI(aZipReader.file, "chrome.manifest"); |
michael@0 | 1039 | let chromeManifest = ChromeManifestParser.parseSync(uri); |
michael@0 | 1040 | addon.hasBinaryComponents = ChromeManifestParser.hasType(chromeManifest, |
michael@0 | 1041 | "binary-component"); |
michael@0 | 1042 | } else { |
michael@0 | 1043 | addon.hasBinaryComponents = false; |
michael@0 | 1044 | } |
michael@0 | 1045 | |
michael@0 | 1046 | addon.appDisabled = !isUsableAddon(addon); |
michael@0 | 1047 | return addon; |
michael@0 | 1048 | } |
michael@0 | 1049 | finally { |
michael@0 | 1050 | bis.close(); |
michael@0 | 1051 | zis.close(); |
michael@0 | 1052 | } |
michael@0 | 1053 | } |
michael@0 | 1054 | |
michael@0 | 1055 | /** |
michael@0 | 1056 | * Loads an AddonInternal object from an add-on in an XPI file. |
michael@0 | 1057 | * |
michael@0 | 1058 | * @param aXPIFile |
michael@0 | 1059 | * An nsIFile pointing to the add-on's XPI file |
michael@0 | 1060 | * @return an AddonInternal object |
michael@0 | 1061 | * @throws if the XPI file does not contain a valid install manifest |
michael@0 | 1062 | */ |
michael@0 | 1063 | function loadManifestFromZipFile(aXPIFile) { |
michael@0 | 1064 | let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"]. |
michael@0 | 1065 | createInstance(Ci.nsIZipReader); |
michael@0 | 1066 | try { |
michael@0 | 1067 | zipReader.open(aXPIFile); |
michael@0 | 1068 | |
michael@0 | 1069 | return loadManifestFromZipReader(zipReader); |
michael@0 | 1070 | } |
michael@0 | 1071 | finally { |
michael@0 | 1072 | zipReader.close(); |
michael@0 | 1073 | } |
michael@0 | 1074 | } |
michael@0 | 1075 | |
michael@0 | 1076 | function loadManifestFromFile(aFile) { |
michael@0 | 1077 | if (aFile.isFile()) |
michael@0 | 1078 | return loadManifestFromZipFile(aFile); |
michael@0 | 1079 | else |
michael@0 | 1080 | return loadManifestFromDir(aFile); |
michael@0 | 1081 | } |
michael@0 | 1082 | |
michael@0 | 1083 | /** |
michael@0 | 1084 | * Gets an nsIURI for a file within another file, either a directory or an XPI |
michael@0 | 1085 | * file. If aFile is a directory then this will return a file: URI, if it is an |
michael@0 | 1086 | * XPI file then it will return a jar: URI. |
michael@0 | 1087 | * |
michael@0 | 1088 | * @param aFile |
michael@0 | 1089 | * The file containing the resources, must be either a directory or an |
michael@0 | 1090 | * XPI file |
michael@0 | 1091 | * @param aPath |
michael@0 | 1092 | * The path to find the resource at, "/" separated. If aPath is empty |
michael@0 | 1093 | * then the uri to the root of the contained files will be returned |
michael@0 | 1094 | * @return an nsIURI pointing at the resource |
michael@0 | 1095 | */ |
michael@0 | 1096 | function getURIForResourceInFile(aFile, aPath) { |
michael@0 | 1097 | if (aFile.isDirectory()) { |
michael@0 | 1098 | let resource = aFile.clone(); |
michael@0 | 1099 | if (aPath) { |
michael@0 | 1100 | aPath.split("/").forEach(function(aPart) { |
michael@0 | 1101 | resource.append(aPart); |
michael@0 | 1102 | }); |
michael@0 | 1103 | } |
michael@0 | 1104 | return NetUtil.newURI(resource); |
michael@0 | 1105 | } |
michael@0 | 1106 | |
michael@0 | 1107 | return buildJarURI(aFile, aPath); |
michael@0 | 1108 | } |
michael@0 | 1109 | |
michael@0 | 1110 | /** |
michael@0 | 1111 | * Creates a jar: URI for a file inside a ZIP file. |
michael@0 | 1112 | * |
michael@0 | 1113 | * @param aJarfile |
michael@0 | 1114 | * The ZIP file as an nsIFile |
michael@0 | 1115 | * @param aPath |
michael@0 | 1116 | * The path inside the ZIP file |
michael@0 | 1117 | * @return an nsIURI for the file |
michael@0 | 1118 | */ |
michael@0 | 1119 | function buildJarURI(aJarfile, aPath) { |
michael@0 | 1120 | let uri = Services.io.newFileURI(aJarfile); |
michael@0 | 1121 | uri = "jar:" + uri.spec + "!/" + aPath; |
michael@0 | 1122 | return NetUtil.newURI(uri); |
michael@0 | 1123 | } |
michael@0 | 1124 | |
michael@0 | 1125 | /** |
michael@0 | 1126 | * Sends local and remote notifications to flush a JAR file cache entry |
michael@0 | 1127 | * |
michael@0 | 1128 | * @param aJarFile |
michael@0 | 1129 | * The ZIP/XPI/JAR file as a nsIFile |
michael@0 | 1130 | */ |
michael@0 | 1131 | function flushJarCache(aJarFile) { |
michael@0 | 1132 | Services.obs.notifyObservers(aJarFile, "flush-cache-entry", null); |
michael@0 | 1133 | Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageBroadcaster) |
michael@0 | 1134 | .broadcastAsyncMessage(MSG_JAR_FLUSH, aJarFile.path); |
michael@0 | 1135 | } |
michael@0 | 1136 | |
michael@0 | 1137 | function flushStartupCache() { |
michael@0 | 1138 | // Init this, so it will get the notification. |
michael@0 | 1139 | Services.obs.notifyObservers(null, "startupcache-invalidate", null); |
michael@0 | 1140 | } |
michael@0 | 1141 | |
michael@0 | 1142 | /** |
michael@0 | 1143 | * Creates and returns a new unique temporary file. The caller should delete |
michael@0 | 1144 | * the file when it is no longer needed. |
michael@0 | 1145 | * |
michael@0 | 1146 | * @return an nsIFile that points to a randomly named, initially empty file in |
michael@0 | 1147 | * the OS temporary files directory |
michael@0 | 1148 | */ |
michael@0 | 1149 | function getTemporaryFile() { |
michael@0 | 1150 | let file = FileUtils.getDir(KEY_TEMPDIR, []); |
michael@0 | 1151 | let random = Math.random().toString(36).replace(/0./, '').substr(-3); |
michael@0 | 1152 | file.append("tmp-" + random + ".xpi"); |
michael@0 | 1153 | file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE); |
michael@0 | 1154 | |
michael@0 | 1155 | return file; |
michael@0 | 1156 | } |
michael@0 | 1157 | |
michael@0 | 1158 | /** |
michael@0 | 1159 | * Verifies that a zip file's contents are all signed by the same principal. |
michael@0 | 1160 | * Directory entries and anything in the META-INF directory are not checked. |
michael@0 | 1161 | * |
michael@0 | 1162 | * @param aZip |
michael@0 | 1163 | * A nsIZipReader to check |
michael@0 | 1164 | * @param aPrincipal |
michael@0 | 1165 | * The nsIPrincipal to compare against |
michael@0 | 1166 | * @return true if all the contents that should be signed were signed by the |
michael@0 | 1167 | * principal |
michael@0 | 1168 | */ |
michael@0 | 1169 | function verifyZipSigning(aZip, aPrincipal) { |
michael@0 | 1170 | var count = 0; |
michael@0 | 1171 | var entries = aZip.findEntries(null); |
michael@0 | 1172 | while (entries.hasMore()) { |
michael@0 | 1173 | var entry = entries.getNext(); |
michael@0 | 1174 | // Nothing in META-INF is in the manifest. |
michael@0 | 1175 | if (entry.substr(0, 9) == "META-INF/") |
michael@0 | 1176 | continue; |
michael@0 | 1177 | // Directory entries aren't in the manifest. |
michael@0 | 1178 | if (entry.substr(-1) == "/") |
michael@0 | 1179 | continue; |
michael@0 | 1180 | count++; |
michael@0 | 1181 | var entryPrincipal = aZip.getCertificatePrincipal(entry); |
michael@0 | 1182 | if (!entryPrincipal || !aPrincipal.equals(entryPrincipal)) |
michael@0 | 1183 | return false; |
michael@0 | 1184 | } |
michael@0 | 1185 | return aZip.manifestEntriesCount == count; |
michael@0 | 1186 | } |
michael@0 | 1187 | |
michael@0 | 1188 | /** |
michael@0 | 1189 | * Replaces %...% strings in an addon url (update and updateInfo) with |
michael@0 | 1190 | * appropriate values. |
michael@0 | 1191 | * |
michael@0 | 1192 | * @param aAddon |
michael@0 | 1193 | * The AddonInternal representing the add-on |
michael@0 | 1194 | * @param aUri |
michael@0 | 1195 | * The uri to escape |
michael@0 | 1196 | * @param aUpdateType |
michael@0 | 1197 | * An optional number representing the type of update, only applicable |
michael@0 | 1198 | * when creating a url for retrieving an update manifest |
michael@0 | 1199 | * @param aAppVersion |
michael@0 | 1200 | * The optional application version to use for %APP_VERSION% |
michael@0 | 1201 | * @return the appropriately escaped uri. |
michael@0 | 1202 | */ |
michael@0 | 1203 | function escapeAddonURI(aAddon, aUri, aUpdateType, aAppVersion) |
michael@0 | 1204 | { |
michael@0 | 1205 | let uri = AddonManager.escapeAddonURI(aAddon, aUri, aAppVersion); |
michael@0 | 1206 | |
michael@0 | 1207 | // If there is an updateType then replace the UPDATE_TYPE string |
michael@0 | 1208 | if (aUpdateType) |
michael@0 | 1209 | uri = uri.replace(/%UPDATE_TYPE%/g, aUpdateType); |
michael@0 | 1210 | |
michael@0 | 1211 | // If this add-on has compatibility information for either the current |
michael@0 | 1212 | // application or toolkit then replace the ITEM_MAXAPPVERSION with the |
michael@0 | 1213 | // maxVersion |
michael@0 | 1214 | let app = aAddon.matchingTargetApplication; |
michael@0 | 1215 | if (app) |
michael@0 | 1216 | var maxVersion = app.maxVersion; |
michael@0 | 1217 | else |
michael@0 | 1218 | maxVersion = ""; |
michael@0 | 1219 | uri = uri.replace(/%ITEM_MAXAPPVERSION%/g, maxVersion); |
michael@0 | 1220 | |
michael@0 | 1221 | let compatMode = "normal"; |
michael@0 | 1222 | if (!AddonManager.checkCompatibility) |
michael@0 | 1223 | compatMode = "ignore"; |
michael@0 | 1224 | else if (AddonManager.strictCompatibility) |
michael@0 | 1225 | compatMode = "strict"; |
michael@0 | 1226 | uri = uri.replace(/%COMPATIBILITY_MODE%/g, compatMode); |
michael@0 | 1227 | |
michael@0 | 1228 | return uri; |
michael@0 | 1229 | } |
michael@0 | 1230 | |
michael@0 | 1231 | function removeAsync(aFile) { |
michael@0 | 1232 | return Task.spawn(function () { |
michael@0 | 1233 | let info = null; |
michael@0 | 1234 | try { |
michael@0 | 1235 | info = yield OS.File.stat(aFile.path); |
michael@0 | 1236 | if (info.isDir) |
michael@0 | 1237 | yield OS.File.removeDir(aFile.path); |
michael@0 | 1238 | else |
michael@0 | 1239 | yield OS.File.remove(aFile.path); |
michael@0 | 1240 | } |
michael@0 | 1241 | catch (e if e instanceof OS.File.Error && e.becauseNoSuchFile) { |
michael@0 | 1242 | // The file has already gone away |
michael@0 | 1243 | return; |
michael@0 | 1244 | } |
michael@0 | 1245 | }); |
michael@0 | 1246 | } |
michael@0 | 1247 | |
michael@0 | 1248 | /** |
michael@0 | 1249 | * Recursively removes a directory or file fixing permissions when necessary. |
michael@0 | 1250 | * |
michael@0 | 1251 | * @param aFile |
michael@0 | 1252 | * The nsIFile to remove |
michael@0 | 1253 | */ |
michael@0 | 1254 | function recursiveRemove(aFile) { |
michael@0 | 1255 | let isDir = null; |
michael@0 | 1256 | |
michael@0 | 1257 | try { |
michael@0 | 1258 | isDir = aFile.isDirectory(); |
michael@0 | 1259 | } |
michael@0 | 1260 | catch (e) { |
michael@0 | 1261 | // If the file has already gone away then don't worry about it, this can |
michael@0 | 1262 | // happen on OSX where the resource fork is automatically moved with the |
michael@0 | 1263 | // data fork for the file. See bug 733436. |
michael@0 | 1264 | if (e.result == Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) |
michael@0 | 1265 | return; |
michael@0 | 1266 | if (e.result == Cr.NS_ERROR_FILE_NOT_FOUND) |
michael@0 | 1267 | return; |
michael@0 | 1268 | |
michael@0 | 1269 | throw e; |
michael@0 | 1270 | } |
michael@0 | 1271 | |
michael@0 | 1272 | setFilePermissions(aFile, isDir ? FileUtils.PERMS_DIRECTORY |
michael@0 | 1273 | : FileUtils.PERMS_FILE); |
michael@0 | 1274 | |
michael@0 | 1275 | try { |
michael@0 | 1276 | aFile.remove(true); |
michael@0 | 1277 | return; |
michael@0 | 1278 | } |
michael@0 | 1279 | catch (e) { |
michael@0 | 1280 | if (!aFile.isDirectory()) { |
michael@0 | 1281 | logger.error("Failed to remove file " + aFile.path, e); |
michael@0 | 1282 | throw e; |
michael@0 | 1283 | } |
michael@0 | 1284 | } |
michael@0 | 1285 | |
michael@0 | 1286 | // Use a snapshot of the directory contents to avoid possible issues with |
michael@0 | 1287 | // iterating over a directory while removing files from it (the YAFFS2 |
michael@0 | 1288 | // embedded filesystem has this issue, see bug 772238), and to remove |
michael@0 | 1289 | // normal files before their resource forks on OSX (see bug 733436). |
michael@0 | 1290 | let entries = getDirectoryEntries(aFile, true); |
michael@0 | 1291 | entries.forEach(recursiveRemove); |
michael@0 | 1292 | |
michael@0 | 1293 | try { |
michael@0 | 1294 | aFile.remove(true); |
michael@0 | 1295 | } |
michael@0 | 1296 | catch (e) { |
michael@0 | 1297 | logger.error("Failed to remove empty directory " + aFile.path, e); |
michael@0 | 1298 | throw e; |
michael@0 | 1299 | } |
michael@0 | 1300 | } |
michael@0 | 1301 | |
michael@0 | 1302 | /** |
michael@0 | 1303 | * Returns the timestamp and leaf file name of the most recently modified |
michael@0 | 1304 | * entry in a directory, |
michael@0 | 1305 | * or simply the file's own timestamp if it is not a directory. |
michael@0 | 1306 | * Also returns the total number of items (directories and files) visited in the scan |
michael@0 | 1307 | * |
michael@0 | 1308 | * @param aFile |
michael@0 | 1309 | * A non-null nsIFile object |
michael@0 | 1310 | * @return [File Name, Epoch time, items visited], as described above. |
michael@0 | 1311 | */ |
michael@0 | 1312 | function recursiveLastModifiedTime(aFile) { |
michael@0 | 1313 | try { |
michael@0 | 1314 | let modTime = aFile.lastModifiedTime; |
michael@0 | 1315 | let fileName = aFile.leafName; |
michael@0 | 1316 | if (aFile.isFile()) |
michael@0 | 1317 | return [fileName, modTime, 1]; |
michael@0 | 1318 | |
michael@0 | 1319 | if (aFile.isDirectory()) { |
michael@0 | 1320 | let entries = aFile.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator); |
michael@0 | 1321 | let entry; |
michael@0 | 1322 | let totalItems = 1; |
michael@0 | 1323 | while ((entry = entries.nextFile)) { |
michael@0 | 1324 | let [subName, subTime, items] = recursiveLastModifiedTime(entry); |
michael@0 | 1325 | totalItems += items; |
michael@0 | 1326 | if (subTime > modTime) { |
michael@0 | 1327 | modTime = subTime; |
michael@0 | 1328 | fileName = subName; |
michael@0 | 1329 | } |
michael@0 | 1330 | } |
michael@0 | 1331 | entries.close(); |
michael@0 | 1332 | return [fileName, modTime, totalItems]; |
michael@0 | 1333 | } |
michael@0 | 1334 | } |
michael@0 | 1335 | catch (e) { |
michael@0 | 1336 | logger.warn("Problem getting last modified time for " + aFile.path, e); |
michael@0 | 1337 | } |
michael@0 | 1338 | |
michael@0 | 1339 | // If the file is something else, just ignore it. |
michael@0 | 1340 | return ["", 0, 0]; |
michael@0 | 1341 | } |
michael@0 | 1342 | |
michael@0 | 1343 | /** |
michael@0 | 1344 | * Gets a snapshot of directory entries. |
michael@0 | 1345 | * |
michael@0 | 1346 | * @param aDir |
michael@0 | 1347 | * Directory to look at |
michael@0 | 1348 | * @param aSortEntries |
michael@0 | 1349 | * True to sort entries by filename |
michael@0 | 1350 | * @return An array of nsIFile, or an empty array if aDir is not a readable directory |
michael@0 | 1351 | */ |
michael@0 | 1352 | function getDirectoryEntries(aDir, aSortEntries) { |
michael@0 | 1353 | let dirEnum; |
michael@0 | 1354 | try { |
michael@0 | 1355 | dirEnum = aDir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator); |
michael@0 | 1356 | let entries = []; |
michael@0 | 1357 | while (dirEnum.hasMoreElements()) |
michael@0 | 1358 | entries.push(dirEnum.nextFile); |
michael@0 | 1359 | |
michael@0 | 1360 | if (aSortEntries) { |
michael@0 | 1361 | entries.sort(function sortDirEntries(a, b) { |
michael@0 | 1362 | return a.path > b.path ? -1 : 1; |
michael@0 | 1363 | }); |
michael@0 | 1364 | } |
michael@0 | 1365 | |
michael@0 | 1366 | return entries |
michael@0 | 1367 | } |
michael@0 | 1368 | catch (e) { |
michael@0 | 1369 | logger.warn("Can't iterate directory " + aDir.path, e); |
michael@0 | 1370 | return []; |
michael@0 | 1371 | } |
michael@0 | 1372 | finally { |
michael@0 | 1373 | if (dirEnum) { |
michael@0 | 1374 | dirEnum.close(); |
michael@0 | 1375 | } |
michael@0 | 1376 | } |
michael@0 | 1377 | } |
michael@0 | 1378 | |
michael@0 | 1379 | /** |
michael@0 | 1380 | * A helpful wrapper around the prefs service that allows for default values |
michael@0 | 1381 | * when requested values aren't set. |
michael@0 | 1382 | */ |
michael@0 | 1383 | var Prefs = { |
michael@0 | 1384 | /** |
michael@0 | 1385 | * Gets a preference from the default branch ignoring user-set values. |
michael@0 | 1386 | * |
michael@0 | 1387 | * @param aName |
michael@0 | 1388 | * The name of the preference |
michael@0 | 1389 | * @param aDefaultValue |
michael@0 | 1390 | * A value to return if the preference does not exist |
michael@0 | 1391 | * @return the default value of the preference or aDefaultValue if there is |
michael@0 | 1392 | * none |
michael@0 | 1393 | */ |
michael@0 | 1394 | getDefaultCharPref: function Prefs_getDefaultCharPref(aName, aDefaultValue) { |
michael@0 | 1395 | try { |
michael@0 | 1396 | return Services.prefs.getDefaultBranch("").getCharPref(aName); |
michael@0 | 1397 | } |
michael@0 | 1398 | catch (e) { |
michael@0 | 1399 | } |
michael@0 | 1400 | return aDefaultValue; |
michael@0 | 1401 | }, |
michael@0 | 1402 | |
michael@0 | 1403 | /** |
michael@0 | 1404 | * Gets a string preference. |
michael@0 | 1405 | * |
michael@0 | 1406 | * @param aName |
michael@0 | 1407 | * The name of the preference |
michael@0 | 1408 | * @param aDefaultValue |
michael@0 | 1409 | * A value to return if the preference does not exist |
michael@0 | 1410 | * @return the value of the preference or aDefaultValue if there is none |
michael@0 | 1411 | */ |
michael@0 | 1412 | getCharPref: function Prefs_getCharPref(aName, aDefaultValue) { |
michael@0 | 1413 | try { |
michael@0 | 1414 | return Services.prefs.getCharPref(aName); |
michael@0 | 1415 | } |
michael@0 | 1416 | catch (e) { |
michael@0 | 1417 | } |
michael@0 | 1418 | return aDefaultValue; |
michael@0 | 1419 | }, |
michael@0 | 1420 | |
michael@0 | 1421 | /** |
michael@0 | 1422 | * Gets a complex preference. |
michael@0 | 1423 | * |
michael@0 | 1424 | * @param aName |
michael@0 | 1425 | * The name of the preference |
michael@0 | 1426 | * @param aType |
michael@0 | 1427 | * The interface type of the preference |
michael@0 | 1428 | * @param aDefaultValue |
michael@0 | 1429 | * A value to return if the preference does not exist |
michael@0 | 1430 | * @return the value of the preference or aDefaultValue if there is none |
michael@0 | 1431 | */ |
michael@0 | 1432 | getComplexValue: function Prefs_getComplexValue(aName, aType, aDefaultValue) { |
michael@0 | 1433 | try { |
michael@0 | 1434 | return Services.prefs.getComplexValue(aName, aType).data; |
michael@0 | 1435 | } |
michael@0 | 1436 | catch (e) { |
michael@0 | 1437 | } |
michael@0 | 1438 | return aDefaultValue; |
michael@0 | 1439 | }, |
michael@0 | 1440 | |
michael@0 | 1441 | /** |
michael@0 | 1442 | * Gets a boolean preference. |
michael@0 | 1443 | * |
michael@0 | 1444 | * @param aName |
michael@0 | 1445 | * The name of the preference |
michael@0 | 1446 | * @param aDefaultValue |
michael@0 | 1447 | * A value to return if the preference does not exist |
michael@0 | 1448 | * @return the value of the preference or aDefaultValue if there is none |
michael@0 | 1449 | */ |
michael@0 | 1450 | getBoolPref: function Prefs_getBoolPref(aName, aDefaultValue) { |
michael@0 | 1451 | try { |
michael@0 | 1452 | return Services.prefs.getBoolPref(aName); |
michael@0 | 1453 | } |
michael@0 | 1454 | catch (e) { |
michael@0 | 1455 | } |
michael@0 | 1456 | return aDefaultValue; |
michael@0 | 1457 | }, |
michael@0 | 1458 | |
michael@0 | 1459 | /** |
michael@0 | 1460 | * Gets an integer preference. |
michael@0 | 1461 | * |
michael@0 | 1462 | * @param aName |
michael@0 | 1463 | * The name of the preference |
michael@0 | 1464 | * @param defaultValue |
michael@0 | 1465 | * A value to return if the preference does not exist |
michael@0 | 1466 | * @return the value of the preference or defaultValue if there is none |
michael@0 | 1467 | */ |
michael@0 | 1468 | getIntPref: function Prefs_getIntPref(aName, defaultValue) { |
michael@0 | 1469 | try { |
michael@0 | 1470 | return Services.prefs.getIntPref(aName); |
michael@0 | 1471 | } |
michael@0 | 1472 | catch (e) { |
michael@0 | 1473 | } |
michael@0 | 1474 | return defaultValue; |
michael@0 | 1475 | }, |
michael@0 | 1476 | |
michael@0 | 1477 | /** |
michael@0 | 1478 | * Clears a preference if it has a user value |
michael@0 | 1479 | * |
michael@0 | 1480 | * @param aName |
michael@0 | 1481 | * The name of the preference |
michael@0 | 1482 | */ |
michael@0 | 1483 | clearUserPref: function Prefs_clearUserPref(aName) { |
michael@0 | 1484 | if (Services.prefs.prefHasUserValue(aName)) |
michael@0 | 1485 | Services.prefs.clearUserPref(aName); |
michael@0 | 1486 | } |
michael@0 | 1487 | } |
michael@0 | 1488 | |
michael@0 | 1489 | // Helper function to compare JSON saved version of the directory state |
michael@0 | 1490 | // with the new state returned by getInstallLocationStates() |
michael@0 | 1491 | // Structure is: ordered array of {'name':?, 'addons': {addonID: {'descriptor':?, 'mtime':?} ...}} |
michael@0 | 1492 | function directoryStateDiffers(aState, aCache) |
michael@0 | 1493 | { |
michael@0 | 1494 | // check equality of an object full of addons; fortunately we can destroy the 'aOld' object |
michael@0 | 1495 | function addonsMismatch(aNew, aOld) { |
michael@0 | 1496 | for (let [id, val] of aNew) { |
michael@0 | 1497 | if (!id in aOld) |
michael@0 | 1498 | return true; |
michael@0 | 1499 | if (val.descriptor != aOld[id].descriptor || |
michael@0 | 1500 | val.mtime != aOld[id].mtime) |
michael@0 | 1501 | return true; |
michael@0 | 1502 | delete aOld[id]; |
michael@0 | 1503 | } |
michael@0 | 1504 | // make sure aOld doesn't have any extra entries |
michael@0 | 1505 | for (let id in aOld) |
michael@0 | 1506 | return true; |
michael@0 | 1507 | return false; |
michael@0 | 1508 | } |
michael@0 | 1509 | |
michael@0 | 1510 | if (!aCache) |
michael@0 | 1511 | return true; |
michael@0 | 1512 | try { |
michael@0 | 1513 | let old = JSON.parse(aCache); |
michael@0 | 1514 | if (aState.length != old.length) |
michael@0 | 1515 | return true; |
michael@0 | 1516 | for (let i = 0; i < aState.length; i++) { |
michael@0 | 1517 | // conveniently, any missing fields would require a 'true' return, which is |
michael@0 | 1518 | // handled by our catch wrapper |
michael@0 | 1519 | if (aState[i].name != old[i].name) |
michael@0 | 1520 | return true; |
michael@0 | 1521 | if (addonsMismatch(aState[i].addons, old[i].addons)) |
michael@0 | 1522 | return true; |
michael@0 | 1523 | } |
michael@0 | 1524 | } |
michael@0 | 1525 | catch (e) { |
michael@0 | 1526 | return true; |
michael@0 | 1527 | } |
michael@0 | 1528 | return false; |
michael@0 | 1529 | } |
michael@0 | 1530 | |
michael@0 | 1531 | /** |
michael@0 | 1532 | * Wraps a function in an exception handler to protect against exceptions inside callbacks |
michael@0 | 1533 | * @param aFunction function(args...) |
michael@0 | 1534 | * @return function(args...), a function that takes the same arguments as aFunction |
michael@0 | 1535 | * and returns the same result unless aFunction throws, in which case it logs |
michael@0 | 1536 | * a warning and returns undefined. |
michael@0 | 1537 | */ |
michael@0 | 1538 | function makeSafe(aFunction) { |
michael@0 | 1539 | return function(...aArgs) { |
michael@0 | 1540 | try { |
michael@0 | 1541 | return aFunction(...aArgs); |
michael@0 | 1542 | } |
michael@0 | 1543 | catch(ex) { |
michael@0 | 1544 | logger.warn("XPIProvider callback failed", ex); |
michael@0 | 1545 | } |
michael@0 | 1546 | return undefined; |
michael@0 | 1547 | } |
michael@0 | 1548 | } |
michael@0 | 1549 | |
michael@0 | 1550 | this.XPIProvider = { |
michael@0 | 1551 | // An array of known install locations |
michael@0 | 1552 | installLocations: null, |
michael@0 | 1553 | // A dictionary of known install locations by name |
michael@0 | 1554 | installLocationsByName: null, |
michael@0 | 1555 | // An array of currently active AddonInstalls |
michael@0 | 1556 | installs: null, |
michael@0 | 1557 | // The default skin for the application |
michael@0 | 1558 | defaultSkin: "classic/1.0", |
michael@0 | 1559 | // The current skin used by the application |
michael@0 | 1560 | currentSkin: null, |
michael@0 | 1561 | // The selected skin to be used by the application when it is restarted. This |
michael@0 | 1562 | // will be the same as currentSkin when it is the skin to be used when the |
michael@0 | 1563 | // application is restarted |
michael@0 | 1564 | selectedSkin: null, |
michael@0 | 1565 | // The value of the minCompatibleAppVersion preference |
michael@0 | 1566 | minCompatibleAppVersion: null, |
michael@0 | 1567 | // The value of the minCompatiblePlatformVersion preference |
michael@0 | 1568 | minCompatiblePlatformVersion: null, |
michael@0 | 1569 | // A dictionary of the file descriptors for bootstrappable add-ons by ID |
michael@0 | 1570 | bootstrappedAddons: {}, |
michael@0 | 1571 | // A dictionary of JS scopes of loaded bootstrappable add-ons by ID |
michael@0 | 1572 | bootstrapScopes: {}, |
michael@0 | 1573 | // True if the platform could have activated extensions |
michael@0 | 1574 | extensionsActive: false, |
michael@0 | 1575 | // File / directory state of installed add-ons |
michael@0 | 1576 | installStates: [], |
michael@0 | 1577 | // True if all of the add-ons found during startup were installed in the |
michael@0 | 1578 | // application install location |
michael@0 | 1579 | allAppGlobal: true, |
michael@0 | 1580 | // A string listing the enabled add-ons for annotating crash reports |
michael@0 | 1581 | enabledAddons: null, |
michael@0 | 1582 | // An array of add-on IDs of add-ons that were inactive during startup |
michael@0 | 1583 | inactiveAddonIDs: [], |
michael@0 | 1584 | // Keep track of startup phases for telemetry |
michael@0 | 1585 | runPhase: XPI_STARTING, |
michael@0 | 1586 | // Keep track of the newest file in each add-on, in case we want to |
michael@0 | 1587 | // report it to telemetry. |
michael@0 | 1588 | _mostRecentlyModifiedFile: {}, |
michael@0 | 1589 | // Per-addon telemetry information |
michael@0 | 1590 | _telemetryDetails: {}, |
michael@0 | 1591 | // Experiments are disabled by default. Track ones that are locally enabled. |
michael@0 | 1592 | _enabledExperiments: null, |
michael@0 | 1593 | |
michael@0 | 1594 | /* |
michael@0 | 1595 | * Set a value in the telemetry hash for a given ID |
michael@0 | 1596 | */ |
michael@0 | 1597 | setTelemetry: function XPI_setTelemetry(aId, aName, aValue) { |
michael@0 | 1598 | if (!this._telemetryDetails[aId]) |
michael@0 | 1599 | this._telemetryDetails[aId] = {}; |
michael@0 | 1600 | this._telemetryDetails[aId][aName] = aValue; |
michael@0 | 1601 | }, |
michael@0 | 1602 | |
michael@0 | 1603 | // Keep track of in-progress operations that support cancel() |
michael@0 | 1604 | _inProgress: new Set(), |
michael@0 | 1605 | |
michael@0 | 1606 | doing: function XPI_doing(aCancellable) { |
michael@0 | 1607 | this._inProgress.add(aCancellable); |
michael@0 | 1608 | }, |
michael@0 | 1609 | |
michael@0 | 1610 | done: function XPI_done(aCancellable) { |
michael@0 | 1611 | return this._inProgress.delete(aCancellable); |
michael@0 | 1612 | }, |
michael@0 | 1613 | |
michael@0 | 1614 | cancelAll: function XPI_cancelAll() { |
michael@0 | 1615 | // Cancelling one may alter _inProgress, so restart the iterator after each |
michael@0 | 1616 | while (this._inProgress.size > 0) { |
michael@0 | 1617 | for (let c of this._inProgress) { |
michael@0 | 1618 | try { |
michael@0 | 1619 | c.cancel(); |
michael@0 | 1620 | } |
michael@0 | 1621 | catch (e) { |
michael@0 | 1622 | logger.warn("Cancel failed", e); |
michael@0 | 1623 | } |
michael@0 | 1624 | this._inProgress.delete(c); |
michael@0 | 1625 | } |
michael@0 | 1626 | } |
michael@0 | 1627 | }, |
michael@0 | 1628 | |
michael@0 | 1629 | /** |
michael@0 | 1630 | * Adds or updates a URI mapping for an Addon.id. |
michael@0 | 1631 | * |
michael@0 | 1632 | * Mappings should not be removed at any point. This is so that the mappings |
michael@0 | 1633 | * will be still valid after an add-on gets disabled or uninstalled, as |
michael@0 | 1634 | * consumers may still have URIs of (leaked) resources they want to map. |
michael@0 | 1635 | */ |
michael@0 | 1636 | _addURIMapping: function XPI__addURIMapping(aID, aFile) { |
michael@0 | 1637 | try { |
michael@0 | 1638 | // Always use our own mechanics instead of nsIIOService.newFileURI, so |
michael@0 | 1639 | // that we can be sure to map things as we want them mapped. |
michael@0 | 1640 | let uri = this._resolveURIToFile(getURIForResourceInFile(aFile, ".")); |
michael@0 | 1641 | if (!uri) { |
michael@0 | 1642 | throw new Error("Cannot resolve"); |
michael@0 | 1643 | } |
michael@0 | 1644 | this._ensureURIMappings(); |
michael@0 | 1645 | this._uriMappings[aID] = uri.spec; |
michael@0 | 1646 | } |
michael@0 | 1647 | catch (ex) { |
michael@0 | 1648 | logger.warn("Failed to add URI mapping", ex); |
michael@0 | 1649 | } |
michael@0 | 1650 | }, |
michael@0 | 1651 | |
michael@0 | 1652 | /** |
michael@0 | 1653 | * Ensures that the URI to Addon mappings are available. |
michael@0 | 1654 | * |
michael@0 | 1655 | * The function will add mappings for all non-bootstrapped but enabled |
michael@0 | 1656 | * add-ons. |
michael@0 | 1657 | * Bootstrapped add-on mappings will be added directly when the bootstrap |
michael@0 | 1658 | * scope get loaded. (See XPIProvider._addURIMapping() and callers) |
michael@0 | 1659 | */ |
michael@0 | 1660 | _ensureURIMappings: function XPI__ensureURIMappings() { |
michael@0 | 1661 | if (this._uriMappings) { |
michael@0 | 1662 | return; |
michael@0 | 1663 | } |
michael@0 | 1664 | // XXX Convert to Map(), once it gets stable with stable iterators |
michael@0 | 1665 | this._uriMappings = Object.create(null); |
michael@0 | 1666 | |
michael@0 | 1667 | // XXX Convert to Set(), once it gets stable with stable iterators |
michael@0 | 1668 | let enabled = Object.create(null); |
michael@0 | 1669 | let enabledAddons = this.enabledAddons || ""; |
michael@0 | 1670 | for (let a of enabledAddons.split(",")) { |
michael@0 | 1671 | a = decodeURIComponent(a.split(":")[0]); |
michael@0 | 1672 | enabled[a] = null; |
michael@0 | 1673 | } |
michael@0 | 1674 | |
michael@0 | 1675 | let cache = JSON.parse(Prefs.getCharPref(PREF_INSTALL_CACHE, "[]")); |
michael@0 | 1676 | for (let loc of cache) { |
michael@0 | 1677 | for (let [id, val] in Iterator(loc.addons)) { |
michael@0 | 1678 | if (!(id in enabled)) { |
michael@0 | 1679 | continue; |
michael@0 | 1680 | } |
michael@0 | 1681 | let file = new nsIFile(val.descriptor); |
michael@0 | 1682 | let spec = Services.io.newFileURI(file).spec; |
michael@0 | 1683 | this._uriMappings[id] = spec; |
michael@0 | 1684 | } |
michael@0 | 1685 | } |
michael@0 | 1686 | }, |
michael@0 | 1687 | |
michael@0 | 1688 | /** |
michael@0 | 1689 | * Resolve a URI back to physical file. |
michael@0 | 1690 | * |
michael@0 | 1691 | * Of course, this works only for URIs pointing to local resources. |
michael@0 | 1692 | * |
michael@0 | 1693 | * @param aURI |
michael@0 | 1694 | * URI to resolve |
michael@0 | 1695 | * @return |
michael@0 | 1696 | * resolved nsIFileURL |
michael@0 | 1697 | */ |
michael@0 | 1698 | _resolveURIToFile: function XPI__resolveURIToFile(aURI) { |
michael@0 | 1699 | switch (aURI.scheme) { |
michael@0 | 1700 | case "jar": |
michael@0 | 1701 | case "file": |
michael@0 | 1702 | if (aURI instanceof Ci.nsIJARURI) { |
michael@0 | 1703 | return this._resolveURIToFile(aURI.JARFile); |
michael@0 | 1704 | } |
michael@0 | 1705 | return aURI; |
michael@0 | 1706 | |
michael@0 | 1707 | case "chrome": |
michael@0 | 1708 | aURI = ChromeRegistry.convertChromeURL(aURI); |
michael@0 | 1709 | return this._resolveURIToFile(aURI); |
michael@0 | 1710 | |
michael@0 | 1711 | case "resource": |
michael@0 | 1712 | aURI = Services.io.newURI(ResProtocolHandler.resolveURI(aURI), null, |
michael@0 | 1713 | null); |
michael@0 | 1714 | return this._resolveURIToFile(aURI); |
michael@0 | 1715 | |
michael@0 | 1716 | case "view-source": |
michael@0 | 1717 | aURI = Services.io.newURI(aURI.path, null, null); |
michael@0 | 1718 | return this._resolveURIToFile(aURI); |
michael@0 | 1719 | |
michael@0 | 1720 | case "about": |
michael@0 | 1721 | if (aURI.spec == "about:blank") { |
michael@0 | 1722 | // Do not attempt to map about:blank |
michael@0 | 1723 | return null; |
michael@0 | 1724 | } |
michael@0 | 1725 | |
michael@0 | 1726 | let chan; |
michael@0 | 1727 | try { |
michael@0 | 1728 | chan = Services.io.newChannelFromURI(aURI); |
michael@0 | 1729 | } |
michael@0 | 1730 | catch (ex) { |
michael@0 | 1731 | return null; |
michael@0 | 1732 | } |
michael@0 | 1733 | // Avoid looping |
michael@0 | 1734 | if (chan.URI.equals(aURI)) { |
michael@0 | 1735 | return null; |
michael@0 | 1736 | } |
michael@0 | 1737 | // We want to clone the channel URI to avoid accidentially keeping |
michael@0 | 1738 | // unnecessary references to the channel or implementation details |
michael@0 | 1739 | // around. |
michael@0 | 1740 | return this._resolveURIToFile(chan.URI.clone()); |
michael@0 | 1741 | |
michael@0 | 1742 | default: |
michael@0 | 1743 | return null; |
michael@0 | 1744 | } |
michael@0 | 1745 | }, |
michael@0 | 1746 | |
michael@0 | 1747 | /** |
michael@0 | 1748 | * Starts the XPI provider initializes the install locations and prefs. |
michael@0 | 1749 | * |
michael@0 | 1750 | * @param aAppChanged |
michael@0 | 1751 | * A tri-state value. Undefined means the current profile was created |
michael@0 | 1752 | * for this session, true means the profile already existed but was |
michael@0 | 1753 | * last used with an application with a different version number, |
michael@0 | 1754 | * false means that the profile was last used by this version of the |
michael@0 | 1755 | * application. |
michael@0 | 1756 | * @param aOldAppVersion |
michael@0 | 1757 | * The version of the application last run with this profile or null |
michael@0 | 1758 | * if it is a new profile or the version is unknown |
michael@0 | 1759 | * @param aOldPlatformVersion |
michael@0 | 1760 | * The version of the platform last run with this profile or null |
michael@0 | 1761 | * if it is a new profile or the version is unknown |
michael@0 | 1762 | */ |
michael@0 | 1763 | startup: function XPI_startup(aAppChanged, aOldAppVersion, aOldPlatformVersion) { |
michael@0 | 1764 | function addDirectoryInstallLocation(aName, aKey, aPaths, aScope, aLocked) { |
michael@0 | 1765 | try { |
michael@0 | 1766 | var dir = FileUtils.getDir(aKey, aPaths); |
michael@0 | 1767 | } |
michael@0 | 1768 | catch (e) { |
michael@0 | 1769 | // Some directories aren't defined on some platforms, ignore them |
michael@0 | 1770 | logger.debug("Skipping unavailable install location " + aName); |
michael@0 | 1771 | return; |
michael@0 | 1772 | } |
michael@0 | 1773 | |
michael@0 | 1774 | try { |
michael@0 | 1775 | var location = new DirectoryInstallLocation(aName, dir, aScope, aLocked); |
michael@0 | 1776 | } |
michael@0 | 1777 | catch (e) { |
michael@0 | 1778 | logger.warn("Failed to add directory install location " + aName, e); |
michael@0 | 1779 | return; |
michael@0 | 1780 | } |
michael@0 | 1781 | |
michael@0 | 1782 | XPIProvider.installLocations.push(location); |
michael@0 | 1783 | XPIProvider.installLocationsByName[location.name] = location; |
michael@0 | 1784 | } |
michael@0 | 1785 | |
michael@0 | 1786 | function addRegistryInstallLocation(aName, aRootkey, aScope) { |
michael@0 | 1787 | try { |
michael@0 | 1788 | var location = new WinRegInstallLocation(aName, aRootkey, aScope); |
michael@0 | 1789 | } |
michael@0 | 1790 | catch (e) { |
michael@0 | 1791 | logger.warn("Failed to add registry install location " + aName, e); |
michael@0 | 1792 | return; |
michael@0 | 1793 | } |
michael@0 | 1794 | |
michael@0 | 1795 | XPIProvider.installLocations.push(location); |
michael@0 | 1796 | XPIProvider.installLocationsByName[location.name] = location; |
michael@0 | 1797 | } |
michael@0 | 1798 | |
michael@0 | 1799 | try { |
michael@0 | 1800 | AddonManagerPrivate.recordTimestamp("XPI_startup_begin"); |
michael@0 | 1801 | |
michael@0 | 1802 | logger.debug("startup"); |
michael@0 | 1803 | this.runPhase = XPI_STARTING; |
michael@0 | 1804 | this.installs = []; |
michael@0 | 1805 | this.installLocations = []; |
michael@0 | 1806 | this.installLocationsByName = {}; |
michael@0 | 1807 | // Hook for tests to detect when saving database at shutdown time fails |
michael@0 | 1808 | this._shutdownError = null; |
michael@0 | 1809 | // Clear this at startup for xpcshell test restarts |
michael@0 | 1810 | this._telemetryDetails = {}; |
michael@0 | 1811 | // Clear the set of enabled experiments (experiments disabled by default). |
michael@0 | 1812 | this._enabledExperiments = new Set(); |
michael@0 | 1813 | // Register our details structure with AddonManager |
michael@0 | 1814 | AddonManagerPrivate.setTelemetryDetails("XPI", this._telemetryDetails); |
michael@0 | 1815 | |
michael@0 | 1816 | let hasRegistry = ("nsIWindowsRegKey" in Ci); |
michael@0 | 1817 | |
michael@0 | 1818 | let enabledScopes = Prefs.getIntPref(PREF_EM_ENABLED_SCOPES, |
michael@0 | 1819 | AddonManager.SCOPE_ALL); |
michael@0 | 1820 | |
michael@0 | 1821 | // These must be in order of priority for processFileChanges etc. to work |
michael@0 | 1822 | if (enabledScopes & AddonManager.SCOPE_SYSTEM) { |
michael@0 | 1823 | if (hasRegistry) { |
michael@0 | 1824 | addRegistryInstallLocation("winreg-app-global", |
michael@0 | 1825 | Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE, |
michael@0 | 1826 | AddonManager.SCOPE_SYSTEM); |
michael@0 | 1827 | } |
michael@0 | 1828 | addDirectoryInstallLocation(KEY_APP_SYSTEM_LOCAL, "XRESysLExtPD", |
michael@0 | 1829 | [Services.appinfo.ID], |
michael@0 | 1830 | AddonManager.SCOPE_SYSTEM, true); |
michael@0 | 1831 | addDirectoryInstallLocation(KEY_APP_SYSTEM_SHARE, "XRESysSExtPD", |
michael@0 | 1832 | [Services.appinfo.ID], |
michael@0 | 1833 | AddonManager.SCOPE_SYSTEM, true); |
michael@0 | 1834 | } |
michael@0 | 1835 | |
michael@0 | 1836 | if (enabledScopes & AddonManager.SCOPE_APPLICATION) { |
michael@0 | 1837 | addDirectoryInstallLocation(KEY_APP_GLOBAL, KEY_APPDIR, |
michael@0 | 1838 | [DIR_EXTENSIONS], |
michael@0 | 1839 | AddonManager.SCOPE_APPLICATION, true); |
michael@0 | 1840 | } |
michael@0 | 1841 | |
michael@0 | 1842 | if (enabledScopes & AddonManager.SCOPE_USER) { |
michael@0 | 1843 | if (hasRegistry) { |
michael@0 | 1844 | addRegistryInstallLocation("winreg-app-user", |
michael@0 | 1845 | Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, |
michael@0 | 1846 | AddonManager.SCOPE_USER); |
michael@0 | 1847 | } |
michael@0 | 1848 | addDirectoryInstallLocation(KEY_APP_SYSTEM_USER, "XREUSysExt", |
michael@0 | 1849 | [Services.appinfo.ID], |
michael@0 | 1850 | AddonManager.SCOPE_USER, true); |
michael@0 | 1851 | } |
michael@0 | 1852 | |
michael@0 | 1853 | // The profile location is always enabled |
michael@0 | 1854 | addDirectoryInstallLocation(KEY_APP_PROFILE, KEY_PROFILEDIR, |
michael@0 | 1855 | [DIR_EXTENSIONS], |
michael@0 | 1856 | AddonManager.SCOPE_PROFILE, false); |
michael@0 | 1857 | |
michael@0 | 1858 | this.defaultSkin = Prefs.getDefaultCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN, |
michael@0 | 1859 | "classic/1.0"); |
michael@0 | 1860 | this.currentSkin = Prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN, |
michael@0 | 1861 | this.defaultSkin); |
michael@0 | 1862 | this.selectedSkin = this.currentSkin; |
michael@0 | 1863 | this.applyThemeChange(); |
michael@0 | 1864 | |
michael@0 | 1865 | this.minCompatibleAppVersion = Prefs.getCharPref(PREF_EM_MIN_COMPAT_APP_VERSION, |
michael@0 | 1866 | null); |
michael@0 | 1867 | this.minCompatiblePlatformVersion = Prefs.getCharPref(PREF_EM_MIN_COMPAT_PLATFORM_VERSION, |
michael@0 | 1868 | null); |
michael@0 | 1869 | this.enabledAddons = ""; |
michael@0 | 1870 | |
michael@0 | 1871 | Services.prefs.addObserver(PREF_EM_MIN_COMPAT_APP_VERSION, this, false); |
michael@0 | 1872 | Services.prefs.addObserver(PREF_EM_MIN_COMPAT_PLATFORM_VERSION, this, false); |
michael@0 | 1873 | Services.obs.addObserver(this, NOTIFICATION_FLUSH_PERMISSIONS, false); |
michael@0 | 1874 | |
michael@0 | 1875 | try { |
michael@0 | 1876 | BrowserToolboxProcess.on("connectionchange", |
michael@0 | 1877 | this.onDebugConnectionChange.bind(this)); |
michael@0 | 1878 | } |
michael@0 | 1879 | catch (e) { |
michael@0 | 1880 | // BrowserToolboxProcess is not available in all applications |
michael@0 | 1881 | } |
michael@0 | 1882 | |
michael@0 | 1883 | let flushCaches = this.checkForChanges(aAppChanged, aOldAppVersion, |
michael@0 | 1884 | aOldPlatformVersion); |
michael@0 | 1885 | |
michael@0 | 1886 | // Changes to installed extensions may have changed which theme is selected |
michael@0 | 1887 | this.applyThemeChange(); |
michael@0 | 1888 | |
michael@0 | 1889 | // If the application has been upgraded and there are add-ons outside the |
michael@0 | 1890 | // application directory then we may need to synchronize compatibility |
michael@0 | 1891 | // information but only if the mismatch UI isn't disabled |
michael@0 | 1892 | if (aAppChanged && !this.allAppGlobal && |
michael@0 | 1893 | Prefs.getBoolPref(PREF_EM_SHOW_MISMATCH_UI, true)) { |
michael@0 | 1894 | this.showUpgradeUI(); |
michael@0 | 1895 | flushCaches = true; |
michael@0 | 1896 | } |
michael@0 | 1897 | else if (aAppChanged === undefined) { |
michael@0 | 1898 | // For new profiles we will never need to show the add-on selection UI |
michael@0 | 1899 | Services.prefs.setBoolPref(PREF_SHOWN_SELECTION_UI, true); |
michael@0 | 1900 | } |
michael@0 | 1901 | |
michael@0 | 1902 | if (flushCaches) { |
michael@0 | 1903 | flushStartupCache(); |
michael@0 | 1904 | |
michael@0 | 1905 | // UI displayed early in startup (like the compatibility UI) may have |
michael@0 | 1906 | // caused us to cache parts of the skin or locale in memory. These must |
michael@0 | 1907 | // be flushed to allow extension provided skins and locales to take full |
michael@0 | 1908 | // effect |
michael@0 | 1909 | Services.obs.notifyObservers(null, "chrome-flush-skin-caches", null); |
michael@0 | 1910 | Services.obs.notifyObservers(null, "chrome-flush-caches", null); |
michael@0 | 1911 | } |
michael@0 | 1912 | |
michael@0 | 1913 | this.enabledAddons = Prefs.getCharPref(PREF_EM_ENABLED_ADDONS, ""); |
michael@0 | 1914 | |
michael@0 | 1915 | // Invalidate the URI mappings now that |enabledAddons| was updated. |
michael@0 | 1916 | // |_ensureMappings()| will re-create the mappings when needed. |
michael@0 | 1917 | delete this._uriMappings; |
michael@0 | 1918 | |
michael@0 | 1919 | if ("nsICrashReporter" in Ci && |
michael@0 | 1920 | Services.appinfo instanceof Ci.nsICrashReporter) { |
michael@0 | 1921 | // Annotate the crash report with relevant add-on information. |
michael@0 | 1922 | try { |
michael@0 | 1923 | Services.appinfo.annotateCrashReport("Theme", this.currentSkin); |
michael@0 | 1924 | } catch (e) { } |
michael@0 | 1925 | try { |
michael@0 | 1926 | Services.appinfo.annotateCrashReport("EMCheckCompatibility", |
michael@0 | 1927 | AddonManager.checkCompatibility); |
michael@0 | 1928 | } catch (e) { } |
michael@0 | 1929 | this.addAddonsToCrashReporter(); |
michael@0 | 1930 | } |
michael@0 | 1931 | |
michael@0 | 1932 | try { |
michael@0 | 1933 | AddonManagerPrivate.recordTimestamp("XPI_bootstrap_addons_begin"); |
michael@0 | 1934 | for (let id in this.bootstrappedAddons) { |
michael@0 | 1935 | try { |
michael@0 | 1936 | let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); |
michael@0 | 1937 | file.persistentDescriptor = this.bootstrappedAddons[id].descriptor; |
michael@0 | 1938 | let reason = BOOTSTRAP_REASONS.APP_STARTUP; |
michael@0 | 1939 | // Eventually set INSTALLED reason when a bootstrap addon |
michael@0 | 1940 | // is dropped in profile folder and automatically installed |
michael@0 | 1941 | if (AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_INSTALLED) |
michael@0 | 1942 | .indexOf(id) !== -1) |
michael@0 | 1943 | reason = BOOTSTRAP_REASONS.ADDON_INSTALL; |
michael@0 | 1944 | this.callBootstrapMethod(id, this.bootstrappedAddons[id].version, |
michael@0 | 1945 | this.bootstrappedAddons[id].type, file, |
michael@0 | 1946 | "startup", reason); |
michael@0 | 1947 | } |
michael@0 | 1948 | catch (e) { |
michael@0 | 1949 | logger.error("Failed to load bootstrap addon " + id + " from " + |
michael@0 | 1950 | this.bootstrappedAddons[id].descriptor, e); |
michael@0 | 1951 | } |
michael@0 | 1952 | } |
michael@0 | 1953 | AddonManagerPrivate.recordTimestamp("XPI_bootstrap_addons_end"); |
michael@0 | 1954 | } |
michael@0 | 1955 | catch (e) { |
michael@0 | 1956 | logger.error("bootstrap startup failed", e); |
michael@0 | 1957 | AddonManagerPrivate.recordException("XPI-BOOTSTRAP", "startup failed", e); |
michael@0 | 1958 | } |
michael@0 | 1959 | |
michael@0 | 1960 | // Let these shutdown a little earlier when they still have access to most |
michael@0 | 1961 | // of XPCOM |
michael@0 | 1962 | Services.obs.addObserver({ |
michael@0 | 1963 | observe: function shutdownObserver(aSubject, aTopic, aData) { |
michael@0 | 1964 | for (let id in XPIProvider.bootstrappedAddons) { |
michael@0 | 1965 | let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); |
michael@0 | 1966 | file.persistentDescriptor = XPIProvider.bootstrappedAddons[id].descriptor; |
michael@0 | 1967 | XPIProvider.callBootstrapMethod(id, XPIProvider.bootstrappedAddons[id].version, |
michael@0 | 1968 | XPIProvider.bootstrappedAddons[id].type, file, "shutdown", |
michael@0 | 1969 | BOOTSTRAP_REASONS.APP_SHUTDOWN); |
michael@0 | 1970 | } |
michael@0 | 1971 | Services.obs.removeObserver(this, "quit-application-granted"); |
michael@0 | 1972 | } |
michael@0 | 1973 | }, "quit-application-granted", false); |
michael@0 | 1974 | |
michael@0 | 1975 | // Detect final-ui-startup for telemetry reporting |
michael@0 | 1976 | Services.obs.addObserver({ |
michael@0 | 1977 | observe: function uiStartupObserver(aSubject, aTopic, aData) { |
michael@0 | 1978 | AddonManagerPrivate.recordTimestamp("XPI_finalUIStartup"); |
michael@0 | 1979 | XPIProvider.runPhase = XPI_AFTER_UI_STARTUP; |
michael@0 | 1980 | Services.obs.removeObserver(this, "final-ui-startup"); |
michael@0 | 1981 | } |
michael@0 | 1982 | }, "final-ui-startup", false); |
michael@0 | 1983 | |
michael@0 | 1984 | AddonManagerPrivate.recordTimestamp("XPI_startup_end"); |
michael@0 | 1985 | |
michael@0 | 1986 | this.extensionsActive = true; |
michael@0 | 1987 | this.runPhase = XPI_BEFORE_UI_STARTUP; |
michael@0 | 1988 | } |
michael@0 | 1989 | catch (e) { |
michael@0 | 1990 | logger.error("startup failed", e); |
michael@0 | 1991 | AddonManagerPrivate.recordException("XPI", "startup failed", e); |
michael@0 | 1992 | } |
michael@0 | 1993 | }, |
michael@0 | 1994 | |
michael@0 | 1995 | /** |
michael@0 | 1996 | * Shuts down the database and releases all references. |
michael@0 | 1997 | * Return: Promise{integer} resolves / rejects with the result of |
michael@0 | 1998 | * flushing the XPI Database if it was loaded, |
michael@0 | 1999 | * 0 otherwise. |
michael@0 | 2000 | */ |
michael@0 | 2001 | shutdown: function XPI_shutdown() { |
michael@0 | 2002 | logger.debug("shutdown"); |
michael@0 | 2003 | |
michael@0 | 2004 | // Stop anything we were doing asynchronously |
michael@0 | 2005 | this.cancelAll(); |
michael@0 | 2006 | |
michael@0 | 2007 | this.bootstrappedAddons = {}; |
michael@0 | 2008 | this.bootstrapScopes = {}; |
michael@0 | 2009 | this.enabledAddons = null; |
michael@0 | 2010 | this.allAppGlobal = true; |
michael@0 | 2011 | |
michael@0 | 2012 | this.inactiveAddonIDs = []; |
michael@0 | 2013 | |
michael@0 | 2014 | // If there are pending operations then we must update the list of active |
michael@0 | 2015 | // add-ons |
michael@0 | 2016 | if (Prefs.getBoolPref(PREF_PENDING_OPERATIONS, false)) { |
michael@0 | 2017 | XPIDatabase.updateActiveAddons(); |
michael@0 | 2018 | Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, |
michael@0 | 2019 | !XPIDatabase.writeAddonsList()); |
michael@0 | 2020 | } |
michael@0 | 2021 | |
michael@0 | 2022 | this.installs = null; |
michael@0 | 2023 | this.installLocations = null; |
michael@0 | 2024 | this.installLocationsByName = null; |
michael@0 | 2025 | |
michael@0 | 2026 | // This is needed to allow xpcshell tests to simulate a restart |
michael@0 | 2027 | this.extensionsActive = false; |
michael@0 | 2028 | |
michael@0 | 2029 | // Remove URI mappings again |
michael@0 | 2030 | delete this._uriMappings; |
michael@0 | 2031 | |
michael@0 | 2032 | if (gLazyObjectsLoaded) { |
michael@0 | 2033 | let done = XPIDatabase.shutdown(); |
michael@0 | 2034 | done.then( |
michael@0 | 2035 | ret => { |
michael@0 | 2036 | logger.debug("Notifying XPI shutdown observers"); |
michael@0 | 2037 | Services.obs.notifyObservers(null, "xpi-provider-shutdown", null); |
michael@0 | 2038 | }, |
michael@0 | 2039 | err => { |
michael@0 | 2040 | logger.debug("Notifying XPI shutdown observers"); |
michael@0 | 2041 | this._shutdownError = err; |
michael@0 | 2042 | Services.obs.notifyObservers(null, "xpi-provider-shutdown", err); |
michael@0 | 2043 | } |
michael@0 | 2044 | ); |
michael@0 | 2045 | return done; |
michael@0 | 2046 | } |
michael@0 | 2047 | else { |
michael@0 | 2048 | logger.debug("Notifying XPI shutdown observers"); |
michael@0 | 2049 | Services.obs.notifyObservers(null, "xpi-provider-shutdown", null); |
michael@0 | 2050 | } |
michael@0 | 2051 | }, |
michael@0 | 2052 | |
michael@0 | 2053 | /** |
michael@0 | 2054 | * Applies any pending theme change to the preferences. |
michael@0 | 2055 | */ |
michael@0 | 2056 | applyThemeChange: function XPI_applyThemeChange() { |
michael@0 | 2057 | if (!Prefs.getBoolPref(PREF_DSS_SWITCHPENDING, false)) |
michael@0 | 2058 | return; |
michael@0 | 2059 | |
michael@0 | 2060 | // Tell the Chrome Registry which Skin to select |
michael@0 | 2061 | try { |
michael@0 | 2062 | this.selectedSkin = Prefs.getCharPref(PREF_DSS_SKIN_TO_SELECT); |
michael@0 | 2063 | Services.prefs.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN, |
michael@0 | 2064 | this.selectedSkin); |
michael@0 | 2065 | Services.prefs.clearUserPref(PREF_DSS_SKIN_TO_SELECT); |
michael@0 | 2066 | logger.debug("Changed skin to " + this.selectedSkin); |
michael@0 | 2067 | this.currentSkin = this.selectedSkin; |
michael@0 | 2068 | } |
michael@0 | 2069 | catch (e) { |
michael@0 | 2070 | logger.error("Error applying theme change", e); |
michael@0 | 2071 | } |
michael@0 | 2072 | Services.prefs.clearUserPref(PREF_DSS_SWITCHPENDING); |
michael@0 | 2073 | }, |
michael@0 | 2074 | |
michael@0 | 2075 | /** |
michael@0 | 2076 | * Shows the "Compatibility Updates" UI |
michael@0 | 2077 | */ |
michael@0 | 2078 | showUpgradeUI: function XPI_showUpgradeUI() { |
michael@0 | 2079 | // Flip a flag to indicate that we interrupted startup with an interactive prompt |
michael@0 | 2080 | Services.startup.interrupted = true; |
michael@0 | 2081 | |
michael@0 | 2082 | if (!Prefs.getBoolPref(PREF_SHOWN_SELECTION_UI, false)) { |
michael@0 | 2083 | // This *must* be modal as it has to block startup. |
michael@0 | 2084 | var features = "chrome,centerscreen,dialog,titlebar,modal"; |
michael@0 | 2085 | Services.ww.openWindow(null, URI_EXTENSION_SELECT_DIALOG, "", features, null); |
michael@0 | 2086 | Services.prefs.setBoolPref(PREF_SHOWN_SELECTION_UI, true); |
michael@0 | 2087 | } |
michael@0 | 2088 | else { |
michael@0 | 2089 | var variant = Cc["@mozilla.org/variant;1"]. |
michael@0 | 2090 | createInstance(Ci.nsIWritableVariant); |
michael@0 | 2091 | variant.setFromVariant(this.inactiveAddonIDs); |
michael@0 | 2092 | |
michael@0 | 2093 | // This *must* be modal as it has to block startup. |
michael@0 | 2094 | var features = "chrome,centerscreen,dialog,titlebar,modal"; |
michael@0 | 2095 | var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"]. |
michael@0 | 2096 | getService(Ci.nsIWindowWatcher); |
michael@0 | 2097 | ww.openWindow(null, URI_EXTENSION_UPDATE_DIALOG, "", features, variant); |
michael@0 | 2098 | } |
michael@0 | 2099 | |
michael@0 | 2100 | // Ensure any changes to the add-ons list are flushed to disk |
michael@0 | 2101 | Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, |
michael@0 | 2102 | !XPIDatabase.writeAddonsList()); |
michael@0 | 2103 | }, |
michael@0 | 2104 | |
michael@0 | 2105 | /** |
michael@0 | 2106 | * Persists changes to XPIProvider.bootstrappedAddons to its store (a pref). |
michael@0 | 2107 | */ |
michael@0 | 2108 | persistBootstrappedAddons: function XPI_persistBootstrappedAddons() { |
michael@0 | 2109 | // Experiments are disabled upon app load, so don't persist references. |
michael@0 | 2110 | let filtered = {}; |
michael@0 | 2111 | for (let id in this.bootstrappedAddons) { |
michael@0 | 2112 | let entry = this.bootstrappedAddons[id]; |
michael@0 | 2113 | if (entry.type == "experiment") { |
michael@0 | 2114 | continue; |
michael@0 | 2115 | } |
michael@0 | 2116 | |
michael@0 | 2117 | filtered[id] = entry; |
michael@0 | 2118 | } |
michael@0 | 2119 | |
michael@0 | 2120 | Services.prefs.setCharPref(PREF_BOOTSTRAP_ADDONS, |
michael@0 | 2121 | JSON.stringify(filtered)); |
michael@0 | 2122 | }, |
michael@0 | 2123 | |
michael@0 | 2124 | /** |
michael@0 | 2125 | * Adds a list of currently active add-ons to the next crash report. |
michael@0 | 2126 | */ |
michael@0 | 2127 | addAddonsToCrashReporter: function XPI_addAddonsToCrashReporter() { |
michael@0 | 2128 | if (!("nsICrashReporter" in Ci) || |
michael@0 | 2129 | !(Services.appinfo instanceof Ci.nsICrashReporter)) |
michael@0 | 2130 | return; |
michael@0 | 2131 | |
michael@0 | 2132 | // In safe mode no add-ons are loaded so we should not include them in the |
michael@0 | 2133 | // crash report |
michael@0 | 2134 | if (Services.appinfo.inSafeMode) |
michael@0 | 2135 | return; |
michael@0 | 2136 | |
michael@0 | 2137 | let data = this.enabledAddons; |
michael@0 | 2138 | for (let id in this.bootstrappedAddons) { |
michael@0 | 2139 | data += (data ? "," : "") + encodeURIComponent(id) + ":" + |
michael@0 | 2140 | encodeURIComponent(this.bootstrappedAddons[id].version); |
michael@0 | 2141 | } |
michael@0 | 2142 | |
michael@0 | 2143 | try { |
michael@0 | 2144 | Services.appinfo.annotateCrashReport("Add-ons", data); |
michael@0 | 2145 | } |
michael@0 | 2146 | catch (e) { } |
michael@0 | 2147 | |
michael@0 | 2148 | Cu.import("resource://gre/modules/TelemetryPing.jsm", {}).TelemetryPing.setAddOns(data); |
michael@0 | 2149 | }, |
michael@0 | 2150 | |
michael@0 | 2151 | /** |
michael@0 | 2152 | * Gets the add-on states for an install location. |
michael@0 | 2153 | * This function may be expensive because of the recursiveLastModifiedTime call. |
michael@0 | 2154 | * |
michael@0 | 2155 | * @param location |
michael@0 | 2156 | * The install location to retrieve the add-on states for |
michael@0 | 2157 | * @return a dictionary mapping add-on IDs to objects with a descriptor |
michael@0 | 2158 | * property which contains the add-ons dir/file descriptor and an |
michael@0 | 2159 | * mtime property which contains the add-on's last modified time as |
michael@0 | 2160 | * the number of milliseconds since the epoch. |
michael@0 | 2161 | */ |
michael@0 | 2162 | getAddonStates: function XPI_getAddonStates(aLocation) { |
michael@0 | 2163 | let addonStates = {}; |
michael@0 | 2164 | for (let file of aLocation.addonLocations) { |
michael@0 | 2165 | let scanStarted = Date.now(); |
michael@0 | 2166 | let id = aLocation.getIDForLocation(file); |
michael@0 | 2167 | let unpacked = 0; |
michael@0 | 2168 | let [modFile, modTime, items] = recursiveLastModifiedTime(file); |
michael@0 | 2169 | addonStates[id] = { |
michael@0 | 2170 | descriptor: file.persistentDescriptor, |
michael@0 | 2171 | mtime: modTime |
michael@0 | 2172 | }; |
michael@0 | 2173 | try { |
michael@0 | 2174 | // get the install.rdf update time, if any |
michael@0 | 2175 | file.append(FILE_INSTALL_MANIFEST); |
michael@0 | 2176 | let rdfTime = file.lastModifiedTime; |
michael@0 | 2177 | addonStates[id].rdfTime = rdfTime; |
michael@0 | 2178 | unpacked = 1; |
michael@0 | 2179 | } |
michael@0 | 2180 | catch (e) { } |
michael@0 | 2181 | this._mostRecentlyModifiedFile[id] = modFile; |
michael@0 | 2182 | this.setTelemetry(id, "unpacked", unpacked); |
michael@0 | 2183 | this.setTelemetry(id, "location", aLocation.name); |
michael@0 | 2184 | this.setTelemetry(id, "scan_MS", Date.now() - scanStarted); |
michael@0 | 2185 | this.setTelemetry(id, "scan_items", items); |
michael@0 | 2186 | } |
michael@0 | 2187 | |
michael@0 | 2188 | return addonStates; |
michael@0 | 2189 | }, |
michael@0 | 2190 | |
michael@0 | 2191 | /** |
michael@0 | 2192 | * Gets an array of install location states which uniquely describes all |
michael@0 | 2193 | * installed add-ons with the add-on's InstallLocation name and last modified |
michael@0 | 2194 | * time. This function may be expensive because of the getAddonStates() call. |
michael@0 | 2195 | * |
michael@0 | 2196 | * @return an array of add-on states for each install location. Each state |
michael@0 | 2197 | * is an object with a name property holding the location's name and |
michael@0 | 2198 | * an addons property holding the add-on states for the location |
michael@0 | 2199 | */ |
michael@0 | 2200 | getInstallLocationStates: function XPI_getInstallLocationStates() { |
michael@0 | 2201 | let states = []; |
michael@0 | 2202 | this.installLocations.forEach(function(aLocation) { |
michael@0 | 2203 | let addons = aLocation.addonLocations; |
michael@0 | 2204 | if (addons.length == 0) |
michael@0 | 2205 | return; |
michael@0 | 2206 | |
michael@0 | 2207 | let locationState = { |
michael@0 | 2208 | name: aLocation.name, |
michael@0 | 2209 | addons: this.getAddonStates(aLocation) |
michael@0 | 2210 | }; |
michael@0 | 2211 | |
michael@0 | 2212 | states.push(locationState); |
michael@0 | 2213 | }, this); |
michael@0 | 2214 | return states; |
michael@0 | 2215 | }, |
michael@0 | 2216 | |
michael@0 | 2217 | /** |
michael@0 | 2218 | * Check the staging directories of install locations for any add-ons to be |
michael@0 | 2219 | * installed or add-ons to be uninstalled. |
michael@0 | 2220 | * |
michael@0 | 2221 | * @param aManifests |
michael@0 | 2222 | * A dictionary to add detected install manifests to for the purpose |
michael@0 | 2223 | * of passing through updated compatibility information |
michael@0 | 2224 | * @return true if an add-on was installed or uninstalled |
michael@0 | 2225 | */ |
michael@0 | 2226 | processPendingFileChanges: function XPI_processPendingFileChanges(aManifests) { |
michael@0 | 2227 | let changed = false; |
michael@0 | 2228 | this.installLocations.forEach(function(aLocation) { |
michael@0 | 2229 | aManifests[aLocation.name] = {}; |
michael@0 | 2230 | // We can't install or uninstall anything in locked locations |
michael@0 | 2231 | if (aLocation.locked) |
michael@0 | 2232 | return; |
michael@0 | 2233 | |
michael@0 | 2234 | let stagedXPIDir = aLocation.getXPIStagingDir(); |
michael@0 | 2235 | let stagingDir = aLocation.getStagingDir(); |
michael@0 | 2236 | |
michael@0 | 2237 | if (stagedXPIDir.exists() && stagedXPIDir.isDirectory()) { |
michael@0 | 2238 | let entries = stagedXPIDir.directoryEntries |
michael@0 | 2239 | .QueryInterface(Ci.nsIDirectoryEnumerator); |
michael@0 | 2240 | while (entries.hasMoreElements()) { |
michael@0 | 2241 | let stageDirEntry = entries.nextFile; |
michael@0 | 2242 | |
michael@0 | 2243 | if (!stageDirEntry.isDirectory()) { |
michael@0 | 2244 | logger.warn("Ignoring file in XPI staging directory: " + stageDirEntry.path); |
michael@0 | 2245 | continue; |
michael@0 | 2246 | } |
michael@0 | 2247 | |
michael@0 | 2248 | // Find the last added XPI file in the directory |
michael@0 | 2249 | let stagedXPI = null; |
michael@0 | 2250 | var xpiEntries = stageDirEntry.directoryEntries |
michael@0 | 2251 | .QueryInterface(Ci.nsIDirectoryEnumerator); |
michael@0 | 2252 | while (xpiEntries.hasMoreElements()) { |
michael@0 | 2253 | let file = xpiEntries.nextFile; |
michael@0 | 2254 | if (file.isDirectory()) |
michael@0 | 2255 | continue; |
michael@0 | 2256 | |
michael@0 | 2257 | let extension = file.leafName; |
michael@0 | 2258 | extension = extension.substring(extension.length - 4); |
michael@0 | 2259 | |
michael@0 | 2260 | if (extension != ".xpi" && extension != ".jar") |
michael@0 | 2261 | continue; |
michael@0 | 2262 | |
michael@0 | 2263 | stagedXPI = file; |
michael@0 | 2264 | } |
michael@0 | 2265 | xpiEntries.close(); |
michael@0 | 2266 | |
michael@0 | 2267 | if (!stagedXPI) |
michael@0 | 2268 | continue; |
michael@0 | 2269 | |
michael@0 | 2270 | let addon = null; |
michael@0 | 2271 | try { |
michael@0 | 2272 | addon = loadManifestFromZipFile(stagedXPI); |
michael@0 | 2273 | } |
michael@0 | 2274 | catch (e) { |
michael@0 | 2275 | logger.error("Unable to read add-on manifest from " + stagedXPI.path, e); |
michael@0 | 2276 | continue; |
michael@0 | 2277 | } |
michael@0 | 2278 | |
michael@0 | 2279 | logger.debug("Migrating staged install of " + addon.id + " in " + aLocation.name); |
michael@0 | 2280 | |
michael@0 | 2281 | if (addon.unpack || Prefs.getBoolPref(PREF_XPI_UNPACK, false)) { |
michael@0 | 2282 | let targetDir = stagingDir.clone(); |
michael@0 | 2283 | targetDir.append(addon.id); |
michael@0 | 2284 | try { |
michael@0 | 2285 | targetDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); |
michael@0 | 2286 | } |
michael@0 | 2287 | catch (e) { |
michael@0 | 2288 | logger.error("Failed to create staging directory for add-on " + addon.id, e); |
michael@0 | 2289 | continue; |
michael@0 | 2290 | } |
michael@0 | 2291 | |
michael@0 | 2292 | try { |
michael@0 | 2293 | ZipUtils.extractFiles(stagedXPI, targetDir); |
michael@0 | 2294 | } |
michael@0 | 2295 | catch (e) { |
michael@0 | 2296 | logger.error("Failed to extract staged XPI for add-on " + addon.id + " in " + |
michael@0 | 2297 | aLocation.name, e); |
michael@0 | 2298 | } |
michael@0 | 2299 | } |
michael@0 | 2300 | else { |
michael@0 | 2301 | try { |
michael@0 | 2302 | stagedXPI.moveTo(stagingDir, addon.id + ".xpi"); |
michael@0 | 2303 | } |
michael@0 | 2304 | catch (e) { |
michael@0 | 2305 | logger.error("Failed to move staged XPI for add-on " + addon.id + " in " + |
michael@0 | 2306 | aLocation.name, e); |
michael@0 | 2307 | } |
michael@0 | 2308 | } |
michael@0 | 2309 | } |
michael@0 | 2310 | entries.close(); |
michael@0 | 2311 | } |
michael@0 | 2312 | |
michael@0 | 2313 | if (stagedXPIDir.exists()) { |
michael@0 | 2314 | try { |
michael@0 | 2315 | recursiveRemove(stagedXPIDir); |
michael@0 | 2316 | } |
michael@0 | 2317 | catch (e) { |
michael@0 | 2318 | // Non-critical, just saves some perf on startup if we clean this up. |
michael@0 | 2319 | logger.debug("Error removing XPI staging dir " + stagedXPIDir.path, e); |
michael@0 | 2320 | } |
michael@0 | 2321 | } |
michael@0 | 2322 | |
michael@0 | 2323 | try { |
michael@0 | 2324 | if (!stagingDir || !stagingDir.exists() || !stagingDir.isDirectory()) |
michael@0 | 2325 | return; |
michael@0 | 2326 | } |
michael@0 | 2327 | catch (e) { |
michael@0 | 2328 | logger.warn("Failed to find staging directory", e); |
michael@0 | 2329 | return; |
michael@0 | 2330 | } |
michael@0 | 2331 | |
michael@0 | 2332 | let seenFiles = []; |
michael@0 | 2333 | // Use a snapshot of the directory contents to avoid possible issues with |
michael@0 | 2334 | // iterating over a directory while removing files from it (the YAFFS2 |
michael@0 | 2335 | // embedded filesystem has this issue, see bug 772238), and to remove |
michael@0 | 2336 | // normal files before their resource forks on OSX (see bug 733436). |
michael@0 | 2337 | let stagingDirEntries = getDirectoryEntries(stagingDir, true); |
michael@0 | 2338 | for (let stageDirEntry of stagingDirEntries) { |
michael@0 | 2339 | let id = stageDirEntry.leafName; |
michael@0 | 2340 | |
michael@0 | 2341 | let isDir; |
michael@0 | 2342 | try { |
michael@0 | 2343 | isDir = stageDirEntry.isDirectory(); |
michael@0 | 2344 | } |
michael@0 | 2345 | catch (e if e.result == Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) { |
michael@0 | 2346 | // If the file has already gone away then don't worry about it, this |
michael@0 | 2347 | // can happen on OSX where the resource fork is automatically moved |
michael@0 | 2348 | // with the data fork for the file. See bug 733436. |
michael@0 | 2349 | continue; |
michael@0 | 2350 | } |
michael@0 | 2351 | |
michael@0 | 2352 | if (!isDir) { |
michael@0 | 2353 | if (id.substring(id.length - 4).toLowerCase() == ".xpi") { |
michael@0 | 2354 | id = id.substring(0, id.length - 4); |
michael@0 | 2355 | } |
michael@0 | 2356 | else { |
michael@0 | 2357 | if (id.substring(id.length - 5).toLowerCase() != ".json") { |
michael@0 | 2358 | logger.warn("Ignoring file: " + stageDirEntry.path); |
michael@0 | 2359 | seenFiles.push(stageDirEntry.leafName); |
michael@0 | 2360 | } |
michael@0 | 2361 | continue; |
michael@0 | 2362 | } |
michael@0 | 2363 | } |
michael@0 | 2364 | |
michael@0 | 2365 | // Check that the directory's name is a valid ID. |
michael@0 | 2366 | if (!gIDTest.test(id)) { |
michael@0 | 2367 | logger.warn("Ignoring directory whose name is not a valid add-on ID: " + |
michael@0 | 2368 | stageDirEntry.path); |
michael@0 | 2369 | seenFiles.push(stageDirEntry.leafName); |
michael@0 | 2370 | continue; |
michael@0 | 2371 | } |
michael@0 | 2372 | |
michael@0 | 2373 | changed = true; |
michael@0 | 2374 | |
michael@0 | 2375 | if (isDir) { |
michael@0 | 2376 | // Check if the directory contains an install manifest. |
michael@0 | 2377 | let manifest = stageDirEntry.clone(); |
michael@0 | 2378 | manifest.append(FILE_INSTALL_MANIFEST); |
michael@0 | 2379 | |
michael@0 | 2380 | // If the install manifest doesn't exist uninstall this add-on in this |
michael@0 | 2381 | // install location. |
michael@0 | 2382 | if (!manifest.exists()) { |
michael@0 | 2383 | logger.debug("Processing uninstall of " + id + " in " + aLocation.name); |
michael@0 | 2384 | try { |
michael@0 | 2385 | aLocation.uninstallAddon(id); |
michael@0 | 2386 | seenFiles.push(stageDirEntry.leafName); |
michael@0 | 2387 | } |
michael@0 | 2388 | catch (e) { |
michael@0 | 2389 | logger.error("Failed to uninstall add-on " + id + " in " + aLocation.name, e); |
michael@0 | 2390 | } |
michael@0 | 2391 | // The file check later will spot the removal and cleanup the database |
michael@0 | 2392 | continue; |
michael@0 | 2393 | } |
michael@0 | 2394 | } |
michael@0 | 2395 | |
michael@0 | 2396 | aManifests[aLocation.name][id] = null; |
michael@0 | 2397 | let existingAddonID = id; |
michael@0 | 2398 | |
michael@0 | 2399 | let jsonfile = stagingDir.clone(); |
michael@0 | 2400 | jsonfile.append(id + ".json"); |
michael@0 | 2401 | |
michael@0 | 2402 | try { |
michael@0 | 2403 | aManifests[aLocation.name][id] = loadManifestFromFile(stageDirEntry); |
michael@0 | 2404 | } |
michael@0 | 2405 | catch (e) { |
michael@0 | 2406 | logger.error("Unable to read add-on manifest from " + stageDirEntry.path, e); |
michael@0 | 2407 | // This add-on can't be installed so just remove it now |
michael@0 | 2408 | seenFiles.push(stageDirEntry.leafName); |
michael@0 | 2409 | seenFiles.push(jsonfile.leafName); |
michael@0 | 2410 | continue; |
michael@0 | 2411 | } |
michael@0 | 2412 | |
michael@0 | 2413 | // Check for a cached metadata for this add-on, it may contain updated |
michael@0 | 2414 | // compatibility information |
michael@0 | 2415 | if (jsonfile.exists()) { |
michael@0 | 2416 | logger.debug("Found updated metadata for " + id + " in " + aLocation.name); |
michael@0 | 2417 | let fis = Cc["@mozilla.org/network/file-input-stream;1"]. |
michael@0 | 2418 | createInstance(Ci.nsIFileInputStream); |
michael@0 | 2419 | let json = Cc["@mozilla.org/dom/json;1"]. |
michael@0 | 2420 | createInstance(Ci.nsIJSON); |
michael@0 | 2421 | |
michael@0 | 2422 | try { |
michael@0 | 2423 | fis.init(jsonfile, -1, 0, 0); |
michael@0 | 2424 | let metadata = json.decodeFromStream(fis, jsonfile.fileSize); |
michael@0 | 2425 | aManifests[aLocation.name][id].importMetadata(metadata); |
michael@0 | 2426 | } |
michael@0 | 2427 | catch (e) { |
michael@0 | 2428 | // If some data can't be recovered from the cached metadata then it |
michael@0 | 2429 | // is unlikely to be a problem big enough to justify throwing away |
michael@0 | 2430 | // the install, just log and error and continue |
michael@0 | 2431 | logger.error("Unable to read metadata from " + jsonfile.path, e); |
michael@0 | 2432 | } |
michael@0 | 2433 | finally { |
michael@0 | 2434 | fis.close(); |
michael@0 | 2435 | } |
michael@0 | 2436 | } |
michael@0 | 2437 | seenFiles.push(jsonfile.leafName); |
michael@0 | 2438 | |
michael@0 | 2439 | existingAddonID = aManifests[aLocation.name][id].existingAddonID || id; |
michael@0 | 2440 | |
michael@0 | 2441 | var oldBootstrap = null; |
michael@0 | 2442 | logger.debug("Processing install of " + id + " in " + aLocation.name); |
michael@0 | 2443 | if (existingAddonID in this.bootstrappedAddons) { |
michael@0 | 2444 | try { |
michael@0 | 2445 | var existingAddon = aLocation.getLocationForID(existingAddonID); |
michael@0 | 2446 | if (this.bootstrappedAddons[existingAddonID].descriptor == |
michael@0 | 2447 | existingAddon.persistentDescriptor) { |
michael@0 | 2448 | oldBootstrap = this.bootstrappedAddons[existingAddonID]; |
michael@0 | 2449 | |
michael@0 | 2450 | // We'll be replacing a currently active bootstrapped add-on so |
michael@0 | 2451 | // call its uninstall method |
michael@0 | 2452 | let newVersion = aManifests[aLocation.name][id].version; |
michael@0 | 2453 | let oldVersion = oldBootstrap.version; |
michael@0 | 2454 | let uninstallReason = Services.vc.compare(oldVersion, newVersion) < 0 ? |
michael@0 | 2455 | BOOTSTRAP_REASONS.ADDON_UPGRADE : |
michael@0 | 2456 | BOOTSTRAP_REASONS.ADDON_DOWNGRADE; |
michael@0 | 2457 | |
michael@0 | 2458 | this.callBootstrapMethod(existingAddonID, oldBootstrap.version, |
michael@0 | 2459 | oldBootstrap.type, existingAddon, "uninstall", uninstallReason, |
michael@0 | 2460 | { newVersion: newVersion }); |
michael@0 | 2461 | this.unloadBootstrapScope(existingAddonID); |
michael@0 | 2462 | flushStartupCache(); |
michael@0 | 2463 | } |
michael@0 | 2464 | } |
michael@0 | 2465 | catch (e) { |
michael@0 | 2466 | } |
michael@0 | 2467 | } |
michael@0 | 2468 | |
michael@0 | 2469 | try { |
michael@0 | 2470 | var addonInstallLocation = aLocation.installAddon(id, stageDirEntry, |
michael@0 | 2471 | existingAddonID); |
michael@0 | 2472 | if (aManifests[aLocation.name][id]) |
michael@0 | 2473 | aManifests[aLocation.name][id]._sourceBundle = addonInstallLocation; |
michael@0 | 2474 | } |
michael@0 | 2475 | catch (e) { |
michael@0 | 2476 | logger.error("Failed to install staged add-on " + id + " in " + aLocation.name, |
michael@0 | 2477 | e); |
michael@0 | 2478 | // Re-create the staged install |
michael@0 | 2479 | AddonInstall.createStagedInstall(aLocation, stageDirEntry, |
michael@0 | 2480 | aManifests[aLocation.name][id]); |
michael@0 | 2481 | // Make sure not to delete the cached manifest json file |
michael@0 | 2482 | seenFiles.pop(); |
michael@0 | 2483 | |
michael@0 | 2484 | delete aManifests[aLocation.name][id]; |
michael@0 | 2485 | |
michael@0 | 2486 | if (oldBootstrap) { |
michael@0 | 2487 | // Re-install the old add-on |
michael@0 | 2488 | this.callBootstrapMethod(existingAddonID, oldBootstrap.version, |
michael@0 | 2489 | oldBootstrap.type, existingAddon, "install", |
michael@0 | 2490 | BOOTSTRAP_REASONS.ADDON_INSTALL); |
michael@0 | 2491 | } |
michael@0 | 2492 | continue; |
michael@0 | 2493 | } |
michael@0 | 2494 | } |
michael@0 | 2495 | |
michael@0 | 2496 | try { |
michael@0 | 2497 | aLocation.cleanStagingDir(seenFiles); |
michael@0 | 2498 | } |
michael@0 | 2499 | catch (e) { |
michael@0 | 2500 | // Non-critical, just saves some perf on startup if we clean this up. |
michael@0 | 2501 | logger.debug("Error cleaning staging dir " + stagingDir.path, e); |
michael@0 | 2502 | } |
michael@0 | 2503 | }, this); |
michael@0 | 2504 | return changed; |
michael@0 | 2505 | }, |
michael@0 | 2506 | |
michael@0 | 2507 | /** |
michael@0 | 2508 | * Installs any add-ons located in the extensions directory of the |
michael@0 | 2509 | * application's distribution specific directory into the profile unless a |
michael@0 | 2510 | * newer version already exists or the user has previously uninstalled the |
michael@0 | 2511 | * distributed add-on. |
michael@0 | 2512 | * |
michael@0 | 2513 | * @param aManifests |
michael@0 | 2514 | * A dictionary to add new install manifests to to save having to |
michael@0 | 2515 | * reload them later |
michael@0 | 2516 | * @return true if any new add-ons were installed |
michael@0 | 2517 | */ |
michael@0 | 2518 | installDistributionAddons: function XPI_installDistributionAddons(aManifests) { |
michael@0 | 2519 | let distroDir; |
michael@0 | 2520 | try { |
michael@0 | 2521 | distroDir = FileUtils.getDir(KEY_APP_DISTRIBUTION, [DIR_EXTENSIONS]); |
michael@0 | 2522 | } |
michael@0 | 2523 | catch (e) { |
michael@0 | 2524 | return false; |
michael@0 | 2525 | } |
michael@0 | 2526 | |
michael@0 | 2527 | if (!distroDir.exists()) |
michael@0 | 2528 | return false; |
michael@0 | 2529 | |
michael@0 | 2530 | if (!distroDir.isDirectory()) |
michael@0 | 2531 | return false; |
michael@0 | 2532 | |
michael@0 | 2533 | let changed = false; |
michael@0 | 2534 | let profileLocation = this.installLocationsByName[KEY_APP_PROFILE]; |
michael@0 | 2535 | |
michael@0 | 2536 | let entries = distroDir.directoryEntries |
michael@0 | 2537 | .QueryInterface(Ci.nsIDirectoryEnumerator); |
michael@0 | 2538 | let entry; |
michael@0 | 2539 | while ((entry = entries.nextFile)) { |
michael@0 | 2540 | |
michael@0 | 2541 | let id = entry.leafName; |
michael@0 | 2542 | |
michael@0 | 2543 | if (entry.isFile()) { |
michael@0 | 2544 | if (id.substring(id.length - 4).toLowerCase() == ".xpi") { |
michael@0 | 2545 | id = id.substring(0, id.length - 4); |
michael@0 | 2546 | } |
michael@0 | 2547 | else { |
michael@0 | 2548 | logger.debug("Ignoring distribution add-on that isn't an XPI: " + entry.path); |
michael@0 | 2549 | continue; |
michael@0 | 2550 | } |
michael@0 | 2551 | } |
michael@0 | 2552 | else if (!entry.isDirectory()) { |
michael@0 | 2553 | logger.debug("Ignoring distribution add-on that isn't a file or directory: " + |
michael@0 | 2554 | entry.path); |
michael@0 | 2555 | continue; |
michael@0 | 2556 | } |
michael@0 | 2557 | |
michael@0 | 2558 | if (!gIDTest.test(id)) { |
michael@0 | 2559 | logger.debug("Ignoring distribution add-on whose name is not a valid add-on ID: " + |
michael@0 | 2560 | entry.path); |
michael@0 | 2561 | continue; |
michael@0 | 2562 | } |
michael@0 | 2563 | |
michael@0 | 2564 | let addon; |
michael@0 | 2565 | try { |
michael@0 | 2566 | addon = loadManifestFromFile(entry); |
michael@0 | 2567 | } |
michael@0 | 2568 | catch (e) { |
michael@0 | 2569 | logger.warn("File entry " + entry.path + " contains an invalid add-on", e); |
michael@0 | 2570 | continue; |
michael@0 | 2571 | } |
michael@0 | 2572 | |
michael@0 | 2573 | if (addon.id != id) { |
michael@0 | 2574 | logger.warn("File entry " + entry.path + " contains an add-on with an " + |
michael@0 | 2575 | "incorrect ID") |
michael@0 | 2576 | continue; |
michael@0 | 2577 | } |
michael@0 | 2578 | |
michael@0 | 2579 | let existingEntry = null; |
michael@0 | 2580 | try { |
michael@0 | 2581 | existingEntry = profileLocation.getLocationForID(id); |
michael@0 | 2582 | } |
michael@0 | 2583 | catch (e) { |
michael@0 | 2584 | } |
michael@0 | 2585 | |
michael@0 | 2586 | if (existingEntry) { |
michael@0 | 2587 | let existingAddon; |
michael@0 | 2588 | try { |
michael@0 | 2589 | existingAddon = loadManifestFromFile(existingEntry); |
michael@0 | 2590 | |
michael@0 | 2591 | if (Services.vc.compare(addon.version, existingAddon.version) <= 0) |
michael@0 | 2592 | continue; |
michael@0 | 2593 | } |
michael@0 | 2594 | catch (e) { |
michael@0 | 2595 | // Bad add-on in the profile so just proceed and install over the top |
michael@0 | 2596 | logger.warn("Profile contains an add-on with a bad or missing install " + |
michael@0 | 2597 | "manifest at " + existingEntry.path + ", overwriting", e); |
michael@0 | 2598 | } |
michael@0 | 2599 | } |
michael@0 | 2600 | else if (Prefs.getBoolPref(PREF_BRANCH_INSTALLED_ADDON + id, false)) { |
michael@0 | 2601 | continue; |
michael@0 | 2602 | } |
michael@0 | 2603 | |
michael@0 | 2604 | // Install the add-on |
michael@0 | 2605 | try { |
michael@0 | 2606 | profileLocation.installAddon(id, entry, null, true); |
michael@0 | 2607 | logger.debug("Installed distribution add-on " + id); |
michael@0 | 2608 | |
michael@0 | 2609 | Services.prefs.setBoolPref(PREF_BRANCH_INSTALLED_ADDON + id, true) |
michael@0 | 2610 | |
michael@0 | 2611 | // aManifests may contain a copy of a newly installed add-on's manifest |
michael@0 | 2612 | // and we'll have overwritten that so instead cache our install manifest |
michael@0 | 2613 | // which will later be put into the database in processFileChanges |
michael@0 | 2614 | if (!(KEY_APP_PROFILE in aManifests)) |
michael@0 | 2615 | aManifests[KEY_APP_PROFILE] = {}; |
michael@0 | 2616 | aManifests[KEY_APP_PROFILE][id] = addon; |
michael@0 | 2617 | changed = true; |
michael@0 | 2618 | } |
michael@0 | 2619 | catch (e) { |
michael@0 | 2620 | logger.error("Failed to install distribution add-on " + entry.path, e); |
michael@0 | 2621 | } |
michael@0 | 2622 | } |
michael@0 | 2623 | |
michael@0 | 2624 | entries.close(); |
michael@0 | 2625 | |
michael@0 | 2626 | return changed; |
michael@0 | 2627 | }, |
michael@0 | 2628 | |
michael@0 | 2629 | /** |
michael@0 | 2630 | * Compares the add-ons that are currently installed to those that were |
michael@0 | 2631 | * known to be installed when the application last ran and applies any |
michael@0 | 2632 | * changes found to the database. Also sends "startupcache-invalidate" signal to |
michael@0 | 2633 | * observerservice if it detects that data may have changed. |
michael@0 | 2634 | * |
michael@0 | 2635 | * @param aState |
michael@0 | 2636 | * The array of current install location states |
michael@0 | 2637 | * @param aManifests |
michael@0 | 2638 | * A dictionary of cached AddonInstalls for add-ons that have been |
michael@0 | 2639 | * installed |
michael@0 | 2640 | * @param aUpdateCompatibility |
michael@0 | 2641 | * true to update add-ons appDisabled property when the application |
michael@0 | 2642 | * version has changed |
michael@0 | 2643 | * @param aOldAppVersion |
michael@0 | 2644 | * The version of the application last run with this profile or null |
michael@0 | 2645 | * if it is a new profile or the version is unknown |
michael@0 | 2646 | * @param aOldPlatformVersion |
michael@0 | 2647 | * The version of the platform last run with this profile or null |
michael@0 | 2648 | * if it is a new profile or the version is unknown |
michael@0 | 2649 | * @return a boolean indicating if a change requiring flushing the caches was |
michael@0 | 2650 | * detected |
michael@0 | 2651 | */ |
michael@0 | 2652 | processFileChanges: function XPI_processFileChanges(aState, aManifests, |
michael@0 | 2653 | aUpdateCompatibility, |
michael@0 | 2654 | aOldAppVersion, |
michael@0 | 2655 | aOldPlatformVersion) { |
michael@0 | 2656 | let visibleAddons = {}; |
michael@0 | 2657 | let oldBootstrappedAddons = this.bootstrappedAddons; |
michael@0 | 2658 | this.bootstrappedAddons = {}; |
michael@0 | 2659 | |
michael@0 | 2660 | /** |
michael@0 | 2661 | * Updates an add-on's metadata and determines if a restart of the |
michael@0 | 2662 | * application is necessary. This is called when either the add-on's |
michael@0 | 2663 | * install directory path or last modified time has changed. |
michael@0 | 2664 | * |
michael@0 | 2665 | * @param aInstallLocation |
michael@0 | 2666 | * The install location containing the add-on |
michael@0 | 2667 | * @param aOldAddon |
michael@0 | 2668 | * The AddonInternal as it appeared the last time the application |
michael@0 | 2669 | * ran |
michael@0 | 2670 | * @param aAddonState |
michael@0 | 2671 | * The new state of the add-on |
michael@0 | 2672 | * @return a boolean indicating if flushing caches is required to complete |
michael@0 | 2673 | * changing this add-on |
michael@0 | 2674 | */ |
michael@0 | 2675 | function updateMetadata(aInstallLocation, aOldAddon, aAddonState) { |
michael@0 | 2676 | logger.debug("Add-on " + aOldAddon.id + " modified in " + aInstallLocation.name); |
michael@0 | 2677 | |
michael@0 | 2678 | // Check if there is an updated install manifest for this add-on |
michael@0 | 2679 | let newAddon = aManifests[aInstallLocation.name][aOldAddon.id]; |
michael@0 | 2680 | |
michael@0 | 2681 | try { |
michael@0 | 2682 | // If not load it |
michael@0 | 2683 | if (!newAddon) { |
michael@0 | 2684 | let file = aInstallLocation.getLocationForID(aOldAddon.id); |
michael@0 | 2685 | newAddon = loadManifestFromFile(file); |
michael@0 | 2686 | applyBlocklistChanges(aOldAddon, newAddon); |
michael@0 | 2687 | |
michael@0 | 2688 | // Carry over any pendingUninstall state to add-ons modified directly |
michael@0 | 2689 | // in the profile. This is important when the attempt to remove the |
michael@0 | 2690 | // add-on in processPendingFileChanges failed and caused an mtime |
michael@0 | 2691 | // change to the add-ons files. |
michael@0 | 2692 | newAddon.pendingUninstall = aOldAddon.pendingUninstall; |
michael@0 | 2693 | } |
michael@0 | 2694 | |
michael@0 | 2695 | // The ID in the manifest that was loaded must match the ID of the old |
michael@0 | 2696 | // add-on. |
michael@0 | 2697 | if (newAddon.id != aOldAddon.id) |
michael@0 | 2698 | throw new Error("Incorrect id in install manifest"); |
michael@0 | 2699 | } |
michael@0 | 2700 | catch (e) { |
michael@0 | 2701 | logger.warn("Add-on is invalid", e); |
michael@0 | 2702 | XPIDatabase.removeAddonMetadata(aOldAddon); |
michael@0 | 2703 | if (!aInstallLocation.locked) |
michael@0 | 2704 | aInstallLocation.uninstallAddon(aOldAddon.id); |
michael@0 | 2705 | else |
michael@0 | 2706 | logger.warn("Could not uninstall invalid item from locked install location"); |
michael@0 | 2707 | // If this was an active add-on then we must force a restart |
michael@0 | 2708 | if (aOldAddon.active) |
michael@0 | 2709 | return true; |
michael@0 | 2710 | |
michael@0 | 2711 | return false; |
michael@0 | 2712 | } |
michael@0 | 2713 | |
michael@0 | 2714 | // Set the additional properties on the new AddonInternal |
michael@0 | 2715 | newAddon._installLocation = aInstallLocation; |
michael@0 | 2716 | newAddon.updateDate = aAddonState.mtime; |
michael@0 | 2717 | newAddon.visible = !(newAddon.id in visibleAddons); |
michael@0 | 2718 | |
michael@0 | 2719 | // Update the database |
michael@0 | 2720 | let newDBAddon = XPIDatabase.updateAddonMetadata(aOldAddon, newAddon, |
michael@0 | 2721 | aAddonState.descriptor); |
michael@0 | 2722 | if (newDBAddon.visible) { |
michael@0 | 2723 | visibleAddons[newDBAddon.id] = newDBAddon; |
michael@0 | 2724 | // Remember add-ons that were changed during startup |
michael@0 | 2725 | AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED, |
michael@0 | 2726 | newDBAddon.id); |
michael@0 | 2727 | |
michael@0 | 2728 | // If this was the active theme and it is now disabled then enable the |
michael@0 | 2729 | // default theme |
michael@0 | 2730 | if (aOldAddon.active && isAddonDisabled(newDBAddon)) |
michael@0 | 2731 | XPIProvider.enableDefaultTheme(); |
michael@0 | 2732 | |
michael@0 | 2733 | // If the new add-on is bootstrapped and active then call its install method |
michael@0 | 2734 | if (newDBAddon.active && newDBAddon.bootstrap) { |
michael@0 | 2735 | // Startup cache must be flushed before calling the bootstrap script |
michael@0 | 2736 | flushStartupCache(); |
michael@0 | 2737 | |
michael@0 | 2738 | let installReason = Services.vc.compare(aOldAddon.version, newDBAddon.version) < 0 ? |
michael@0 | 2739 | BOOTSTRAP_REASONS.ADDON_UPGRADE : |
michael@0 | 2740 | BOOTSTRAP_REASONS.ADDON_DOWNGRADE; |
michael@0 | 2741 | |
michael@0 | 2742 | let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); |
michael@0 | 2743 | file.persistentDescriptor = aAddonState.descriptor; |
michael@0 | 2744 | XPIProvider.callBootstrapMethod(newDBAddon.id, newDBAddon.version, |
michael@0 | 2745 | newDBAddon.type, file, "install", |
michael@0 | 2746 | installReason, { oldVersion: aOldAddon.version }); |
michael@0 | 2747 | return false; |
michael@0 | 2748 | } |
michael@0 | 2749 | |
michael@0 | 2750 | return true; |
michael@0 | 2751 | } |
michael@0 | 2752 | |
michael@0 | 2753 | return false; |
michael@0 | 2754 | } |
michael@0 | 2755 | |
michael@0 | 2756 | /** |
michael@0 | 2757 | * Updates an add-on's descriptor for when the add-on has moved in the |
michael@0 | 2758 | * filesystem but hasn't changed in any other way. |
michael@0 | 2759 | * |
michael@0 | 2760 | * @param aInstallLocation |
michael@0 | 2761 | * The install location containing the add-on |
michael@0 | 2762 | * @param aOldAddon |
michael@0 | 2763 | * The AddonInternal as it appeared the last time the application |
michael@0 | 2764 | * ran |
michael@0 | 2765 | * @param aAddonState |
michael@0 | 2766 | * The new state of the add-on |
michael@0 | 2767 | * @return a boolean indicating if flushing caches is required to complete |
michael@0 | 2768 | * changing this add-on |
michael@0 | 2769 | */ |
michael@0 | 2770 | function updateDescriptor(aInstallLocation, aOldAddon, aAddonState) { |
michael@0 | 2771 | logger.debug("Add-on " + aOldAddon.id + " moved to " + aAddonState.descriptor); |
michael@0 | 2772 | |
michael@0 | 2773 | aOldAddon.descriptor = aAddonState.descriptor; |
michael@0 | 2774 | aOldAddon.visible = !(aOldAddon.id in visibleAddons); |
michael@0 | 2775 | XPIDatabase.saveChanges(); |
michael@0 | 2776 | |
michael@0 | 2777 | if (aOldAddon.visible) { |
michael@0 | 2778 | visibleAddons[aOldAddon.id] = aOldAddon; |
michael@0 | 2779 | |
michael@0 | 2780 | if (aOldAddon.bootstrap && aOldAddon.active) { |
michael@0 | 2781 | let bootstrap = oldBootstrappedAddons[aOldAddon.id]; |
michael@0 | 2782 | bootstrap.descriptor = aAddonState.descriptor; |
michael@0 | 2783 | XPIProvider.bootstrappedAddons[aOldAddon.id] = bootstrap; |
michael@0 | 2784 | } |
michael@0 | 2785 | |
michael@0 | 2786 | return true; |
michael@0 | 2787 | } |
michael@0 | 2788 | |
michael@0 | 2789 | return false; |
michael@0 | 2790 | } |
michael@0 | 2791 | |
michael@0 | 2792 | /** |
michael@0 | 2793 | * Called when no change has been detected for an add-on's metadata. The |
michael@0 | 2794 | * add-on may have become visible due to other add-ons being removed or |
michael@0 | 2795 | * the add-on may need to be updated when the application version has |
michael@0 | 2796 | * changed. |
michael@0 | 2797 | * |
michael@0 | 2798 | * @param aInstallLocation |
michael@0 | 2799 | * The install location containing the add-on |
michael@0 | 2800 | * @param aOldAddon |
michael@0 | 2801 | * The AddonInternal as it appeared the last time the application |
michael@0 | 2802 | * ran |
michael@0 | 2803 | * @param aAddonState |
michael@0 | 2804 | * The new state of the add-on |
michael@0 | 2805 | * @return a boolean indicating if flushing caches is required to complete |
michael@0 | 2806 | * changing this add-on |
michael@0 | 2807 | */ |
michael@0 | 2808 | function updateVisibilityAndCompatibility(aInstallLocation, aOldAddon, |
michael@0 | 2809 | aAddonState) { |
michael@0 | 2810 | let changed = false; |
michael@0 | 2811 | |
michael@0 | 2812 | // This add-ons metadata has not changed but it may have become visible |
michael@0 | 2813 | if (!(aOldAddon.id in visibleAddons)) { |
michael@0 | 2814 | visibleAddons[aOldAddon.id] = aOldAddon; |
michael@0 | 2815 | |
michael@0 | 2816 | if (!aOldAddon.visible) { |
michael@0 | 2817 | // Remember add-ons that were changed during startup. |
michael@0 | 2818 | AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED, |
michael@0 | 2819 | aOldAddon.id); |
michael@0 | 2820 | XPIDatabase.makeAddonVisible(aOldAddon); |
michael@0 | 2821 | |
michael@0 | 2822 | if (aOldAddon.bootstrap) { |
michael@0 | 2823 | // The add-on is bootstrappable so call its install script |
michael@0 | 2824 | let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); |
michael@0 | 2825 | file.persistentDescriptor = aAddonState.descriptor; |
michael@0 | 2826 | XPIProvider.callBootstrapMethod(aOldAddon.id, aOldAddon.version, aOldAddon.type, file, |
michael@0 | 2827 | "install", |
michael@0 | 2828 | BOOTSTRAP_REASONS.ADDON_INSTALL); |
michael@0 | 2829 | |
michael@0 | 2830 | // If it should be active then mark it as active otherwise unload |
michael@0 | 2831 | // its scope |
michael@0 | 2832 | if (!isAddonDisabled(aOldAddon)) { |
michael@0 | 2833 | XPIDatabase.updateAddonActive(aOldAddon, true); |
michael@0 | 2834 | } |
michael@0 | 2835 | else { |
michael@0 | 2836 | XPIProvider.unloadBootstrapScope(newAddon.id); |
michael@0 | 2837 | } |
michael@0 | 2838 | } |
michael@0 | 2839 | else { |
michael@0 | 2840 | // Otherwise a restart is necessary |
michael@0 | 2841 | changed = true; |
michael@0 | 2842 | } |
michael@0 | 2843 | } |
michael@0 | 2844 | } |
michael@0 | 2845 | |
michael@0 | 2846 | // App version changed, we may need to update the appDisabled property. |
michael@0 | 2847 | if (aUpdateCompatibility) { |
michael@0 | 2848 | let wasDisabled = isAddonDisabled(aOldAddon); |
michael@0 | 2849 | let wasAppDisabled = aOldAddon.appDisabled; |
michael@0 | 2850 | let wasUserDisabled = aOldAddon.userDisabled; |
michael@0 | 2851 | let wasSoftDisabled = aOldAddon.softDisabled; |
michael@0 | 2852 | |
michael@0 | 2853 | // This updates the addon's JSON cached data in place |
michael@0 | 2854 | applyBlocklistChanges(aOldAddon, aOldAddon, aOldAppVersion, |
michael@0 | 2855 | aOldPlatformVersion); |
michael@0 | 2856 | aOldAddon.appDisabled = !isUsableAddon(aOldAddon); |
michael@0 | 2857 | |
michael@0 | 2858 | let isDisabled = isAddonDisabled(aOldAddon); |
michael@0 | 2859 | |
michael@0 | 2860 | // If either property has changed update the database. |
michael@0 | 2861 | if (wasAppDisabled != aOldAddon.appDisabled || |
michael@0 | 2862 | wasUserDisabled != aOldAddon.userDisabled || |
michael@0 | 2863 | wasSoftDisabled != aOldAddon.softDisabled) { |
michael@0 | 2864 | logger.debug("Add-on " + aOldAddon.id + " changed appDisabled state to " + |
michael@0 | 2865 | aOldAddon.appDisabled + ", userDisabled state to " + |
michael@0 | 2866 | aOldAddon.userDisabled + " and softDisabled state to " + |
michael@0 | 2867 | aOldAddon.softDisabled); |
michael@0 | 2868 | XPIDatabase.saveChanges(); |
michael@0 | 2869 | } |
michael@0 | 2870 | |
michael@0 | 2871 | // If this is a visible add-on and it has changed disabled state then we |
michael@0 | 2872 | // may need a restart or to update the bootstrap list. |
michael@0 | 2873 | if (aOldAddon.visible && wasDisabled != isDisabled) { |
michael@0 | 2874 | // Remember add-ons that became disabled or enabled by the application |
michael@0 | 2875 | // change |
michael@0 | 2876 | let change = isDisabled ? AddonManager.STARTUP_CHANGE_DISABLED |
michael@0 | 2877 | : AddonManager.STARTUP_CHANGE_ENABLED; |
michael@0 | 2878 | AddonManagerPrivate.addStartupChange(change, aOldAddon.id); |
michael@0 | 2879 | if (aOldAddon.bootstrap) { |
michael@0 | 2880 | // Update the add-ons active state |
michael@0 | 2881 | XPIDatabase.updateAddonActive(aOldAddon, !isDisabled); |
michael@0 | 2882 | } |
michael@0 | 2883 | else { |
michael@0 | 2884 | changed = true; |
michael@0 | 2885 | } |
michael@0 | 2886 | } |
michael@0 | 2887 | } |
michael@0 | 2888 | |
michael@0 | 2889 | if (aOldAddon.visible && aOldAddon.active && aOldAddon.bootstrap) { |
michael@0 | 2890 | XPIProvider.bootstrappedAddons[aOldAddon.id] = { |
michael@0 | 2891 | version: aOldAddon.version, |
michael@0 | 2892 | type: aOldAddon.type, |
michael@0 | 2893 | descriptor: aAddonState.descriptor |
michael@0 | 2894 | }; |
michael@0 | 2895 | } |
michael@0 | 2896 | |
michael@0 | 2897 | return changed; |
michael@0 | 2898 | } |
michael@0 | 2899 | |
michael@0 | 2900 | /** |
michael@0 | 2901 | * Called when an add-on has been removed. |
michael@0 | 2902 | * |
michael@0 | 2903 | * @param aOldAddon |
michael@0 | 2904 | * The AddonInternal as it appeared the last time the application |
michael@0 | 2905 | * ran |
michael@0 | 2906 | * @return a boolean indicating if flushing caches is required to complete |
michael@0 | 2907 | * changing this add-on |
michael@0 | 2908 | */ |
michael@0 | 2909 | function removeMetadata(aOldAddon) { |
michael@0 | 2910 | // This add-on has disappeared |
michael@0 | 2911 | logger.debug("Add-on " + aOldAddon.id + " removed from " + aOldAddon.location); |
michael@0 | 2912 | XPIDatabase.removeAddonMetadata(aOldAddon); |
michael@0 | 2913 | |
michael@0 | 2914 | // Remember add-ons that were uninstalled during startup |
michael@0 | 2915 | if (aOldAddon.visible) { |
michael@0 | 2916 | AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_UNINSTALLED, |
michael@0 | 2917 | aOldAddon.id); |
michael@0 | 2918 | } |
michael@0 | 2919 | else if (AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_INSTALLED) |
michael@0 | 2920 | .indexOf(aOldAddon.id) != -1) { |
michael@0 | 2921 | AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED, |
michael@0 | 2922 | aOldAddon.id); |
michael@0 | 2923 | } |
michael@0 | 2924 | |
michael@0 | 2925 | if (aOldAddon.active) { |
michael@0 | 2926 | // Enable the default theme if the previously active theme has been |
michael@0 | 2927 | // removed |
michael@0 | 2928 | if (aOldAddon.type == "theme") |
michael@0 | 2929 | XPIProvider.enableDefaultTheme(); |
michael@0 | 2930 | |
michael@0 | 2931 | return true; |
michael@0 | 2932 | } |
michael@0 | 2933 | |
michael@0 | 2934 | return false; |
michael@0 | 2935 | } |
michael@0 | 2936 | |
michael@0 | 2937 | /** |
michael@0 | 2938 | * Called to add the metadata for an add-on in one of the install locations |
michael@0 | 2939 | * to the database. This can be called in three different cases. Either an |
michael@0 | 2940 | * add-on has been dropped into the location from outside of Firefox, or |
michael@0 | 2941 | * an add-on has been installed through the application, or the database |
michael@0 | 2942 | * has been upgraded or become corrupt and add-on data has to be reloaded |
michael@0 | 2943 | * into it. |
michael@0 | 2944 | * |
michael@0 | 2945 | * @param aInstallLocation |
michael@0 | 2946 | * The install location containing the add-on |
michael@0 | 2947 | * @param aId |
michael@0 | 2948 | * The ID of the add-on |
michael@0 | 2949 | * @param aAddonState |
michael@0 | 2950 | * The new state of the add-on |
michael@0 | 2951 | * @param aMigrateData |
michael@0 | 2952 | * If during startup the database had to be upgraded this will |
michael@0 | 2953 | * contain data that used to be held about this add-on |
michael@0 | 2954 | * @return a boolean indicating if flushing caches is required to complete |
michael@0 | 2955 | * changing this add-on |
michael@0 | 2956 | */ |
michael@0 | 2957 | function addMetadata(aInstallLocation, aId, aAddonState, aMigrateData) { |
michael@0 | 2958 | logger.debug("New add-on " + aId + " installed in " + aInstallLocation.name); |
michael@0 | 2959 | |
michael@0 | 2960 | let newAddon = null; |
michael@0 | 2961 | let sameVersion = false; |
michael@0 | 2962 | // Check the updated manifests lists for the install location, If there |
michael@0 | 2963 | // is no manifest for the add-on ID then newAddon will be undefined |
michael@0 | 2964 | if (aInstallLocation.name in aManifests) |
michael@0 | 2965 | newAddon = aManifests[aInstallLocation.name][aId]; |
michael@0 | 2966 | |
michael@0 | 2967 | // If we had staged data for this add-on or we aren't recovering from a |
michael@0 | 2968 | // corrupt database and we don't have migration data for this add-on then |
michael@0 | 2969 | // this must be a new install. |
michael@0 | 2970 | let isNewInstall = (!!newAddon) || (!XPIDatabase.activeBundles && !aMigrateData); |
michael@0 | 2971 | |
michael@0 | 2972 | // If it's a new install and we haven't yet loaded the manifest then it |
michael@0 | 2973 | // must be something dropped directly into the install location |
michael@0 | 2974 | let isDetectedInstall = isNewInstall && !newAddon; |
michael@0 | 2975 | |
michael@0 | 2976 | // Load the manifest if necessary and sanity check the add-on ID |
michael@0 | 2977 | try { |
michael@0 | 2978 | if (!newAddon) { |
michael@0 | 2979 | // Load the manifest from the add-on. |
michael@0 | 2980 | let file = aInstallLocation.getLocationForID(aId); |
michael@0 | 2981 | newAddon = loadManifestFromFile(file); |
michael@0 | 2982 | } |
michael@0 | 2983 | // The add-on in the manifest should match the add-on ID. |
michael@0 | 2984 | if (newAddon.id != aId) { |
michael@0 | 2985 | throw new Error("Invalid addon ID: expected addon ID " + aId + |
michael@0 | 2986 | ", found " + newAddon.id + " in manifest"); |
michael@0 | 2987 | } |
michael@0 | 2988 | } |
michael@0 | 2989 | catch (e) { |
michael@0 | 2990 | logger.warn("Add-on is invalid", e); |
michael@0 | 2991 | |
michael@0 | 2992 | // Remove the invalid add-on from the install location if the install |
michael@0 | 2993 | // location isn't locked, no restart will be necessary |
michael@0 | 2994 | if (!aInstallLocation.locked) |
michael@0 | 2995 | aInstallLocation.uninstallAddon(aId); |
michael@0 | 2996 | else |
michael@0 | 2997 | logger.warn("Could not uninstall invalid item from locked install location"); |
michael@0 | 2998 | return false; |
michael@0 | 2999 | } |
michael@0 | 3000 | |
michael@0 | 3001 | // Update the AddonInternal properties. |
michael@0 | 3002 | newAddon._installLocation = aInstallLocation; |
michael@0 | 3003 | newAddon.visible = !(newAddon.id in visibleAddons); |
michael@0 | 3004 | newAddon.installDate = aAddonState.mtime; |
michael@0 | 3005 | newAddon.updateDate = aAddonState.mtime; |
michael@0 | 3006 | newAddon.foreignInstall = isDetectedInstall; |
michael@0 | 3007 | |
michael@0 | 3008 | if (aMigrateData) { |
michael@0 | 3009 | // If there is migration data then apply it. |
michael@0 | 3010 | logger.debug("Migrating data from old database"); |
michael@0 | 3011 | |
michael@0 | 3012 | DB_MIGRATE_METADATA.forEach(function(aProp) { |
michael@0 | 3013 | // A theme's disabled state is determined by the selected theme |
michael@0 | 3014 | // preference which is read in loadManifestFromRDF |
michael@0 | 3015 | if (aProp == "userDisabled" && newAddon.type == "theme") |
michael@0 | 3016 | return; |
michael@0 | 3017 | |
michael@0 | 3018 | if (aProp in aMigrateData) |
michael@0 | 3019 | newAddon[aProp] = aMigrateData[aProp]; |
michael@0 | 3020 | }); |
michael@0 | 3021 | |
michael@0 | 3022 | // Force all non-profile add-ons to be foreignInstalls since they can't |
michael@0 | 3023 | // have been installed through the API |
michael@0 | 3024 | newAddon.foreignInstall |= aInstallLocation.name != KEY_APP_PROFILE; |
michael@0 | 3025 | |
michael@0 | 3026 | // Some properties should only be migrated if the add-on hasn't changed. |
michael@0 | 3027 | // The version property isn't a perfect check for this but covers the |
michael@0 | 3028 | // vast majority of cases. |
michael@0 | 3029 | if (aMigrateData.version == newAddon.version) { |
michael@0 | 3030 | logger.debug("Migrating compatibility info"); |
michael@0 | 3031 | sameVersion = true; |
michael@0 | 3032 | if ("targetApplications" in aMigrateData) |
michael@0 | 3033 | newAddon.applyCompatibilityUpdate(aMigrateData, true); |
michael@0 | 3034 | } |
michael@0 | 3035 | |
michael@0 | 3036 | // Since the DB schema has changed make sure softDisabled is correct |
michael@0 | 3037 | applyBlocklistChanges(newAddon, newAddon, aOldAppVersion, |
michael@0 | 3038 | aOldPlatformVersion); |
michael@0 | 3039 | } |
michael@0 | 3040 | |
michael@0 | 3041 | // The default theme is never a foreign install |
michael@0 | 3042 | if (newAddon.type == "theme" && newAddon.internalName == XPIProvider.defaultSkin) |
michael@0 | 3043 | newAddon.foreignInstall = false; |
michael@0 | 3044 | |
michael@0 | 3045 | if (isDetectedInstall && newAddon.foreignInstall) { |
michael@0 | 3046 | // If the add-on is a foreign install and is in a scope where add-ons |
michael@0 | 3047 | // that were dropped in should default to disabled then disable it |
michael@0 | 3048 | let disablingScopes = Prefs.getIntPref(PREF_EM_AUTO_DISABLED_SCOPES, 0); |
michael@0 | 3049 | if (aInstallLocation.scope & disablingScopes) |
michael@0 | 3050 | newAddon.userDisabled = true; |
michael@0 | 3051 | } |
michael@0 | 3052 | |
michael@0 | 3053 | // If we have a list of what add-ons should be marked as active then use |
michael@0 | 3054 | // it to guess at migration data. |
michael@0 | 3055 | if (!isNewInstall && XPIDatabase.activeBundles) { |
michael@0 | 3056 | // For themes we know which is active by the current skin setting |
michael@0 | 3057 | if (newAddon.type == "theme") |
michael@0 | 3058 | newAddon.active = newAddon.internalName == XPIProvider.currentSkin; |
michael@0 | 3059 | else |
michael@0 | 3060 | newAddon.active = XPIDatabase.activeBundles.indexOf(aAddonState.descriptor) != -1; |
michael@0 | 3061 | |
michael@0 | 3062 | // If the add-on wasn't active and it isn't already disabled in some way |
michael@0 | 3063 | // then it was probably either softDisabled or userDisabled |
michael@0 | 3064 | if (!newAddon.active && newAddon.visible && !isAddonDisabled(newAddon)) { |
michael@0 | 3065 | // If the add-on is softblocked then assume it is softDisabled |
michael@0 | 3066 | if (newAddon.blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED) |
michael@0 | 3067 | newAddon.softDisabled = true; |
michael@0 | 3068 | else |
michael@0 | 3069 | newAddon.userDisabled = true; |
michael@0 | 3070 | } |
michael@0 | 3071 | } |
michael@0 | 3072 | else { |
michael@0 | 3073 | newAddon.active = (newAddon.visible && !isAddonDisabled(newAddon)) |
michael@0 | 3074 | } |
michael@0 | 3075 | |
michael@0 | 3076 | let newDBAddon = XPIDatabase.addAddonMetadata(newAddon, aAddonState.descriptor); |
michael@0 | 3077 | |
michael@0 | 3078 | if (newDBAddon.visible) { |
michael@0 | 3079 | // Remember add-ons that were first detected during startup. |
michael@0 | 3080 | if (isDetectedInstall) { |
michael@0 | 3081 | // If a copy from a higher priority location was removed then this |
michael@0 | 3082 | // add-on has changed |
michael@0 | 3083 | if (AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_UNINSTALLED) |
michael@0 | 3084 | .indexOf(newDBAddon.id) != -1) { |
michael@0 | 3085 | AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED, |
michael@0 | 3086 | newDBAddon.id); |
michael@0 | 3087 | } |
michael@0 | 3088 | else { |
michael@0 | 3089 | AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_INSTALLED, |
michael@0 | 3090 | newDBAddon.id); |
michael@0 | 3091 | } |
michael@0 | 3092 | } |
michael@0 | 3093 | |
michael@0 | 3094 | // Note if any visible add-on is not in the application install location |
michael@0 | 3095 | if (newDBAddon._installLocation.name != KEY_APP_GLOBAL) |
michael@0 | 3096 | XPIProvider.allAppGlobal = false; |
michael@0 | 3097 | |
michael@0 | 3098 | visibleAddons[newDBAddon.id] = newDBAddon; |
michael@0 | 3099 | |
michael@0 | 3100 | let installReason = BOOTSTRAP_REASONS.ADDON_INSTALL; |
michael@0 | 3101 | let extraParams = {}; |
michael@0 | 3102 | |
michael@0 | 3103 | // If we're hiding a bootstrapped add-on then call its uninstall method |
michael@0 | 3104 | if (newDBAddon.id in oldBootstrappedAddons) { |
michael@0 | 3105 | let oldBootstrap = oldBootstrappedAddons[newDBAddon.id]; |
michael@0 | 3106 | extraParams.oldVersion = oldBootstrap.version; |
michael@0 | 3107 | XPIProvider.bootstrappedAddons[newDBAddon.id] = oldBootstrap; |
michael@0 | 3108 | |
michael@0 | 3109 | // If the old version is the same as the new version, or we're |
michael@0 | 3110 | // recovering from a corrupt DB, don't call uninstall and install |
michael@0 | 3111 | // methods. |
michael@0 | 3112 | if (sameVersion || !isNewInstall) |
michael@0 | 3113 | return false; |
michael@0 | 3114 | |
michael@0 | 3115 | installReason = Services.vc.compare(oldBootstrap.version, newDBAddon.version) < 0 ? |
michael@0 | 3116 | BOOTSTRAP_REASONS.ADDON_UPGRADE : |
michael@0 | 3117 | BOOTSTRAP_REASONS.ADDON_DOWNGRADE; |
michael@0 | 3118 | |
michael@0 | 3119 | let oldAddonFile = Cc["@mozilla.org/file/local;1"]. |
michael@0 | 3120 | createInstance(Ci.nsIFile); |
michael@0 | 3121 | oldAddonFile.persistentDescriptor = oldBootstrap.descriptor; |
michael@0 | 3122 | |
michael@0 | 3123 | XPIProvider.callBootstrapMethod(newDBAddon.id, oldBootstrap.version, |
michael@0 | 3124 | oldBootstrap.type, oldAddonFile, "uninstall", |
michael@0 | 3125 | installReason, { newVersion: newDBAddon.version }); |
michael@0 | 3126 | XPIProvider.unloadBootstrapScope(newDBAddon.id); |
michael@0 | 3127 | |
michael@0 | 3128 | // If the new add-on is bootstrapped then we must flush the caches |
michael@0 | 3129 | // before calling the new bootstrap script |
michael@0 | 3130 | if (newDBAddon.bootstrap) |
michael@0 | 3131 | flushStartupCache(); |
michael@0 | 3132 | } |
michael@0 | 3133 | |
michael@0 | 3134 | if (!newDBAddon.bootstrap) |
michael@0 | 3135 | return true; |
michael@0 | 3136 | |
michael@0 | 3137 | // Visible bootstrapped add-ons need to have their install method called |
michael@0 | 3138 | let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); |
michael@0 | 3139 | file.persistentDescriptor = aAddonState.descriptor; |
michael@0 | 3140 | XPIProvider.callBootstrapMethod(newDBAddon.id, newDBAddon.version, newDBAddon.type, file, |
michael@0 | 3141 | "install", installReason, extraParams); |
michael@0 | 3142 | if (!newDBAddon.active) |
michael@0 | 3143 | XPIProvider.unloadBootstrapScope(newDBAddon.id); |
michael@0 | 3144 | } |
michael@0 | 3145 | |
michael@0 | 3146 | return false; |
michael@0 | 3147 | } |
michael@0 | 3148 | |
michael@0 | 3149 | let changed = false; |
michael@0 | 3150 | let knownLocations = XPIDatabase.getInstallLocations(); |
michael@0 | 3151 | |
michael@0 | 3152 | // The install locations are iterated in reverse order of priority so when |
michael@0 | 3153 | // there are multiple add-ons installed with the same ID the one that |
michael@0 | 3154 | // should be visible is the first one encountered. |
michael@0 | 3155 | for (let aSt of aState.reverse()) { |
michael@0 | 3156 | |
michael@0 | 3157 | // We can't include the install location directly in the state as it has |
michael@0 | 3158 | // to be cached as JSON. |
michael@0 | 3159 | let installLocation = this.installLocationsByName[aSt.name]; |
michael@0 | 3160 | let addonStates = aSt.addons; |
michael@0 | 3161 | |
michael@0 | 3162 | // Check if the database knows about any add-ons in this install location. |
michael@0 | 3163 | if (knownLocations.has(installLocation.name)) { |
michael@0 | 3164 | knownLocations.delete(installLocation.name); |
michael@0 | 3165 | let addons = XPIDatabase.getAddonsInLocation(installLocation.name); |
michael@0 | 3166 | // Iterate through the add-ons installed the last time the application |
michael@0 | 3167 | // ran |
michael@0 | 3168 | for (let aOldAddon of addons) { |
michael@0 | 3169 | // If a version of this add-on has been installed in an higher |
michael@0 | 3170 | // priority install location then count it as changed |
michael@0 | 3171 | if (AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_INSTALLED) |
michael@0 | 3172 | .indexOf(aOldAddon.id) != -1) { |
michael@0 | 3173 | AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED, |
michael@0 | 3174 | aOldAddon.id); |
michael@0 | 3175 | } |
michael@0 | 3176 | |
michael@0 | 3177 | // Check if the add-on is still installed |
michael@0 | 3178 | if (aOldAddon.id in addonStates) { |
michael@0 | 3179 | let addonState = addonStates[aOldAddon.id]; |
michael@0 | 3180 | delete addonStates[aOldAddon.id]; |
michael@0 | 3181 | |
michael@0 | 3182 | // Remember add-ons that were inactive during startup |
michael@0 | 3183 | if (aOldAddon.visible && !aOldAddon.active) |
michael@0 | 3184 | XPIProvider.inactiveAddonIDs.push(aOldAddon.id); |
michael@0 | 3185 | |
michael@0 | 3186 | // record a bit more per-addon telemetry |
michael@0 | 3187 | let loc = aOldAddon.defaultLocale; |
michael@0 | 3188 | if (loc) { |
michael@0 | 3189 | XPIProvider.setTelemetry(aOldAddon.id, "name", loc.name); |
michael@0 | 3190 | XPIProvider.setTelemetry(aOldAddon.id, "creator", loc.creator); |
michael@0 | 3191 | } |
michael@0 | 3192 | |
michael@0 | 3193 | // Check if the add-on has been changed outside the XPI provider |
michael@0 | 3194 | if (aOldAddon.updateDate != addonState.mtime) { |
michael@0 | 3195 | // Did time change in the wrong direction? |
michael@0 | 3196 | if (addonState.mtime < aOldAddon.updateDate) { |
michael@0 | 3197 | this.setTelemetry(aOldAddon.id, "olderFile", { |
michael@0 | 3198 | name: this._mostRecentlyModifiedFile[aOldAddon.id], |
michael@0 | 3199 | mtime: addonState.mtime, |
michael@0 | 3200 | oldtime: aOldAddon.updateDate |
michael@0 | 3201 | }); |
michael@0 | 3202 | } |
michael@0 | 3203 | // Is the add-on unpacked? |
michael@0 | 3204 | else if (addonState.rdfTime) { |
michael@0 | 3205 | // Was the addon manifest "install.rdf" modified, or some other file? |
michael@0 | 3206 | if (addonState.rdfTime > aOldAddon.updateDate) { |
michael@0 | 3207 | this.setTelemetry(aOldAddon.id, "modifiedInstallRDF", 1); |
michael@0 | 3208 | } |
michael@0 | 3209 | else { |
michael@0 | 3210 | this.setTelemetry(aOldAddon.id, "modifiedFile", |
michael@0 | 3211 | this._mostRecentlyModifiedFile[aOldAddon.id]); |
michael@0 | 3212 | } |
michael@0 | 3213 | } |
michael@0 | 3214 | else { |
michael@0 | 3215 | this.setTelemetry(aOldAddon.id, "modifiedXPI", 1); |
michael@0 | 3216 | } |
michael@0 | 3217 | } |
michael@0 | 3218 | |
michael@0 | 3219 | // The add-on has changed if the modification time has changed, or |
michael@0 | 3220 | // we have an updated manifest for it. Also reload the metadata for |
michael@0 | 3221 | // add-ons in the application directory when the application version |
michael@0 | 3222 | // has changed |
michael@0 | 3223 | if (aOldAddon.id in aManifests[installLocation.name] || |
michael@0 | 3224 | aOldAddon.updateDate != addonState.mtime || |
michael@0 | 3225 | (aUpdateCompatibility && installLocation.name == KEY_APP_GLOBAL)) { |
michael@0 | 3226 | changed = updateMetadata(installLocation, aOldAddon, addonState) || |
michael@0 | 3227 | changed; |
michael@0 | 3228 | } |
michael@0 | 3229 | else if (aOldAddon.descriptor != addonState.descriptor) { |
michael@0 | 3230 | changed = updateDescriptor(installLocation, aOldAddon, addonState) || |
michael@0 | 3231 | changed; |
michael@0 | 3232 | } |
michael@0 | 3233 | else { |
michael@0 | 3234 | changed = updateVisibilityAndCompatibility(installLocation, |
michael@0 | 3235 | aOldAddon, addonState) || |
michael@0 | 3236 | changed; |
michael@0 | 3237 | } |
michael@0 | 3238 | if (aOldAddon.visible && aOldAddon._installLocation.name != KEY_APP_GLOBAL) |
michael@0 | 3239 | XPIProvider.allAppGlobal = false; |
michael@0 | 3240 | } |
michael@0 | 3241 | else { |
michael@0 | 3242 | changed = removeMetadata(aOldAddon) || changed; |
michael@0 | 3243 | } |
michael@0 | 3244 | } |
michael@0 | 3245 | } |
michael@0 | 3246 | |
michael@0 | 3247 | // All the remaining add-ons in this install location must be new. |
michael@0 | 3248 | |
michael@0 | 3249 | // Get the migration data for this install location. |
michael@0 | 3250 | let locMigrateData = {}; |
michael@0 | 3251 | if (XPIDatabase.migrateData && installLocation.name in XPIDatabase.migrateData) |
michael@0 | 3252 | locMigrateData = XPIDatabase.migrateData[installLocation.name]; |
michael@0 | 3253 | for (let id in addonStates) { |
michael@0 | 3254 | changed = addMetadata(installLocation, id, addonStates[id], |
michael@0 | 3255 | (locMigrateData[id] || null)) || changed; |
michael@0 | 3256 | } |
michael@0 | 3257 | } |
michael@0 | 3258 | |
michael@0 | 3259 | // The remaining locations that had add-ons installed in them no longer |
michael@0 | 3260 | // have any add-ons installed in them, or the locations no longer exist. |
michael@0 | 3261 | // The metadata for the add-ons that were in them must be removed from the |
michael@0 | 3262 | // database. |
michael@0 | 3263 | for (let location of knownLocations) { |
michael@0 | 3264 | let addons = XPIDatabase.getAddonsInLocation(location); |
michael@0 | 3265 | for (let aOldAddon of addons) { |
michael@0 | 3266 | changed = removeMetadata(aOldAddon) || changed; |
michael@0 | 3267 | } |
michael@0 | 3268 | } |
michael@0 | 3269 | |
michael@0 | 3270 | // Cache the new install location states |
michael@0 | 3271 | this.installStates = this.getInstallLocationStates(); |
michael@0 | 3272 | let cache = JSON.stringify(this.installStates); |
michael@0 | 3273 | Services.prefs.setCharPref(PREF_INSTALL_CACHE, cache); |
michael@0 | 3274 | this.persistBootstrappedAddons(); |
michael@0 | 3275 | |
michael@0 | 3276 | // Clear out any cached migration data. |
michael@0 | 3277 | XPIDatabase.migrateData = null; |
michael@0 | 3278 | |
michael@0 | 3279 | return changed; |
michael@0 | 3280 | }, |
michael@0 | 3281 | |
michael@0 | 3282 | /** |
michael@0 | 3283 | * Imports the xpinstall permissions from preferences into the permissions |
michael@0 | 3284 | * manager for the user to change later. |
michael@0 | 3285 | */ |
michael@0 | 3286 | importPermissions: function XPI_importPermissions() { |
michael@0 | 3287 | PermissionsUtils.importFromPrefs(PREF_XPI_PERMISSIONS_BRANCH, |
michael@0 | 3288 | XPI_PERMISSION); |
michael@0 | 3289 | }, |
michael@0 | 3290 | |
michael@0 | 3291 | /** |
michael@0 | 3292 | * Checks for any changes that have occurred since the last time the |
michael@0 | 3293 | * application was launched. |
michael@0 | 3294 | * |
michael@0 | 3295 | * @param aAppChanged |
michael@0 | 3296 | * A tri-state value. Undefined means the current profile was created |
michael@0 | 3297 | * for this session, true means the profile already existed but was |
michael@0 | 3298 | * last used with an application with a different version number, |
michael@0 | 3299 | * false means that the profile was last used by this version of the |
michael@0 | 3300 | * application. |
michael@0 | 3301 | * @param aOldAppVersion |
michael@0 | 3302 | * The version of the application last run with this profile or null |
michael@0 | 3303 | * if it is a new profile or the version is unknown |
michael@0 | 3304 | * @param aOldPlatformVersion |
michael@0 | 3305 | * The version of the platform last run with this profile or null |
michael@0 | 3306 | * if it is a new profile or the version is unknown |
michael@0 | 3307 | * @return true if a change requiring a restart was detected |
michael@0 | 3308 | */ |
michael@0 | 3309 | checkForChanges: function XPI_checkForChanges(aAppChanged, aOldAppVersion, |
michael@0 | 3310 | aOldPlatformVersion) { |
michael@0 | 3311 | logger.debug("checkForChanges"); |
michael@0 | 3312 | |
michael@0 | 3313 | // Keep track of whether and why we need to open and update the database at |
michael@0 | 3314 | // startup time. |
michael@0 | 3315 | let updateReasons = []; |
michael@0 | 3316 | if (aAppChanged) { |
michael@0 | 3317 | updateReasons.push("appChanged"); |
michael@0 | 3318 | } |
michael@0 | 3319 | |
michael@0 | 3320 | // Load the list of bootstrapped add-ons first so processFileChanges can |
michael@0 | 3321 | // modify it |
michael@0 | 3322 | try { |
michael@0 | 3323 | this.bootstrappedAddons = JSON.parse(Prefs.getCharPref(PREF_BOOTSTRAP_ADDONS, |
michael@0 | 3324 | "{}")); |
michael@0 | 3325 | } catch (e) { |
michael@0 | 3326 | logger.warn("Error parsing enabled bootstrapped extensions cache", e); |
michael@0 | 3327 | } |
michael@0 | 3328 | |
michael@0 | 3329 | // First install any new add-ons into the locations, if there are any |
michael@0 | 3330 | // changes then we must update the database with the information in the |
michael@0 | 3331 | // install locations |
michael@0 | 3332 | let manifests = {}; |
michael@0 | 3333 | let updated = this.processPendingFileChanges(manifests); |
michael@0 | 3334 | if (updated) { |
michael@0 | 3335 | updateReasons.push("pendingFileChanges"); |
michael@0 | 3336 | } |
michael@0 | 3337 | |
michael@0 | 3338 | // This will be true if the previous session made changes that affect the |
michael@0 | 3339 | // active state of add-ons but didn't commit them properly (normally due |
michael@0 | 3340 | // to the application crashing) |
michael@0 | 3341 | let hasPendingChanges = Prefs.getBoolPref(PREF_PENDING_OPERATIONS); |
michael@0 | 3342 | if (hasPendingChanges) { |
michael@0 | 3343 | updateReasons.push("hasPendingChanges"); |
michael@0 | 3344 | } |
michael@0 | 3345 | |
michael@0 | 3346 | // If the application has changed then check for new distribution add-ons |
michael@0 | 3347 | if (aAppChanged !== false && |
michael@0 | 3348 | Prefs.getBoolPref(PREF_INSTALL_DISTRO_ADDONS, true)) |
michael@0 | 3349 | { |
michael@0 | 3350 | updated = this.installDistributionAddons(manifests); |
michael@0 | 3351 | if (updated) { |
michael@0 | 3352 | updateReasons.push("installDistributionAddons"); |
michael@0 | 3353 | } |
michael@0 | 3354 | } |
michael@0 | 3355 | |
michael@0 | 3356 | // Telemetry probe added around getInstallLocationStates() to check perf |
michael@0 | 3357 | let telemetryCaptureTime = Date.now(); |
michael@0 | 3358 | this.installStates = this.getInstallLocationStates(); |
michael@0 | 3359 | let telemetry = Services.telemetry; |
michael@0 | 3360 | telemetry.getHistogramById("CHECK_ADDONS_MODIFIED_MS").add(Date.now() - telemetryCaptureTime); |
michael@0 | 3361 | |
michael@0 | 3362 | // If the install directory state has changed then we must update the database |
michael@0 | 3363 | let cache = Prefs.getCharPref(PREF_INSTALL_CACHE, "[]"); |
michael@0 | 3364 | // For a little while, gather telemetry on whether the deep comparison |
michael@0 | 3365 | // makes a difference |
michael@0 | 3366 | let newState = JSON.stringify(this.installStates); |
michael@0 | 3367 | if (cache != newState) { |
michael@0 | 3368 | logger.debug("Directory state JSON differs: cache " + cache + " state " + newState); |
michael@0 | 3369 | if (directoryStateDiffers(this.installStates, cache)) { |
michael@0 | 3370 | updateReasons.push("directoryState"); |
michael@0 | 3371 | } |
michael@0 | 3372 | else { |
michael@0 | 3373 | AddonManagerPrivate.recordSimpleMeasure("XPIDB_startup_state_badCompare", 1); |
michael@0 | 3374 | } |
michael@0 | 3375 | } |
michael@0 | 3376 | |
michael@0 | 3377 | // If the schema appears to have changed then we should update the database |
michael@0 | 3378 | if (DB_SCHEMA != Prefs.getIntPref(PREF_DB_SCHEMA, 0)) { |
michael@0 | 3379 | // If we don't have any add-ons, just update the pref, since we don't need to |
michael@0 | 3380 | // write the database |
michael@0 | 3381 | if (this.installStates.length == 0) { |
michael@0 | 3382 | logger.debug("Empty XPI database, setting schema version preference to " + DB_SCHEMA); |
michael@0 | 3383 | Services.prefs.setIntPref(PREF_DB_SCHEMA, DB_SCHEMA); |
michael@0 | 3384 | } |
michael@0 | 3385 | else { |
michael@0 | 3386 | updateReasons.push("schemaChanged"); |
michael@0 | 3387 | } |
michael@0 | 3388 | } |
michael@0 | 3389 | |
michael@0 | 3390 | // If the database doesn't exist and there are add-ons installed then we |
michael@0 | 3391 | // must update the database however if there are no add-ons then there is |
michael@0 | 3392 | // no need to update the database. |
michael@0 | 3393 | let dbFile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_DATABASE], true); |
michael@0 | 3394 | if (!dbFile.exists() && this.installStates.length > 0) { |
michael@0 | 3395 | updateReasons.push("needNewDatabase"); |
michael@0 | 3396 | } |
michael@0 | 3397 | |
michael@0 | 3398 | if (updateReasons.length == 0) { |
michael@0 | 3399 | let bootstrapDescriptors = [this.bootstrappedAddons[b].descriptor |
michael@0 | 3400 | for (b in this.bootstrappedAddons)]; |
michael@0 | 3401 | |
michael@0 | 3402 | this.installStates.forEach(function(aInstallLocationState) { |
michael@0 | 3403 | for (let id in aInstallLocationState.addons) { |
michael@0 | 3404 | let pos = bootstrapDescriptors.indexOf(aInstallLocationState.addons[id].descriptor); |
michael@0 | 3405 | if (pos != -1) |
michael@0 | 3406 | bootstrapDescriptors.splice(pos, 1); |
michael@0 | 3407 | } |
michael@0 | 3408 | }); |
michael@0 | 3409 | |
michael@0 | 3410 | if (bootstrapDescriptors.length > 0) { |
michael@0 | 3411 | logger.warn("Bootstrap state is invalid (missing add-ons: " + bootstrapDescriptors.toSource() + ")"); |
michael@0 | 3412 | updateReasons.push("missingBootstrapAddon"); |
michael@0 | 3413 | } |
michael@0 | 3414 | } |
michael@0 | 3415 | |
michael@0 | 3416 | // Catch and log any errors during the main startup |
michael@0 | 3417 | try { |
michael@0 | 3418 | let extensionListChanged = false; |
michael@0 | 3419 | // If the database needs to be updated then open it and then update it |
michael@0 | 3420 | // from the filesystem |
michael@0 | 3421 | if (updateReasons.length > 0) { |
michael@0 | 3422 | AddonManagerPrivate.recordSimpleMeasure("XPIDB_startup_load_reasons", updateReasons); |
michael@0 | 3423 | XPIDatabase.syncLoadDB(false); |
michael@0 | 3424 | try { |
michael@0 | 3425 | extensionListChanged = this.processFileChanges(this.installStates, manifests, |
michael@0 | 3426 | aAppChanged, |
michael@0 | 3427 | aOldAppVersion, |
michael@0 | 3428 | aOldPlatformVersion); |
michael@0 | 3429 | } |
michael@0 | 3430 | catch (e) { |
michael@0 | 3431 | logger.error("Failed to process extension changes at startup", e); |
michael@0 | 3432 | } |
michael@0 | 3433 | } |
michael@0 | 3434 | |
michael@0 | 3435 | if (aAppChanged) { |
michael@0 | 3436 | // When upgrading the app and using a custom skin make sure it is still |
michael@0 | 3437 | // compatible otherwise switch back the default |
michael@0 | 3438 | if (this.currentSkin != this.defaultSkin) { |
michael@0 | 3439 | let oldSkin = XPIDatabase.getVisibleAddonForInternalName(this.currentSkin); |
michael@0 | 3440 | if (!oldSkin || isAddonDisabled(oldSkin)) |
michael@0 | 3441 | this.enableDefaultTheme(); |
michael@0 | 3442 | } |
michael@0 | 3443 | |
michael@0 | 3444 | // When upgrading remove the old extensions cache to force older |
michael@0 | 3445 | // versions to rescan the entire list of extensions |
michael@0 | 3446 | try { |
michael@0 | 3447 | let oldCache = FileUtils.getFile(KEY_PROFILEDIR, [FILE_OLD_CACHE], true); |
michael@0 | 3448 | if (oldCache.exists()) |
michael@0 | 3449 | oldCache.remove(true); |
michael@0 | 3450 | } |
michael@0 | 3451 | catch (e) { |
michael@0 | 3452 | logger.warn("Unable to remove old extension cache " + oldCache.path, e); |
michael@0 | 3453 | } |
michael@0 | 3454 | } |
michael@0 | 3455 | |
michael@0 | 3456 | // If the application crashed before completing any pending operations then |
michael@0 | 3457 | // we should perform them now. |
michael@0 | 3458 | if (extensionListChanged || hasPendingChanges) { |
michael@0 | 3459 | logger.debug("Updating database with changes to installed add-ons"); |
michael@0 | 3460 | XPIDatabase.updateActiveAddons(); |
michael@0 | 3461 | Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, |
michael@0 | 3462 | !XPIDatabase.writeAddonsList()); |
michael@0 | 3463 | Services.prefs.setCharPref(PREF_BOOTSTRAP_ADDONS, |
michael@0 | 3464 | JSON.stringify(this.bootstrappedAddons)); |
michael@0 | 3465 | return true; |
michael@0 | 3466 | } |
michael@0 | 3467 | |
michael@0 | 3468 | logger.debug("No changes found"); |
michael@0 | 3469 | } |
michael@0 | 3470 | catch (e) { |
michael@0 | 3471 | logger.error("Error during startup file checks", e); |
michael@0 | 3472 | } |
michael@0 | 3473 | |
michael@0 | 3474 | // Check that the add-ons list still exists |
michael@0 | 3475 | let addonsList = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST], |
michael@0 | 3476 | true); |
michael@0 | 3477 | if (addonsList.exists() == (this.installStates.length == 0)) { |
michael@0 | 3478 | logger.debug("Add-ons list is invalid, rebuilding"); |
michael@0 | 3479 | XPIDatabase.writeAddonsList(); |
michael@0 | 3480 | } |
michael@0 | 3481 | |
michael@0 | 3482 | return false; |
michael@0 | 3483 | }, |
michael@0 | 3484 | |
michael@0 | 3485 | /** |
michael@0 | 3486 | * Called to test whether this provider supports installing a particular |
michael@0 | 3487 | * mimetype. |
michael@0 | 3488 | * |
michael@0 | 3489 | * @param aMimetype |
michael@0 | 3490 | * The mimetype to check for |
michael@0 | 3491 | * @return true if the mimetype is application/x-xpinstall |
michael@0 | 3492 | */ |
michael@0 | 3493 | supportsMimetype: function XPI_supportsMimetype(aMimetype) { |
michael@0 | 3494 | return aMimetype == "application/x-xpinstall"; |
michael@0 | 3495 | }, |
michael@0 | 3496 | |
michael@0 | 3497 | /** |
michael@0 | 3498 | * Called to test whether installing XPI add-ons is enabled. |
michael@0 | 3499 | * |
michael@0 | 3500 | * @return true if installing is enabled |
michael@0 | 3501 | */ |
michael@0 | 3502 | isInstallEnabled: function XPI_isInstallEnabled() { |
michael@0 | 3503 | // Default to enabled if the preference does not exist |
michael@0 | 3504 | return Prefs.getBoolPref(PREF_XPI_ENABLED, true); |
michael@0 | 3505 | }, |
michael@0 | 3506 | |
michael@0 | 3507 | /** |
michael@0 | 3508 | * Called to test whether installing XPI add-ons by direct URL requests is |
michael@0 | 3509 | * whitelisted. |
michael@0 | 3510 | * |
michael@0 | 3511 | * @return true if installing by direct requests is whitelisted |
michael@0 | 3512 | */ |
michael@0 | 3513 | isDirectRequestWhitelisted: function XPI_isDirectRequestWhitelisted() { |
michael@0 | 3514 | // Default to whitelisted if the preference does not exist. |
michael@0 | 3515 | return Prefs.getBoolPref(PREF_XPI_DIRECT_WHITELISTED, true); |
michael@0 | 3516 | }, |
michael@0 | 3517 | |
michael@0 | 3518 | /** |
michael@0 | 3519 | * Called to test whether installing XPI add-ons from file referrers is |
michael@0 | 3520 | * whitelisted. |
michael@0 | 3521 | * |
michael@0 | 3522 | * @return true if installing from file referrers is whitelisted |
michael@0 | 3523 | */ |
michael@0 | 3524 | isFileRequestWhitelisted: function XPI_isFileRequestWhitelisted() { |
michael@0 | 3525 | // Default to whitelisted if the preference does not exist. |
michael@0 | 3526 | return Prefs.getBoolPref(PREF_XPI_FILE_WHITELISTED, true); |
michael@0 | 3527 | }, |
michael@0 | 3528 | |
michael@0 | 3529 | /** |
michael@0 | 3530 | * Called to test whether installing XPI add-ons from a URI is allowed. |
michael@0 | 3531 | * |
michael@0 | 3532 | * @param aUri |
michael@0 | 3533 | * The URI being installed from |
michael@0 | 3534 | * @return true if installing is allowed |
michael@0 | 3535 | */ |
michael@0 | 3536 | isInstallAllowed: function XPI_isInstallAllowed(aUri) { |
michael@0 | 3537 | if (!this.isInstallEnabled()) |
michael@0 | 3538 | return false; |
michael@0 | 3539 | |
michael@0 | 3540 | // Direct requests without a referrer are either whitelisted or blocked. |
michael@0 | 3541 | if (!aUri) |
michael@0 | 3542 | return this.isDirectRequestWhitelisted(); |
michael@0 | 3543 | |
michael@0 | 3544 | // Local referrers can be whitelisted. |
michael@0 | 3545 | if (this.isFileRequestWhitelisted() && |
michael@0 | 3546 | (aUri.schemeIs("chrome") || aUri.schemeIs("file"))) |
michael@0 | 3547 | return true; |
michael@0 | 3548 | |
michael@0 | 3549 | this.importPermissions(); |
michael@0 | 3550 | |
michael@0 | 3551 | let permission = Services.perms.testPermission(aUri, XPI_PERMISSION); |
michael@0 | 3552 | if (permission == Ci.nsIPermissionManager.DENY_ACTION) |
michael@0 | 3553 | return false; |
michael@0 | 3554 | |
michael@0 | 3555 | let requireWhitelist = Prefs.getBoolPref(PREF_XPI_WHITELIST_REQUIRED, true); |
michael@0 | 3556 | if (requireWhitelist && (permission != Ci.nsIPermissionManager.ALLOW_ACTION)) |
michael@0 | 3557 | return false; |
michael@0 | 3558 | |
michael@0 | 3559 | return true; |
michael@0 | 3560 | }, |
michael@0 | 3561 | |
michael@0 | 3562 | /** |
michael@0 | 3563 | * Called to get an AddonInstall to download and install an add-on from a URL. |
michael@0 | 3564 | * |
michael@0 | 3565 | * @param aUrl |
michael@0 | 3566 | * The URL to be installed |
michael@0 | 3567 | * @param aHash |
michael@0 | 3568 | * A hash for the install |
michael@0 | 3569 | * @param aName |
michael@0 | 3570 | * A name for the install |
michael@0 | 3571 | * @param aIcons |
michael@0 | 3572 | * Icon URLs for the install |
michael@0 | 3573 | * @param aVersion |
michael@0 | 3574 | * A version for the install |
michael@0 | 3575 | * @param aLoadGroup |
michael@0 | 3576 | * An nsILoadGroup to associate requests with |
michael@0 | 3577 | * @param aCallback |
michael@0 | 3578 | * A callback to pass the AddonInstall to |
michael@0 | 3579 | */ |
michael@0 | 3580 | getInstallForURL: function XPI_getInstallForURL(aUrl, aHash, aName, aIcons, |
michael@0 | 3581 | aVersion, aLoadGroup, aCallback) { |
michael@0 | 3582 | AddonInstall.createDownload(function getInstallForURL_createDownload(aInstall) { |
michael@0 | 3583 | aCallback(aInstall.wrapper); |
michael@0 | 3584 | }, aUrl, aHash, aName, aIcons, aVersion, aLoadGroup); |
michael@0 | 3585 | }, |
michael@0 | 3586 | |
michael@0 | 3587 | /** |
michael@0 | 3588 | * Called to get an AddonInstall to install an add-on from a local file. |
michael@0 | 3589 | * |
michael@0 | 3590 | * @param aFile |
michael@0 | 3591 | * The file to be installed |
michael@0 | 3592 | * @param aCallback |
michael@0 | 3593 | * A callback to pass the AddonInstall to |
michael@0 | 3594 | */ |
michael@0 | 3595 | getInstallForFile: function XPI_getInstallForFile(aFile, aCallback) { |
michael@0 | 3596 | AddonInstall.createInstall(function getInstallForFile_createInstall(aInstall) { |
michael@0 | 3597 | if (aInstall) |
michael@0 | 3598 | aCallback(aInstall.wrapper); |
michael@0 | 3599 | else |
michael@0 | 3600 | aCallback(null); |
michael@0 | 3601 | }, aFile); |
michael@0 | 3602 | }, |
michael@0 | 3603 | |
michael@0 | 3604 | /** |
michael@0 | 3605 | * Removes an AddonInstall from the list of active installs. |
michael@0 | 3606 | * |
michael@0 | 3607 | * @param install |
michael@0 | 3608 | * The AddonInstall to remove |
michael@0 | 3609 | */ |
michael@0 | 3610 | removeActiveInstall: function XPI_removeActiveInstall(aInstall) { |
michael@0 | 3611 | this.installs = this.installs.filter(function installFilter(i) i != aInstall); |
michael@0 | 3612 | }, |
michael@0 | 3613 | |
michael@0 | 3614 | /** |
michael@0 | 3615 | * Called to get an Addon with a particular ID. |
michael@0 | 3616 | * |
michael@0 | 3617 | * @param aId |
michael@0 | 3618 | * The ID of the add-on to retrieve |
michael@0 | 3619 | * @param aCallback |
michael@0 | 3620 | * A callback to pass the Addon to |
michael@0 | 3621 | */ |
michael@0 | 3622 | getAddonByID: function XPI_getAddonByID(aId, aCallback) { |
michael@0 | 3623 | XPIDatabase.getVisibleAddonForID (aId, function getAddonByID_getVisibleAddonForID(aAddon) { |
michael@0 | 3624 | aCallback(createWrapper(aAddon)); |
michael@0 | 3625 | }); |
michael@0 | 3626 | }, |
michael@0 | 3627 | |
michael@0 | 3628 | /** |
michael@0 | 3629 | * Called to get Addons of a particular type. |
michael@0 | 3630 | * |
michael@0 | 3631 | * @param aTypes |
michael@0 | 3632 | * An array of types to fetch. Can be null to get all types. |
michael@0 | 3633 | * @param aCallback |
michael@0 | 3634 | * A callback to pass an array of Addons to |
michael@0 | 3635 | */ |
michael@0 | 3636 | getAddonsByTypes: function XPI_getAddonsByTypes(aTypes, aCallback) { |
michael@0 | 3637 | XPIDatabase.getVisibleAddons(aTypes, function getAddonsByTypes_getVisibleAddons(aAddons) { |
michael@0 | 3638 | aCallback([createWrapper(a) for each (a in aAddons)]); |
michael@0 | 3639 | }); |
michael@0 | 3640 | }, |
michael@0 | 3641 | |
michael@0 | 3642 | /** |
michael@0 | 3643 | * Obtain an Addon having the specified Sync GUID. |
michael@0 | 3644 | * |
michael@0 | 3645 | * @param aGUID |
michael@0 | 3646 | * String GUID of add-on to retrieve |
michael@0 | 3647 | * @param aCallback |
michael@0 | 3648 | * A callback to pass the Addon to. Receives null if not found. |
michael@0 | 3649 | */ |
michael@0 | 3650 | getAddonBySyncGUID: function XPI_getAddonBySyncGUID(aGUID, aCallback) { |
michael@0 | 3651 | XPIDatabase.getAddonBySyncGUID(aGUID, function getAddonBySyncGUID_getAddonBySyncGUID(aAddon) { |
michael@0 | 3652 | aCallback(createWrapper(aAddon)); |
michael@0 | 3653 | }); |
michael@0 | 3654 | }, |
michael@0 | 3655 | |
michael@0 | 3656 | /** |
michael@0 | 3657 | * Called to get Addons that have pending operations. |
michael@0 | 3658 | * |
michael@0 | 3659 | * @param aTypes |
michael@0 | 3660 | * An array of types to fetch. Can be null to get all types |
michael@0 | 3661 | * @param aCallback |
michael@0 | 3662 | * A callback to pass an array of Addons to |
michael@0 | 3663 | */ |
michael@0 | 3664 | getAddonsWithOperationsByTypes: |
michael@0 | 3665 | function XPI_getAddonsWithOperationsByTypes(aTypes, aCallback) { |
michael@0 | 3666 | XPIDatabase.getVisibleAddonsWithPendingOperations(aTypes, |
michael@0 | 3667 | function getAddonsWithOpsByTypes_getVisibleAddonsWithPendingOps(aAddons) { |
michael@0 | 3668 | let results = [createWrapper(a) for each (a in aAddons)]; |
michael@0 | 3669 | XPIProvider.installs.forEach(function(aInstall) { |
michael@0 | 3670 | if (aInstall.state == AddonManager.STATE_INSTALLED && |
michael@0 | 3671 | !(aInstall.addon.inDatabase)) |
michael@0 | 3672 | results.push(createWrapper(aInstall.addon)); |
michael@0 | 3673 | }); |
michael@0 | 3674 | aCallback(results); |
michael@0 | 3675 | }); |
michael@0 | 3676 | }, |
michael@0 | 3677 | |
michael@0 | 3678 | /** |
michael@0 | 3679 | * Called to get the current AddonInstalls, optionally limiting to a list of |
michael@0 | 3680 | * types. |
michael@0 | 3681 | * |
michael@0 | 3682 | * @param aTypes |
michael@0 | 3683 | * An array of types or null to get all types |
michael@0 | 3684 | * @param aCallback |
michael@0 | 3685 | * A callback to pass the array of AddonInstalls to |
michael@0 | 3686 | */ |
michael@0 | 3687 | getInstallsByTypes: function XPI_getInstallsByTypes(aTypes, aCallback) { |
michael@0 | 3688 | let results = []; |
michael@0 | 3689 | this.installs.forEach(function(aInstall) { |
michael@0 | 3690 | if (!aTypes || aTypes.indexOf(aInstall.type) >= 0) |
michael@0 | 3691 | results.push(aInstall.wrapper); |
michael@0 | 3692 | }); |
michael@0 | 3693 | aCallback(results); |
michael@0 | 3694 | }, |
michael@0 | 3695 | |
michael@0 | 3696 | /** |
michael@0 | 3697 | * Synchronously map a URI to the corresponding Addon ID. |
michael@0 | 3698 | * |
michael@0 | 3699 | * Mappable URIs are limited to in-application resources belonging to the |
michael@0 | 3700 | * add-on, such as Javascript compartments, XUL windows, XBL bindings, etc. |
michael@0 | 3701 | * but do not include URIs from meta data, such as the add-on homepage. |
michael@0 | 3702 | * |
michael@0 | 3703 | * @param aURI |
michael@0 | 3704 | * nsIURI to map or null |
michael@0 | 3705 | * @return string containing the Addon ID |
michael@0 | 3706 | * @see AddonManager.mapURIToAddonID |
michael@0 | 3707 | * @see amIAddonManager.mapURIToAddonID |
michael@0 | 3708 | */ |
michael@0 | 3709 | mapURIToAddonID: function XPI_mapURIToAddonID(aURI) { |
michael@0 | 3710 | this._ensureURIMappings(); |
michael@0 | 3711 | let resolved = this._resolveURIToFile(aURI); |
michael@0 | 3712 | if (!resolved) { |
michael@0 | 3713 | return null; |
michael@0 | 3714 | } |
michael@0 | 3715 | resolved = resolved.spec; |
michael@0 | 3716 | for (let [id, spec] in Iterator(this._uriMappings)) { |
michael@0 | 3717 | if (resolved.startsWith(spec)) { |
michael@0 | 3718 | return id; |
michael@0 | 3719 | } |
michael@0 | 3720 | } |
michael@0 | 3721 | return null; |
michael@0 | 3722 | }, |
michael@0 | 3723 | |
michael@0 | 3724 | /** |
michael@0 | 3725 | * Called when a new add-on has been enabled when only one add-on of that type |
michael@0 | 3726 | * can be enabled. |
michael@0 | 3727 | * |
michael@0 | 3728 | * @param aId |
michael@0 | 3729 | * The ID of the newly enabled add-on |
michael@0 | 3730 | * @param aType |
michael@0 | 3731 | * The type of the newly enabled add-on |
michael@0 | 3732 | * @param aPendingRestart |
michael@0 | 3733 | * true if the newly enabled add-on will only become enabled after a |
michael@0 | 3734 | * restart |
michael@0 | 3735 | */ |
michael@0 | 3736 | addonChanged: function XPI_addonChanged(aId, aType, aPendingRestart) { |
michael@0 | 3737 | // We only care about themes in this provider |
michael@0 | 3738 | if (aType != "theme") |
michael@0 | 3739 | return; |
michael@0 | 3740 | |
michael@0 | 3741 | if (!aId) { |
michael@0 | 3742 | // Fallback to the default theme when no theme was enabled |
michael@0 | 3743 | this.enableDefaultTheme(); |
michael@0 | 3744 | return; |
michael@0 | 3745 | } |
michael@0 | 3746 | |
michael@0 | 3747 | // Look for the previously enabled theme and find the internalName of the |
michael@0 | 3748 | // currently selected theme |
michael@0 | 3749 | let previousTheme = null; |
michael@0 | 3750 | let newSkin = this.defaultSkin; |
michael@0 | 3751 | let addons = XPIDatabase.getAddonsByType("theme"); |
michael@0 | 3752 | addons.forEach(function(aTheme) { |
michael@0 | 3753 | if (!aTheme.visible) |
michael@0 | 3754 | return; |
michael@0 | 3755 | if (aTheme.id == aId) |
michael@0 | 3756 | newSkin = aTheme.internalName; |
michael@0 | 3757 | else if (aTheme.userDisabled == false && !aTheme.pendingUninstall) |
michael@0 | 3758 | previousTheme = aTheme; |
michael@0 | 3759 | }, this); |
michael@0 | 3760 | |
michael@0 | 3761 | if (aPendingRestart) { |
michael@0 | 3762 | Services.prefs.setBoolPref(PREF_DSS_SWITCHPENDING, true); |
michael@0 | 3763 | Services.prefs.setCharPref(PREF_DSS_SKIN_TO_SELECT, newSkin); |
michael@0 | 3764 | } |
michael@0 | 3765 | else if (newSkin == this.currentSkin) { |
michael@0 | 3766 | try { |
michael@0 | 3767 | Services.prefs.clearUserPref(PREF_DSS_SWITCHPENDING); |
michael@0 | 3768 | } |
michael@0 | 3769 | catch (e) { } |
michael@0 | 3770 | try { |
michael@0 | 3771 | Services.prefs.clearUserPref(PREF_DSS_SKIN_TO_SELECT); |
michael@0 | 3772 | } |
michael@0 | 3773 | catch (e) { } |
michael@0 | 3774 | } |
michael@0 | 3775 | else { |
michael@0 | 3776 | Services.prefs.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN, newSkin); |
michael@0 | 3777 | this.currentSkin = newSkin; |
michael@0 | 3778 | } |
michael@0 | 3779 | this.selectedSkin = newSkin; |
michael@0 | 3780 | |
michael@0 | 3781 | // Flush the preferences to disk so they don't get out of sync with the |
michael@0 | 3782 | // database |
michael@0 | 3783 | Services.prefs.savePrefFile(null); |
michael@0 | 3784 | |
michael@0 | 3785 | // Mark the previous theme as disabled. This won't cause recursion since |
michael@0 | 3786 | // only enabled calls notifyAddonChanged. |
michael@0 | 3787 | if (previousTheme) |
michael@0 | 3788 | this.updateAddonDisabledState(previousTheme, true); |
michael@0 | 3789 | }, |
michael@0 | 3790 | |
michael@0 | 3791 | /** |
michael@0 | 3792 | * Update the appDisabled property for all add-ons. |
michael@0 | 3793 | */ |
michael@0 | 3794 | updateAddonAppDisabledStates: function XPI_updateAddonAppDisabledStates() { |
michael@0 | 3795 | let addons = XPIDatabase.getAddons(); |
michael@0 | 3796 | addons.forEach(function(aAddon) { |
michael@0 | 3797 | this.updateAddonDisabledState(aAddon); |
michael@0 | 3798 | }, this); |
michael@0 | 3799 | }, |
michael@0 | 3800 | |
michael@0 | 3801 | /** |
michael@0 | 3802 | * Update the repositoryAddon property for all add-ons. |
michael@0 | 3803 | * |
michael@0 | 3804 | * @param aCallback |
michael@0 | 3805 | * Function to call when operation is complete. |
michael@0 | 3806 | */ |
michael@0 | 3807 | updateAddonRepositoryData: function XPI_updateAddonRepositoryData(aCallback) { |
michael@0 | 3808 | let self = this; |
michael@0 | 3809 | XPIDatabase.getVisibleAddons(null, function UARD_getVisibleAddonsCallback(aAddons) { |
michael@0 | 3810 | let pending = aAddons.length; |
michael@0 | 3811 | logger.debug("updateAddonRepositoryData found " + pending + " visible add-ons"); |
michael@0 | 3812 | if (pending == 0) { |
michael@0 | 3813 | aCallback(); |
michael@0 | 3814 | return; |
michael@0 | 3815 | } |
michael@0 | 3816 | |
michael@0 | 3817 | function notifyComplete() { |
michael@0 | 3818 | if (--pending == 0) |
michael@0 | 3819 | aCallback(); |
michael@0 | 3820 | } |
michael@0 | 3821 | |
michael@0 | 3822 | for (let addon of aAddons) { |
michael@0 | 3823 | AddonRepository.getCachedAddonByID(addon.id, |
michael@0 | 3824 | function UARD_getCachedAddonCallback(aRepoAddon) { |
michael@0 | 3825 | if (aRepoAddon) { |
michael@0 | 3826 | logger.debug("updateAddonRepositoryData got info for " + addon.id); |
michael@0 | 3827 | addon._repositoryAddon = aRepoAddon; |
michael@0 | 3828 | addon.compatibilityOverrides = aRepoAddon.compatibilityOverrides; |
michael@0 | 3829 | self.updateAddonDisabledState(addon); |
michael@0 | 3830 | } |
michael@0 | 3831 | |
michael@0 | 3832 | notifyComplete(); |
michael@0 | 3833 | }); |
michael@0 | 3834 | }; |
michael@0 | 3835 | }); |
michael@0 | 3836 | }, |
michael@0 | 3837 | |
michael@0 | 3838 | /** |
michael@0 | 3839 | * When the previously selected theme is removed this method will be called |
michael@0 | 3840 | * to enable the default theme. |
michael@0 | 3841 | */ |
michael@0 | 3842 | enableDefaultTheme: function XPI_enableDefaultTheme() { |
michael@0 | 3843 | logger.debug("Activating default theme"); |
michael@0 | 3844 | let addon = XPIDatabase.getVisibleAddonForInternalName(this.defaultSkin); |
michael@0 | 3845 | if (addon) { |
michael@0 | 3846 | if (addon.userDisabled) { |
michael@0 | 3847 | this.updateAddonDisabledState(addon, false); |
michael@0 | 3848 | } |
michael@0 | 3849 | else if (!this.extensionsActive) { |
michael@0 | 3850 | // During startup we may end up trying to enable the default theme when |
michael@0 | 3851 | // the database thinks it is already enabled (see f.e. bug 638847). In |
michael@0 | 3852 | // this case just force the theme preferences to be correct |
michael@0 | 3853 | Services.prefs.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN, |
michael@0 | 3854 | addon.internalName); |
michael@0 | 3855 | this.currentSkin = this.selectedSkin = addon.internalName; |
michael@0 | 3856 | Prefs.clearUserPref(PREF_DSS_SKIN_TO_SELECT); |
michael@0 | 3857 | Prefs.clearUserPref(PREF_DSS_SWITCHPENDING); |
michael@0 | 3858 | } |
michael@0 | 3859 | else { |
michael@0 | 3860 | logger.warn("Attempting to activate an already active default theme"); |
michael@0 | 3861 | } |
michael@0 | 3862 | } |
michael@0 | 3863 | else { |
michael@0 | 3864 | logger.warn("Unable to activate the default theme"); |
michael@0 | 3865 | } |
michael@0 | 3866 | }, |
michael@0 | 3867 | |
michael@0 | 3868 | onDebugConnectionChange: function(aEvent, aWhat, aConnection) { |
michael@0 | 3869 | if (aWhat != "opened") |
michael@0 | 3870 | return; |
michael@0 | 3871 | |
michael@0 | 3872 | for (let id of Object.keys(this.bootstrapScopes)) { |
michael@0 | 3873 | aConnection.setAddonOptions(id, { global: this.bootstrapScopes[id] }); |
michael@0 | 3874 | } |
michael@0 | 3875 | }, |
michael@0 | 3876 | |
michael@0 | 3877 | /** |
michael@0 | 3878 | * Notified when a preference we're interested in has changed. |
michael@0 | 3879 | * |
michael@0 | 3880 | * @see nsIObserver |
michael@0 | 3881 | */ |
michael@0 | 3882 | observe: function XPI_observe(aSubject, aTopic, aData) { |
michael@0 | 3883 | if (aTopic == NOTIFICATION_FLUSH_PERMISSIONS) { |
michael@0 | 3884 | if (!aData || aData == XPI_PERMISSION) { |
michael@0 | 3885 | this.importPermissions(); |
michael@0 | 3886 | } |
michael@0 | 3887 | return; |
michael@0 | 3888 | } |
michael@0 | 3889 | |
michael@0 | 3890 | if (aTopic == "nsPref:changed") { |
michael@0 | 3891 | switch (aData) { |
michael@0 | 3892 | case PREF_EM_MIN_COMPAT_APP_VERSION: |
michael@0 | 3893 | case PREF_EM_MIN_COMPAT_PLATFORM_VERSION: |
michael@0 | 3894 | this.minCompatibleAppVersion = Prefs.getCharPref(PREF_EM_MIN_COMPAT_APP_VERSION, |
michael@0 | 3895 | null); |
michael@0 | 3896 | this.minCompatiblePlatformVersion = Prefs.getCharPref(PREF_EM_MIN_COMPAT_PLATFORM_VERSION, |
michael@0 | 3897 | null); |
michael@0 | 3898 | this.updateAddonAppDisabledStates(); |
michael@0 | 3899 | break; |
michael@0 | 3900 | } |
michael@0 | 3901 | } |
michael@0 | 3902 | }, |
michael@0 | 3903 | |
michael@0 | 3904 | /** |
michael@0 | 3905 | * Tests whether enabling an add-on will require a restart. |
michael@0 | 3906 | * |
michael@0 | 3907 | * @param aAddon |
michael@0 | 3908 | * The add-on to test |
michael@0 | 3909 | * @return true if the operation requires a restart |
michael@0 | 3910 | */ |
michael@0 | 3911 | enableRequiresRestart: function XPI_enableRequiresRestart(aAddon) { |
michael@0 | 3912 | // If the platform couldn't have activated extensions then we can make |
michael@0 | 3913 | // changes without any restart. |
michael@0 | 3914 | if (!this.extensionsActive) |
michael@0 | 3915 | return false; |
michael@0 | 3916 | |
michael@0 | 3917 | // If the application is in safe mode then any change can be made without |
michael@0 | 3918 | // restarting |
michael@0 | 3919 | if (Services.appinfo.inSafeMode) |
michael@0 | 3920 | return false; |
michael@0 | 3921 | |
michael@0 | 3922 | // Anything that is active is already enabled |
michael@0 | 3923 | if (aAddon.active) |
michael@0 | 3924 | return false; |
michael@0 | 3925 | |
michael@0 | 3926 | if (aAddon.type == "theme") { |
michael@0 | 3927 | // If dynamic theme switching is enabled then switching themes does not |
michael@0 | 3928 | // require a restart |
michael@0 | 3929 | if (Prefs.getBoolPref(PREF_EM_DSS_ENABLED)) |
michael@0 | 3930 | return false; |
michael@0 | 3931 | |
michael@0 | 3932 | // If the theme is already the theme in use then no restart is necessary. |
michael@0 | 3933 | // This covers the case where the default theme is in use but a |
michael@0 | 3934 | // lightweight theme is considered active. |
michael@0 | 3935 | return aAddon.internalName != this.currentSkin; |
michael@0 | 3936 | } |
michael@0 | 3937 | |
michael@0 | 3938 | return !aAddon.bootstrap; |
michael@0 | 3939 | }, |
michael@0 | 3940 | |
michael@0 | 3941 | /** |
michael@0 | 3942 | * Tests whether disabling an add-on will require a restart. |
michael@0 | 3943 | * |
michael@0 | 3944 | * @param aAddon |
michael@0 | 3945 | * The add-on to test |
michael@0 | 3946 | * @return true if the operation requires a restart |
michael@0 | 3947 | */ |
michael@0 | 3948 | disableRequiresRestart: function XPI_disableRequiresRestart(aAddon) { |
michael@0 | 3949 | // If the platform couldn't have activated up extensions then we can make |
michael@0 | 3950 | // changes without any restart. |
michael@0 | 3951 | if (!this.extensionsActive) |
michael@0 | 3952 | return false; |
michael@0 | 3953 | |
michael@0 | 3954 | // If the application is in safe mode then any change can be made without |
michael@0 | 3955 | // restarting |
michael@0 | 3956 | if (Services.appinfo.inSafeMode) |
michael@0 | 3957 | return false; |
michael@0 | 3958 | |
michael@0 | 3959 | // Anything that isn't active is already disabled |
michael@0 | 3960 | if (!aAddon.active) |
michael@0 | 3961 | return false; |
michael@0 | 3962 | |
michael@0 | 3963 | if (aAddon.type == "theme") { |
michael@0 | 3964 | // If dynamic theme switching is enabled then switching themes does not |
michael@0 | 3965 | // require a restart |
michael@0 | 3966 | if (Prefs.getBoolPref(PREF_EM_DSS_ENABLED)) |
michael@0 | 3967 | return false; |
michael@0 | 3968 | |
michael@0 | 3969 | // Non-default themes always require a restart to disable since it will |
michael@0 | 3970 | // be switching from one theme to another or to the default theme and a |
michael@0 | 3971 | // lightweight theme. |
michael@0 | 3972 | if (aAddon.internalName != this.defaultSkin) |
michael@0 | 3973 | return true; |
michael@0 | 3974 | |
michael@0 | 3975 | // The default theme requires a restart to disable if we are in the |
michael@0 | 3976 | // process of switching to a different theme. Note that this makes the |
michael@0 | 3977 | // disabled flag of operationsRequiringRestart incorrect for the default |
michael@0 | 3978 | // theme (it will be false most of the time). Bug 520124 would be required |
michael@0 | 3979 | // to fix it. For the UI this isn't a problem since we never try to |
michael@0 | 3980 | // disable or uninstall the default theme. |
michael@0 | 3981 | return this.selectedSkin != this.currentSkin; |
michael@0 | 3982 | } |
michael@0 | 3983 | |
michael@0 | 3984 | return !aAddon.bootstrap; |
michael@0 | 3985 | }, |
michael@0 | 3986 | |
michael@0 | 3987 | /** |
michael@0 | 3988 | * Tests whether installing an add-on will require a restart. |
michael@0 | 3989 | * |
michael@0 | 3990 | * @param aAddon |
michael@0 | 3991 | * The add-on to test |
michael@0 | 3992 | * @return true if the operation requires a restart |
michael@0 | 3993 | */ |
michael@0 | 3994 | installRequiresRestart: function XPI_installRequiresRestart(aAddon) { |
michael@0 | 3995 | // If the platform couldn't have activated up extensions then we can make |
michael@0 | 3996 | // changes without any restart. |
michael@0 | 3997 | if (!this.extensionsActive) |
michael@0 | 3998 | return false; |
michael@0 | 3999 | |
michael@0 | 4000 | // If the application is in safe mode then any change can be made without |
michael@0 | 4001 | // restarting |
michael@0 | 4002 | if (Services.appinfo.inSafeMode) |
michael@0 | 4003 | return false; |
michael@0 | 4004 | |
michael@0 | 4005 | // Add-ons that are already installed don't require a restart to install. |
michael@0 | 4006 | // This wouldn't normally be called for an already installed add-on (except |
michael@0 | 4007 | // for forming the operationsRequiringRestart flags) so is really here as |
michael@0 | 4008 | // a safety measure. |
michael@0 | 4009 | if (aAddon.inDatabase) |
michael@0 | 4010 | return false; |
michael@0 | 4011 | |
michael@0 | 4012 | // If we have an AddonInstall for this add-on then we can see if there is |
michael@0 | 4013 | // an existing installed add-on with the same ID |
michael@0 | 4014 | if ("_install" in aAddon && aAddon._install) { |
michael@0 | 4015 | // If there is an existing installed add-on and uninstalling it would |
michael@0 | 4016 | // require a restart then installing the update will also require a |
michael@0 | 4017 | // restart |
michael@0 | 4018 | let existingAddon = aAddon._install.existingAddon; |
michael@0 | 4019 | if (existingAddon && this.uninstallRequiresRestart(existingAddon)) |
michael@0 | 4020 | return true; |
michael@0 | 4021 | } |
michael@0 | 4022 | |
michael@0 | 4023 | // If the add-on is not going to be active after installation then it |
michael@0 | 4024 | // doesn't require a restart to install. |
michael@0 | 4025 | if (isAddonDisabled(aAddon)) |
michael@0 | 4026 | return false; |
michael@0 | 4027 | |
michael@0 | 4028 | // Themes will require a restart (even if dynamic switching is enabled due |
michael@0 | 4029 | // to some caching issues) and non-bootstrapped add-ons will require a |
michael@0 | 4030 | // restart |
michael@0 | 4031 | return aAddon.type == "theme" || !aAddon.bootstrap; |
michael@0 | 4032 | }, |
michael@0 | 4033 | |
michael@0 | 4034 | /** |
michael@0 | 4035 | * Tests whether uninstalling an add-on will require a restart. |
michael@0 | 4036 | * |
michael@0 | 4037 | * @param aAddon |
michael@0 | 4038 | * The add-on to test |
michael@0 | 4039 | * @return true if the operation requires a restart |
michael@0 | 4040 | */ |
michael@0 | 4041 | uninstallRequiresRestart: function XPI_uninstallRequiresRestart(aAddon) { |
michael@0 | 4042 | // If the platform couldn't have activated up extensions then we can make |
michael@0 | 4043 | // changes without any restart. |
michael@0 | 4044 | if (!this.extensionsActive) |
michael@0 | 4045 | return false; |
michael@0 | 4046 | |
michael@0 | 4047 | // If the application is in safe mode then any change can be made without |
michael@0 | 4048 | // restarting |
michael@0 | 4049 | if (Services.appinfo.inSafeMode) |
michael@0 | 4050 | return false; |
michael@0 | 4051 | |
michael@0 | 4052 | // If the add-on can be disabled without a restart then it can also be |
michael@0 | 4053 | // uninstalled without a restart |
michael@0 | 4054 | return this.disableRequiresRestart(aAddon); |
michael@0 | 4055 | }, |
michael@0 | 4056 | |
michael@0 | 4057 | /** |
michael@0 | 4058 | * Loads a bootstrapped add-on's bootstrap.js into a sandbox and the reason |
michael@0 | 4059 | * values as constants in the scope. This will also add information about the |
michael@0 | 4060 | * add-on to the bootstrappedAddons dictionary and notify the crash reporter |
michael@0 | 4061 | * that new add-ons have been loaded. |
michael@0 | 4062 | * |
michael@0 | 4063 | * @param aId |
michael@0 | 4064 | * The add-on's ID |
michael@0 | 4065 | * @param aFile |
michael@0 | 4066 | * The nsIFile for the add-on |
michael@0 | 4067 | * @param aVersion |
michael@0 | 4068 | * The add-on's version |
michael@0 | 4069 | * @param aType |
michael@0 | 4070 | * The type for the add-on |
michael@0 | 4071 | * @return a JavaScript scope |
michael@0 | 4072 | */ |
michael@0 | 4073 | loadBootstrapScope: function XPI_loadBootstrapScope(aId, aFile, aVersion, aType) { |
michael@0 | 4074 | // Mark the add-on as active for the crash reporter before loading |
michael@0 | 4075 | this.bootstrappedAddons[aId] = { |
michael@0 | 4076 | version: aVersion, |
michael@0 | 4077 | type: aType, |
michael@0 | 4078 | descriptor: aFile.persistentDescriptor |
michael@0 | 4079 | }; |
michael@0 | 4080 | this.persistBootstrappedAddons(); |
michael@0 | 4081 | this.addAddonsToCrashReporter(); |
michael@0 | 4082 | |
michael@0 | 4083 | // Locales only contain chrome and can't have bootstrap scripts |
michael@0 | 4084 | if (aType == "locale") { |
michael@0 | 4085 | this.bootstrapScopes[aId] = null; |
michael@0 | 4086 | return; |
michael@0 | 4087 | } |
michael@0 | 4088 | |
michael@0 | 4089 | logger.debug("Loading bootstrap scope from " + aFile.path); |
michael@0 | 4090 | |
michael@0 | 4091 | let principal = Cc["@mozilla.org/systemprincipal;1"]. |
michael@0 | 4092 | createInstance(Ci.nsIPrincipal); |
michael@0 | 4093 | |
michael@0 | 4094 | if (!aFile.exists()) { |
michael@0 | 4095 | this.bootstrapScopes[aId] = |
michael@0 | 4096 | new Cu.Sandbox(principal, { sandboxName: aFile.path, |
michael@0 | 4097 | wantGlobalProperties: ["indexedDB"], |
michael@0 | 4098 | metadata: { addonID: aId } }); |
michael@0 | 4099 | logger.error("Attempted to load bootstrap scope from missing directory " + aFile.path); |
michael@0 | 4100 | return; |
michael@0 | 4101 | } |
michael@0 | 4102 | |
michael@0 | 4103 | let uri = getURIForResourceInFile(aFile, "bootstrap.js").spec; |
michael@0 | 4104 | if (aType == "dictionary") |
michael@0 | 4105 | uri = "resource://gre/modules/addons/SpellCheckDictionaryBootstrap.js" |
michael@0 | 4106 | |
michael@0 | 4107 | this.bootstrapScopes[aId] = |
michael@0 | 4108 | new Cu.Sandbox(principal, { sandboxName: uri, |
michael@0 | 4109 | wantGlobalProperties: ["indexedDB"], |
michael@0 | 4110 | metadata: { addonID: aId, URI: uri } }); |
michael@0 | 4111 | |
michael@0 | 4112 | let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]. |
michael@0 | 4113 | createInstance(Ci.mozIJSSubScriptLoader); |
michael@0 | 4114 | |
michael@0 | 4115 | // Add a mapping for XPIProvider.mapURIToAddonID |
michael@0 | 4116 | this._addURIMapping(aId, aFile); |
michael@0 | 4117 | |
michael@0 | 4118 | try { |
michael@0 | 4119 | // Copy the reason values from the global object into the bootstrap scope. |
michael@0 | 4120 | for (let name in BOOTSTRAP_REASONS) |
michael@0 | 4121 | this.bootstrapScopes[aId][name] = BOOTSTRAP_REASONS[name]; |
michael@0 | 4122 | |
michael@0 | 4123 | // Add other stuff that extensions want. |
michael@0 | 4124 | const features = [ "Worker", "ChromeWorker" ]; |
michael@0 | 4125 | |
michael@0 | 4126 | for (let feature of features) |
michael@0 | 4127 | this.bootstrapScopes[aId][feature] = gGlobalScope[feature]; |
michael@0 | 4128 | |
michael@0 | 4129 | // As we don't want our caller to control the JS version used for the |
michael@0 | 4130 | // bootstrap file, we run loadSubScript within the context of the |
michael@0 | 4131 | // sandbox with the latest JS version set explicitly. |
michael@0 | 4132 | this.bootstrapScopes[aId].__SCRIPT_URI_SPEC__ = uri; |
michael@0 | 4133 | Components.utils.evalInSandbox( |
michael@0 | 4134 | "Components.classes['@mozilla.org/moz/jssubscript-loader;1'] \ |
michael@0 | 4135 | .createInstance(Components.interfaces.mozIJSSubScriptLoader) \ |
michael@0 | 4136 | .loadSubScript(__SCRIPT_URI_SPEC__);", this.bootstrapScopes[aId], "ECMAv5"); |
michael@0 | 4137 | } |
michael@0 | 4138 | catch (e) { |
michael@0 | 4139 | logger.warn("Error loading bootstrap.js for " + aId, e); |
michael@0 | 4140 | } |
michael@0 | 4141 | |
michael@0 | 4142 | try { |
michael@0 | 4143 | BrowserToolboxProcess.setAddonOptions(aId, { global: this.bootstrapScopes[aId] }); |
michael@0 | 4144 | } |
michael@0 | 4145 | catch (e) { |
michael@0 | 4146 | // BrowserToolboxProcess is not available in all applications |
michael@0 | 4147 | } |
michael@0 | 4148 | }, |
michael@0 | 4149 | |
michael@0 | 4150 | /** |
michael@0 | 4151 | * Unloads a bootstrap scope by dropping all references to it and then |
michael@0 | 4152 | * updating the list of active add-ons with the crash reporter. |
michael@0 | 4153 | * |
michael@0 | 4154 | * @param aId |
michael@0 | 4155 | * The add-on's ID |
michael@0 | 4156 | */ |
michael@0 | 4157 | unloadBootstrapScope: function XPI_unloadBootstrapScope(aId) { |
michael@0 | 4158 | delete this.bootstrapScopes[aId]; |
michael@0 | 4159 | delete this.bootstrappedAddons[aId]; |
michael@0 | 4160 | this.persistBootstrappedAddons(); |
michael@0 | 4161 | this.addAddonsToCrashReporter(); |
michael@0 | 4162 | |
michael@0 | 4163 | try { |
michael@0 | 4164 | BrowserToolboxProcess.setAddonOptions(aId, { global: null }); |
michael@0 | 4165 | } |
michael@0 | 4166 | catch (e) { |
michael@0 | 4167 | // BrowserToolboxProcess is not available in all applications |
michael@0 | 4168 | } |
michael@0 | 4169 | }, |
michael@0 | 4170 | |
michael@0 | 4171 | /** |
michael@0 | 4172 | * Calls a bootstrap method for an add-on. |
michael@0 | 4173 | * |
michael@0 | 4174 | * @param aId |
michael@0 | 4175 | * The ID of the add-on |
michael@0 | 4176 | * @param aVersion |
michael@0 | 4177 | * The version of the add-on |
michael@0 | 4178 | * @param aType |
michael@0 | 4179 | * The type for the add-on |
michael@0 | 4180 | * @param aFile |
michael@0 | 4181 | * The nsIFile for the add-on |
michael@0 | 4182 | * @param aMethod |
michael@0 | 4183 | * The name of the bootstrap method to call |
michael@0 | 4184 | * @param aReason |
michael@0 | 4185 | * The reason flag to pass to the bootstrap's startup method |
michael@0 | 4186 | * @param aExtraParams |
michael@0 | 4187 | * An object of additional key/value pairs to pass to the method in |
michael@0 | 4188 | * the params argument |
michael@0 | 4189 | */ |
michael@0 | 4190 | callBootstrapMethod: function XPI_callBootstrapMethod(aId, aVersion, aType, aFile, |
michael@0 | 4191 | aMethod, aReason, aExtraParams) { |
michael@0 | 4192 | // Never call any bootstrap methods in safe mode |
michael@0 | 4193 | if (Services.appinfo.inSafeMode) |
michael@0 | 4194 | return; |
michael@0 | 4195 | |
michael@0 | 4196 | let timeStart = new Date(); |
michael@0 | 4197 | if (aMethod == "startup") { |
michael@0 | 4198 | logger.debug("Registering manifest for " + aFile.path); |
michael@0 | 4199 | Components.manager.addBootstrappedManifestLocation(aFile); |
michael@0 | 4200 | } |
michael@0 | 4201 | |
michael@0 | 4202 | try { |
michael@0 | 4203 | // Load the scope if it hasn't already been loaded |
michael@0 | 4204 | if (!(aId in this.bootstrapScopes)) |
michael@0 | 4205 | this.loadBootstrapScope(aId, aFile, aVersion, aType); |
michael@0 | 4206 | |
michael@0 | 4207 | // Nothing to call for locales |
michael@0 | 4208 | if (aType == "locale") |
michael@0 | 4209 | return; |
michael@0 | 4210 | |
michael@0 | 4211 | if (!(aMethod in this.bootstrapScopes[aId])) { |
michael@0 | 4212 | logger.warn("Add-on " + aId + " is missing bootstrap method " + aMethod); |
michael@0 | 4213 | return; |
michael@0 | 4214 | } |
michael@0 | 4215 | |
michael@0 | 4216 | let params = { |
michael@0 | 4217 | id: aId, |
michael@0 | 4218 | version: aVersion, |
michael@0 | 4219 | installPath: aFile.clone(), |
michael@0 | 4220 | resourceURI: getURIForResourceInFile(aFile, "") |
michael@0 | 4221 | }; |
michael@0 | 4222 | |
michael@0 | 4223 | if (aExtraParams) { |
michael@0 | 4224 | for (let key in aExtraParams) { |
michael@0 | 4225 | params[key] = aExtraParams[key]; |
michael@0 | 4226 | } |
michael@0 | 4227 | } |
michael@0 | 4228 | |
michael@0 | 4229 | logger.debug("Calling bootstrap method " + aMethod + " on " + aId + " version " + |
michael@0 | 4230 | aVersion); |
michael@0 | 4231 | try { |
michael@0 | 4232 | this.bootstrapScopes[aId][aMethod](params, aReason); |
michael@0 | 4233 | } |
michael@0 | 4234 | catch (e) { |
michael@0 | 4235 | logger.warn("Exception running bootstrap method " + aMethod + " on " + aId, e); |
michael@0 | 4236 | } |
michael@0 | 4237 | } |
michael@0 | 4238 | finally { |
michael@0 | 4239 | if (aMethod == "shutdown" && aReason != BOOTSTRAP_REASONS.APP_SHUTDOWN) { |
michael@0 | 4240 | logger.debug("Removing manifest for " + aFile.path); |
michael@0 | 4241 | Components.manager.removeBootstrappedManifestLocation(aFile); |
michael@0 | 4242 | } |
michael@0 | 4243 | this.setTelemetry(aId, aMethod + "_MS", new Date() - timeStart); |
michael@0 | 4244 | } |
michael@0 | 4245 | }, |
michael@0 | 4246 | |
michael@0 | 4247 | /** |
michael@0 | 4248 | * Updates the disabled state for an add-on. Its appDisabled property will be |
michael@0 | 4249 | * calculated and if the add-on is changed the database will be saved and |
michael@0 | 4250 | * appropriate notifications will be sent out to the registered AddonListeners. |
michael@0 | 4251 | * |
michael@0 | 4252 | * @param aAddon |
michael@0 | 4253 | * The DBAddonInternal to update |
michael@0 | 4254 | * @param aUserDisabled |
michael@0 | 4255 | * Value for the userDisabled property. If undefined the value will |
michael@0 | 4256 | * not change |
michael@0 | 4257 | * @param aSoftDisabled |
michael@0 | 4258 | * Value for the softDisabled property. If undefined the value will |
michael@0 | 4259 | * not change. If true this will force userDisabled to be true |
michael@0 | 4260 | * @throws if addon is not a DBAddonInternal |
michael@0 | 4261 | */ |
michael@0 | 4262 | updateAddonDisabledState: function XPI_updateAddonDisabledState(aAddon, |
michael@0 | 4263 | aUserDisabled, |
michael@0 | 4264 | aSoftDisabled) { |
michael@0 | 4265 | if (!(aAddon.inDatabase)) |
michael@0 | 4266 | throw new Error("Can only update addon states for installed addons."); |
michael@0 | 4267 | if (aUserDisabled !== undefined && aSoftDisabled !== undefined) { |
michael@0 | 4268 | throw new Error("Cannot change userDisabled and softDisabled at the " + |
michael@0 | 4269 | "same time"); |
michael@0 | 4270 | } |
michael@0 | 4271 | |
michael@0 | 4272 | if (aUserDisabled === undefined) { |
michael@0 | 4273 | aUserDisabled = aAddon.userDisabled; |
michael@0 | 4274 | } |
michael@0 | 4275 | else if (!aUserDisabled) { |
michael@0 | 4276 | // If enabling the add-on then remove softDisabled |
michael@0 | 4277 | aSoftDisabled = false; |
michael@0 | 4278 | } |
michael@0 | 4279 | |
michael@0 | 4280 | // If not changing softDisabled or the add-on is already userDisabled then |
michael@0 | 4281 | // use the existing value for softDisabled |
michael@0 | 4282 | if (aSoftDisabled === undefined || aUserDisabled) |
michael@0 | 4283 | aSoftDisabled = aAddon.softDisabled; |
michael@0 | 4284 | |
michael@0 | 4285 | let appDisabled = !isUsableAddon(aAddon); |
michael@0 | 4286 | // No change means nothing to do here |
michael@0 | 4287 | if (aAddon.userDisabled == aUserDisabled && |
michael@0 | 4288 | aAddon.appDisabled == appDisabled && |
michael@0 | 4289 | aAddon.softDisabled == aSoftDisabled) |
michael@0 | 4290 | return; |
michael@0 | 4291 | |
michael@0 | 4292 | let wasDisabled = isAddonDisabled(aAddon); |
michael@0 | 4293 | let isDisabled = aUserDisabled || aSoftDisabled || appDisabled; |
michael@0 | 4294 | |
michael@0 | 4295 | // If appDisabled changes but the result of isAddonDisabled() doesn't, |
michael@0 | 4296 | // no onDisabling/onEnabling is sent - so send a onPropertyChanged. |
michael@0 | 4297 | let appDisabledChanged = aAddon.appDisabled != appDisabled; |
michael@0 | 4298 | |
michael@0 | 4299 | // Update the properties in the database. |
michael@0 | 4300 | // We never persist this for experiments because the disabled flags |
michael@0 | 4301 | // are controlled by the Experiments Manager. |
michael@0 | 4302 | if (aAddon.type != "experiment") { |
michael@0 | 4303 | XPIDatabase.setAddonProperties(aAddon, { |
michael@0 | 4304 | userDisabled: aUserDisabled, |
michael@0 | 4305 | appDisabled: appDisabled, |
michael@0 | 4306 | softDisabled: aSoftDisabled |
michael@0 | 4307 | }); |
michael@0 | 4308 | } |
michael@0 | 4309 | |
michael@0 | 4310 | if (appDisabledChanged) { |
michael@0 | 4311 | AddonManagerPrivate.callAddonListeners("onPropertyChanged", |
michael@0 | 4312 | aAddon, |
michael@0 | 4313 | ["appDisabled"]); |
michael@0 | 4314 | } |
michael@0 | 4315 | |
michael@0 | 4316 | // If the add-on is not visible or the add-on is not changing state then |
michael@0 | 4317 | // there is no need to do anything else |
michael@0 | 4318 | if (!aAddon.visible || (wasDisabled == isDisabled)) |
michael@0 | 4319 | return; |
michael@0 | 4320 | |
michael@0 | 4321 | // Flag that active states in the database need to be updated on shutdown |
michael@0 | 4322 | Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true); |
michael@0 | 4323 | |
michael@0 | 4324 | let wrapper = createWrapper(aAddon); |
michael@0 | 4325 | // Have we just gone back to the current state? |
michael@0 | 4326 | if (isDisabled != aAddon.active) { |
michael@0 | 4327 | AddonManagerPrivate.callAddonListeners("onOperationCancelled", wrapper); |
michael@0 | 4328 | } |
michael@0 | 4329 | else { |
michael@0 | 4330 | if (isDisabled) { |
michael@0 | 4331 | var needsRestart = this.disableRequiresRestart(aAddon); |
michael@0 | 4332 | AddonManagerPrivate.callAddonListeners("onDisabling", wrapper, |
michael@0 | 4333 | needsRestart); |
michael@0 | 4334 | } |
michael@0 | 4335 | else { |
michael@0 | 4336 | needsRestart = this.enableRequiresRestart(aAddon); |
michael@0 | 4337 | AddonManagerPrivate.callAddonListeners("onEnabling", wrapper, |
michael@0 | 4338 | needsRestart); |
michael@0 | 4339 | } |
michael@0 | 4340 | |
michael@0 | 4341 | if (!needsRestart) { |
michael@0 | 4342 | XPIDatabase.updateAddonActive(aAddon, !isDisabled); |
michael@0 | 4343 | if (isDisabled) { |
michael@0 | 4344 | if (aAddon.bootstrap) { |
michael@0 | 4345 | let file = aAddon._installLocation.getLocationForID(aAddon.id); |
michael@0 | 4346 | this.callBootstrapMethod(aAddon.id, aAddon.version, aAddon.type, file, "shutdown", |
michael@0 | 4347 | BOOTSTRAP_REASONS.ADDON_DISABLE); |
michael@0 | 4348 | this.unloadBootstrapScope(aAddon.id); |
michael@0 | 4349 | } |
michael@0 | 4350 | AddonManagerPrivate.callAddonListeners("onDisabled", wrapper); |
michael@0 | 4351 | } |
michael@0 | 4352 | else { |
michael@0 | 4353 | if (aAddon.bootstrap) { |
michael@0 | 4354 | let file = aAddon._installLocation.getLocationForID(aAddon.id); |
michael@0 | 4355 | this.callBootstrapMethod(aAddon.id, aAddon.version, aAddon.type, file, "startup", |
michael@0 | 4356 | BOOTSTRAP_REASONS.ADDON_ENABLE); |
michael@0 | 4357 | } |
michael@0 | 4358 | AddonManagerPrivate.callAddonListeners("onEnabled", wrapper); |
michael@0 | 4359 | } |
michael@0 | 4360 | } |
michael@0 | 4361 | } |
michael@0 | 4362 | |
michael@0 | 4363 | // Notify any other providers that a new theme has been enabled |
michael@0 | 4364 | if (aAddon.type == "theme" && !isDisabled) |
michael@0 | 4365 | AddonManagerPrivate.notifyAddonChanged(aAddon.id, aAddon.type, needsRestart); |
michael@0 | 4366 | }, |
michael@0 | 4367 | |
michael@0 | 4368 | /** |
michael@0 | 4369 | * Uninstalls an add-on, immediately if possible or marks it as pending |
michael@0 | 4370 | * uninstall if not. |
michael@0 | 4371 | * |
michael@0 | 4372 | * @param aAddon |
michael@0 | 4373 | * The DBAddonInternal to uninstall |
michael@0 | 4374 | * @throws if the addon cannot be uninstalled because it is in an install |
michael@0 | 4375 | * location that does not allow it |
michael@0 | 4376 | */ |
michael@0 | 4377 | uninstallAddon: function XPI_uninstallAddon(aAddon) { |
michael@0 | 4378 | if (!(aAddon.inDatabase)) |
michael@0 | 4379 | throw new Error("Can only uninstall installed addons."); |
michael@0 | 4380 | |
michael@0 | 4381 | if (aAddon._installLocation.locked) |
michael@0 | 4382 | throw new Error("Cannot uninstall addons from locked install locations"); |
michael@0 | 4383 | |
michael@0 | 4384 | if ("_hasResourceCache" in aAddon) |
michael@0 | 4385 | aAddon._hasResourceCache = new Map(); |
michael@0 | 4386 | |
michael@0 | 4387 | if (aAddon._updateCheck) { |
michael@0 | 4388 | logger.debug("Cancel in-progress update check for " + aAddon.id); |
michael@0 | 4389 | aAddon._updateCheck.cancel(); |
michael@0 | 4390 | } |
michael@0 | 4391 | |
michael@0 | 4392 | // Inactive add-ons don't require a restart to uninstall |
michael@0 | 4393 | let requiresRestart = this.uninstallRequiresRestart(aAddon); |
michael@0 | 4394 | |
michael@0 | 4395 | if (requiresRestart) { |
michael@0 | 4396 | // We create an empty directory in the staging directory to indicate that |
michael@0 | 4397 | // an uninstall is necessary on next startup. |
michael@0 | 4398 | let stage = aAddon._installLocation.getStagingDir(); |
michael@0 | 4399 | stage.append(aAddon.id); |
michael@0 | 4400 | if (!stage.exists()) |
michael@0 | 4401 | stage.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); |
michael@0 | 4402 | |
michael@0 | 4403 | XPIDatabase.setAddonProperties(aAddon, { |
michael@0 | 4404 | pendingUninstall: true |
michael@0 | 4405 | }); |
michael@0 | 4406 | Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true); |
michael@0 | 4407 | } |
michael@0 | 4408 | |
michael@0 | 4409 | // If the add-on is not visible then there is no need to notify listeners. |
michael@0 | 4410 | if (!aAddon.visible) |
michael@0 | 4411 | return; |
michael@0 | 4412 | |
michael@0 | 4413 | let wrapper = createWrapper(aAddon); |
michael@0 | 4414 | AddonManagerPrivate.callAddonListeners("onUninstalling", wrapper, |
michael@0 | 4415 | requiresRestart); |
michael@0 | 4416 | |
michael@0 | 4417 | // Reveal the highest priority add-on with the same ID |
michael@0 | 4418 | function revealAddon(aAddon) { |
michael@0 | 4419 | XPIDatabase.makeAddonVisible(aAddon); |
michael@0 | 4420 | |
michael@0 | 4421 | let wrappedAddon = createWrapper(aAddon); |
michael@0 | 4422 | AddonManagerPrivate.callAddonListeners("onInstalling", wrappedAddon, false); |
michael@0 | 4423 | |
michael@0 | 4424 | if (!isAddonDisabled(aAddon) && !XPIProvider.enableRequiresRestart(aAddon)) { |
michael@0 | 4425 | XPIDatabase.updateAddonActive(aAddon, true); |
michael@0 | 4426 | } |
michael@0 | 4427 | |
michael@0 | 4428 | if (aAddon.bootstrap) { |
michael@0 | 4429 | let file = aAddon._installLocation.getLocationForID(aAddon.id); |
michael@0 | 4430 | XPIProvider.callBootstrapMethod(aAddon.id, aAddon.version, aAddon.type, file, |
michael@0 | 4431 | "install", BOOTSTRAP_REASONS.ADDON_INSTALL); |
michael@0 | 4432 | |
michael@0 | 4433 | if (aAddon.active) { |
michael@0 | 4434 | XPIProvider.callBootstrapMethod(aAddon.id, aAddon.version, aAddon.type, file, |
michael@0 | 4435 | "startup", BOOTSTRAP_REASONS.ADDON_INSTALL); |
michael@0 | 4436 | } |
michael@0 | 4437 | else { |
michael@0 | 4438 | XPIProvider.unloadBootstrapScope(aAddon.id); |
michael@0 | 4439 | } |
michael@0 | 4440 | } |
michael@0 | 4441 | |
michael@0 | 4442 | // We always send onInstalled even if a restart is required to enable |
michael@0 | 4443 | // the revealed add-on |
michael@0 | 4444 | AddonManagerPrivate.callAddonListeners("onInstalled", wrappedAddon); |
michael@0 | 4445 | } |
michael@0 | 4446 | |
michael@0 | 4447 | function checkInstallLocation(aPos) { |
michael@0 | 4448 | if (aPos < 0) |
michael@0 | 4449 | return; |
michael@0 | 4450 | |
michael@0 | 4451 | let location = XPIProvider.installLocations[aPos]; |
michael@0 | 4452 | XPIDatabase.getAddonInLocation(aAddon.id, location.name, |
michael@0 | 4453 | function checkInstallLocation_getAddonInLocation(aNewAddon) { |
michael@0 | 4454 | if (aNewAddon) |
michael@0 | 4455 | revealAddon(aNewAddon); |
michael@0 | 4456 | else |
michael@0 | 4457 | checkInstallLocation(aPos - 1); |
michael@0 | 4458 | }) |
michael@0 | 4459 | } |
michael@0 | 4460 | |
michael@0 | 4461 | if (!requiresRestart) { |
michael@0 | 4462 | if (aAddon.bootstrap) { |
michael@0 | 4463 | let file = aAddon._installLocation.getLocationForID(aAddon.id); |
michael@0 | 4464 | if (aAddon.active) { |
michael@0 | 4465 | this.callBootstrapMethod(aAddon.id, aAddon.version, aAddon.type, file, |
michael@0 | 4466 | "shutdown", |
michael@0 | 4467 | BOOTSTRAP_REASONS.ADDON_UNINSTALL); |
michael@0 | 4468 | } |
michael@0 | 4469 | |
michael@0 | 4470 | this.callBootstrapMethod(aAddon.id, aAddon.version, aAddon.type, file, |
michael@0 | 4471 | "uninstall", |
michael@0 | 4472 | BOOTSTRAP_REASONS.ADDON_UNINSTALL); |
michael@0 | 4473 | this.unloadBootstrapScope(aAddon.id); |
michael@0 | 4474 | flushStartupCache(); |
michael@0 | 4475 | } |
michael@0 | 4476 | aAddon._installLocation.uninstallAddon(aAddon.id); |
michael@0 | 4477 | XPIDatabase.removeAddonMetadata(aAddon); |
michael@0 | 4478 | AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper); |
michael@0 | 4479 | |
michael@0 | 4480 | checkInstallLocation(this.installLocations.length - 1); |
michael@0 | 4481 | } |
michael@0 | 4482 | |
michael@0 | 4483 | // Notify any other providers that a new theme has been enabled |
michael@0 | 4484 | if (aAddon.type == "theme" && aAddon.active) |
michael@0 | 4485 | AddonManagerPrivate.notifyAddonChanged(null, aAddon.type, requiresRestart); |
michael@0 | 4486 | }, |
michael@0 | 4487 | |
michael@0 | 4488 | /** |
michael@0 | 4489 | * Cancels the pending uninstall of an add-on. |
michael@0 | 4490 | * |
michael@0 | 4491 | * @param aAddon |
michael@0 | 4492 | * The DBAddonInternal to cancel uninstall for |
michael@0 | 4493 | */ |
michael@0 | 4494 | cancelUninstallAddon: function XPI_cancelUninstallAddon(aAddon) { |
michael@0 | 4495 | if (!(aAddon.inDatabase)) |
michael@0 | 4496 | throw new Error("Can only cancel uninstall for installed addons."); |
michael@0 | 4497 | |
michael@0 | 4498 | aAddon._installLocation.cleanStagingDir([aAddon.id]); |
michael@0 | 4499 | |
michael@0 | 4500 | XPIDatabase.setAddonProperties(aAddon, { |
michael@0 | 4501 | pendingUninstall: false |
michael@0 | 4502 | }); |
michael@0 | 4503 | |
michael@0 | 4504 | if (!aAddon.visible) |
michael@0 | 4505 | return; |
michael@0 | 4506 | |
michael@0 | 4507 | Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true); |
michael@0 | 4508 | |
michael@0 | 4509 | // TODO hide hidden add-ons (bug 557710) |
michael@0 | 4510 | let wrapper = createWrapper(aAddon); |
michael@0 | 4511 | AddonManagerPrivate.callAddonListeners("onOperationCancelled", wrapper); |
michael@0 | 4512 | |
michael@0 | 4513 | // Notify any other providers that this theme is now enabled again. |
michael@0 | 4514 | if (aAddon.type == "theme" && aAddon.active) |
michael@0 | 4515 | AddonManagerPrivate.notifyAddonChanged(aAddon.id, aAddon.type, false); |
michael@0 | 4516 | } |
michael@0 | 4517 | }; |
michael@0 | 4518 | |
michael@0 | 4519 | function getHashStringForCrypto(aCrypto) { |
michael@0 | 4520 | // return the two-digit hexadecimal code for a byte |
michael@0 | 4521 | function toHexString(charCode) |
michael@0 | 4522 | ("0" + charCode.toString(16)).slice(-2); |
michael@0 | 4523 | |
michael@0 | 4524 | // convert the binary hash data to a hex string. |
michael@0 | 4525 | let binary = aCrypto.finish(false); |
michael@0 | 4526 | return [toHexString(binary.charCodeAt(i)) for (i in binary)].join("").toLowerCase() |
michael@0 | 4527 | } |
michael@0 | 4528 | |
michael@0 | 4529 | /** |
michael@0 | 4530 | * Instantiates an AddonInstall. |
michael@0 | 4531 | * |
michael@0 | 4532 | * @param aInstallLocation |
michael@0 | 4533 | * The install location the add-on will be installed into |
michael@0 | 4534 | * @param aUrl |
michael@0 | 4535 | * The nsIURL to get the add-on from. If this is an nsIFileURL then |
michael@0 | 4536 | * the add-on will not need to be downloaded |
michael@0 | 4537 | * @param aHash |
michael@0 | 4538 | * An optional hash for the add-on |
michael@0 | 4539 | * @param aReleaseNotesURI |
michael@0 | 4540 | * An optional nsIURI of release notes for the add-on |
michael@0 | 4541 | * @param aExistingAddon |
michael@0 | 4542 | * The add-on this install will update if known |
michael@0 | 4543 | * @param aLoadGroup |
michael@0 | 4544 | * The nsILoadGroup to associate any requests with |
michael@0 | 4545 | * @throws if the url is the url of a local file and the hash does not match |
michael@0 | 4546 | * or the add-on does not contain an valid install manifest |
michael@0 | 4547 | */ |
michael@0 | 4548 | function AddonInstall(aInstallLocation, aUrl, aHash, aReleaseNotesURI, |
michael@0 | 4549 | aExistingAddon, aLoadGroup) { |
michael@0 | 4550 | this.wrapper = new AddonInstallWrapper(this); |
michael@0 | 4551 | this.installLocation = aInstallLocation; |
michael@0 | 4552 | this.sourceURI = aUrl; |
michael@0 | 4553 | this.releaseNotesURI = aReleaseNotesURI; |
michael@0 | 4554 | if (aHash) { |
michael@0 | 4555 | let hashSplit = aHash.toLowerCase().split(":"); |
michael@0 | 4556 | this.originalHash = { |
michael@0 | 4557 | algorithm: hashSplit[0], |
michael@0 | 4558 | data: hashSplit[1] |
michael@0 | 4559 | }; |
michael@0 | 4560 | } |
michael@0 | 4561 | this.hash = this.originalHash; |
michael@0 | 4562 | this.loadGroup = aLoadGroup; |
michael@0 | 4563 | this.listeners = []; |
michael@0 | 4564 | this.icons = {}; |
michael@0 | 4565 | this.existingAddon = aExistingAddon; |
michael@0 | 4566 | this.error = 0; |
michael@0 | 4567 | if (aLoadGroup) |
michael@0 | 4568 | this.window = aLoadGroup.notificationCallbacks |
michael@0 | 4569 | .getInterface(Ci.nsIDOMWindow); |
michael@0 | 4570 | else |
michael@0 | 4571 | this.window = null; |
michael@0 | 4572 | |
michael@0 | 4573 | // Giving each instance of AddonInstall a reference to the logger. |
michael@0 | 4574 | this.logger = logger; |
michael@0 | 4575 | } |
michael@0 | 4576 | |
michael@0 | 4577 | AddonInstall.prototype = { |
michael@0 | 4578 | installLocation: null, |
michael@0 | 4579 | wrapper: null, |
michael@0 | 4580 | stream: null, |
michael@0 | 4581 | crypto: null, |
michael@0 | 4582 | originalHash: null, |
michael@0 | 4583 | hash: null, |
michael@0 | 4584 | loadGroup: null, |
michael@0 | 4585 | badCertHandler: null, |
michael@0 | 4586 | listeners: null, |
michael@0 | 4587 | restartDownload: false, |
michael@0 | 4588 | |
michael@0 | 4589 | name: null, |
michael@0 | 4590 | type: null, |
michael@0 | 4591 | version: null, |
michael@0 | 4592 | icons: null, |
michael@0 | 4593 | releaseNotesURI: null, |
michael@0 | 4594 | sourceURI: null, |
michael@0 | 4595 | file: null, |
michael@0 | 4596 | ownsTempFile: false, |
michael@0 | 4597 | certificate: null, |
michael@0 | 4598 | certName: null, |
michael@0 | 4599 | |
michael@0 | 4600 | linkedInstalls: null, |
michael@0 | 4601 | existingAddon: null, |
michael@0 | 4602 | addon: null, |
michael@0 | 4603 | |
michael@0 | 4604 | state: null, |
michael@0 | 4605 | error: null, |
michael@0 | 4606 | progress: null, |
michael@0 | 4607 | maxProgress: null, |
michael@0 | 4608 | |
michael@0 | 4609 | /** |
michael@0 | 4610 | * Initialises this install to be a staged install waiting to be applied |
michael@0 | 4611 | * |
michael@0 | 4612 | * @param aManifest |
michael@0 | 4613 | * The cached manifest for the staged install |
michael@0 | 4614 | */ |
michael@0 | 4615 | initStagedInstall: function AI_initStagedInstall(aManifest) { |
michael@0 | 4616 | this.name = aManifest.name; |
michael@0 | 4617 | this.type = aManifest.type; |
michael@0 | 4618 | this.version = aManifest.version; |
michael@0 | 4619 | this.icons = aManifest.icons; |
michael@0 | 4620 | this.releaseNotesURI = aManifest.releaseNotesURI ? |
michael@0 | 4621 | NetUtil.newURI(aManifest.releaseNotesURI) : |
michael@0 | 4622 | null |
michael@0 | 4623 | this.sourceURI = aManifest.sourceURI ? |
michael@0 | 4624 | NetUtil.newURI(aManifest.sourceURI) : |
michael@0 | 4625 | null; |
michael@0 | 4626 | this.file = null; |
michael@0 | 4627 | this.addon = aManifest; |
michael@0 | 4628 | |
michael@0 | 4629 | this.state = AddonManager.STATE_INSTALLED; |
michael@0 | 4630 | |
michael@0 | 4631 | XPIProvider.installs.push(this); |
michael@0 | 4632 | }, |
michael@0 | 4633 | |
michael@0 | 4634 | /** |
michael@0 | 4635 | * Initialises this install to be an install from a local file. |
michael@0 | 4636 | * |
michael@0 | 4637 | * @param aCallback |
michael@0 | 4638 | * The callback to pass the initialised AddonInstall to |
michael@0 | 4639 | */ |
michael@0 | 4640 | initLocalInstall: function AI_initLocalInstall(aCallback) { |
michael@0 | 4641 | aCallback = makeSafe(aCallback); |
michael@0 | 4642 | this.file = this.sourceURI.QueryInterface(Ci.nsIFileURL).file; |
michael@0 | 4643 | |
michael@0 | 4644 | if (!this.file.exists()) { |
michael@0 | 4645 | logger.warn("XPI file " + this.file.path + " does not exist"); |
michael@0 | 4646 | this.state = AddonManager.STATE_DOWNLOAD_FAILED; |
michael@0 | 4647 | this.error = AddonManager.ERROR_NETWORK_FAILURE; |
michael@0 | 4648 | aCallback(this); |
michael@0 | 4649 | return; |
michael@0 | 4650 | } |
michael@0 | 4651 | |
michael@0 | 4652 | this.state = AddonManager.STATE_DOWNLOADED; |
michael@0 | 4653 | this.progress = this.file.fileSize; |
michael@0 | 4654 | this.maxProgress = this.file.fileSize; |
michael@0 | 4655 | |
michael@0 | 4656 | if (this.hash) { |
michael@0 | 4657 | let crypto = Cc["@mozilla.org/security/hash;1"]. |
michael@0 | 4658 | createInstance(Ci.nsICryptoHash); |
michael@0 | 4659 | try { |
michael@0 | 4660 | crypto.initWithString(this.hash.algorithm); |
michael@0 | 4661 | } |
michael@0 | 4662 | catch (e) { |
michael@0 | 4663 | logger.warn("Unknown hash algorithm '" + this.hash.algorithm + "' for addon " + this.sourceURI.spec, e); |
michael@0 | 4664 | this.state = AddonManager.STATE_DOWNLOAD_FAILED; |
michael@0 | 4665 | this.error = AddonManager.ERROR_INCORRECT_HASH; |
michael@0 | 4666 | aCallback(this); |
michael@0 | 4667 | return; |
michael@0 | 4668 | } |
michael@0 | 4669 | |
michael@0 | 4670 | let fis = Cc["@mozilla.org/network/file-input-stream;1"]. |
michael@0 | 4671 | createInstance(Ci.nsIFileInputStream); |
michael@0 | 4672 | fis.init(this.file, -1, -1, false); |
michael@0 | 4673 | crypto.updateFromStream(fis, this.file.fileSize); |
michael@0 | 4674 | let calculatedHash = getHashStringForCrypto(crypto); |
michael@0 | 4675 | if (calculatedHash != this.hash.data) { |
michael@0 | 4676 | logger.warn("File hash (" + calculatedHash + ") did not match provided hash (" + |
michael@0 | 4677 | this.hash.data + ")"); |
michael@0 | 4678 | this.state = AddonManager.STATE_DOWNLOAD_FAILED; |
michael@0 | 4679 | this.error = AddonManager.ERROR_INCORRECT_HASH; |
michael@0 | 4680 | aCallback(this); |
michael@0 | 4681 | return; |
michael@0 | 4682 | } |
michael@0 | 4683 | } |
michael@0 | 4684 | |
michael@0 | 4685 | try { |
michael@0 | 4686 | let self = this; |
michael@0 | 4687 | this.loadManifest(function initLocalInstall_loadManifest() { |
michael@0 | 4688 | XPIDatabase.getVisibleAddonForID(self.addon.id, function initLocalInstall_getVisibleAddon(aAddon) { |
michael@0 | 4689 | self.existingAddon = aAddon; |
michael@0 | 4690 | if (aAddon) |
michael@0 | 4691 | applyBlocklistChanges(aAddon, self.addon); |
michael@0 | 4692 | self.addon.updateDate = Date.now(); |
michael@0 | 4693 | self.addon.installDate = aAddon ? aAddon.installDate : self.addon.updateDate; |
michael@0 | 4694 | |
michael@0 | 4695 | if (!self.addon.isCompatible) { |
michael@0 | 4696 | // TODO Should we send some event here? |
michael@0 | 4697 | self.state = AddonManager.STATE_CHECKING; |
michael@0 | 4698 | new UpdateChecker(self.addon, { |
michael@0 | 4699 | onUpdateFinished: function updateChecker_onUpdateFinished(aAddon) { |
michael@0 | 4700 | self.state = AddonManager.STATE_DOWNLOADED; |
michael@0 | 4701 | XPIProvider.installs.push(self); |
michael@0 | 4702 | AddonManagerPrivate.callInstallListeners("onNewInstall", |
michael@0 | 4703 | self.listeners, |
michael@0 | 4704 | self.wrapper); |
michael@0 | 4705 | |
michael@0 | 4706 | aCallback(self); |
michael@0 | 4707 | } |
michael@0 | 4708 | }, AddonManager.UPDATE_WHEN_ADDON_INSTALLED); |
michael@0 | 4709 | } |
michael@0 | 4710 | else { |
michael@0 | 4711 | XPIProvider.installs.push(self); |
michael@0 | 4712 | AddonManagerPrivate.callInstallListeners("onNewInstall", |
michael@0 | 4713 | self.listeners, |
michael@0 | 4714 | self.wrapper); |
michael@0 | 4715 | |
michael@0 | 4716 | aCallback(self); |
michael@0 | 4717 | } |
michael@0 | 4718 | }); |
michael@0 | 4719 | }); |
michael@0 | 4720 | } |
michael@0 | 4721 | catch (e) { |
michael@0 | 4722 | logger.warn("Invalid XPI", e); |
michael@0 | 4723 | this.state = AddonManager.STATE_DOWNLOAD_FAILED; |
michael@0 | 4724 | this.error = AddonManager.ERROR_CORRUPT_FILE; |
michael@0 | 4725 | aCallback(this); |
michael@0 | 4726 | return; |
michael@0 | 4727 | } |
michael@0 | 4728 | }, |
michael@0 | 4729 | |
michael@0 | 4730 | /** |
michael@0 | 4731 | * Initialises this install to be a download from a remote url. |
michael@0 | 4732 | * |
michael@0 | 4733 | * @param aCallback |
michael@0 | 4734 | * The callback to pass the initialised AddonInstall to |
michael@0 | 4735 | * @param aName |
michael@0 | 4736 | * An optional name for the add-on |
michael@0 | 4737 | * @param aType |
michael@0 | 4738 | * An optional type for the add-on |
michael@0 | 4739 | * @param aIcons |
michael@0 | 4740 | * Optional icons for the add-on |
michael@0 | 4741 | * @param aVersion |
michael@0 | 4742 | * An optional version for the add-on |
michael@0 | 4743 | */ |
michael@0 | 4744 | initAvailableDownload: function AI_initAvailableDownload(aName, aType, aIcons, aVersion, aCallback) { |
michael@0 | 4745 | this.state = AddonManager.STATE_AVAILABLE; |
michael@0 | 4746 | this.name = aName; |
michael@0 | 4747 | this.type = aType; |
michael@0 | 4748 | this.version = aVersion; |
michael@0 | 4749 | this.icons = aIcons; |
michael@0 | 4750 | this.progress = 0; |
michael@0 | 4751 | this.maxProgress = -1; |
michael@0 | 4752 | |
michael@0 | 4753 | XPIProvider.installs.push(this); |
michael@0 | 4754 | AddonManagerPrivate.callInstallListeners("onNewInstall", this.listeners, |
michael@0 | 4755 | this.wrapper); |
michael@0 | 4756 | |
michael@0 | 4757 | makeSafe(aCallback)(this); |
michael@0 | 4758 | }, |
michael@0 | 4759 | |
michael@0 | 4760 | /** |
michael@0 | 4761 | * Starts installation of this add-on from whatever state it is currently at |
michael@0 | 4762 | * if possible. |
michael@0 | 4763 | * |
michael@0 | 4764 | * @throws if installation cannot proceed from the current state |
michael@0 | 4765 | */ |
michael@0 | 4766 | install: function AI_install() { |
michael@0 | 4767 | switch (this.state) { |
michael@0 | 4768 | case AddonManager.STATE_AVAILABLE: |
michael@0 | 4769 | this.startDownload(); |
michael@0 | 4770 | break; |
michael@0 | 4771 | case AddonManager.STATE_DOWNLOADED: |
michael@0 | 4772 | this.startInstall(); |
michael@0 | 4773 | break; |
michael@0 | 4774 | case AddonManager.STATE_DOWNLOAD_FAILED: |
michael@0 | 4775 | case AddonManager.STATE_INSTALL_FAILED: |
michael@0 | 4776 | case AddonManager.STATE_CANCELLED: |
michael@0 | 4777 | this.removeTemporaryFile(); |
michael@0 | 4778 | this.state = AddonManager.STATE_AVAILABLE; |
michael@0 | 4779 | this.error = 0; |
michael@0 | 4780 | this.progress = 0; |
michael@0 | 4781 | this.maxProgress = -1; |
michael@0 | 4782 | this.hash = this.originalHash; |
michael@0 | 4783 | XPIProvider.installs.push(this); |
michael@0 | 4784 | this.startDownload(); |
michael@0 | 4785 | break; |
michael@0 | 4786 | case AddonManager.STATE_DOWNLOADING: |
michael@0 | 4787 | case AddonManager.STATE_CHECKING: |
michael@0 | 4788 | case AddonManager.STATE_INSTALLING: |
michael@0 | 4789 | // Installation is already running |
michael@0 | 4790 | return; |
michael@0 | 4791 | default: |
michael@0 | 4792 | throw new Error("Cannot start installing from this state"); |
michael@0 | 4793 | } |
michael@0 | 4794 | }, |
michael@0 | 4795 | |
michael@0 | 4796 | /** |
michael@0 | 4797 | * Cancels installation of this add-on. |
michael@0 | 4798 | * |
michael@0 | 4799 | * @throws if installation cannot be cancelled from the current state |
michael@0 | 4800 | */ |
michael@0 | 4801 | cancel: function AI_cancel() { |
michael@0 | 4802 | switch (this.state) { |
michael@0 | 4803 | case AddonManager.STATE_DOWNLOADING: |
michael@0 | 4804 | if (this.channel) |
michael@0 | 4805 | this.channel.cancel(Cr.NS_BINDING_ABORTED); |
michael@0 | 4806 | case AddonManager.STATE_AVAILABLE: |
michael@0 | 4807 | case AddonManager.STATE_DOWNLOADED: |
michael@0 | 4808 | logger.debug("Cancelling download of " + this.sourceURI.spec); |
michael@0 | 4809 | this.state = AddonManager.STATE_CANCELLED; |
michael@0 | 4810 | XPIProvider.removeActiveInstall(this); |
michael@0 | 4811 | AddonManagerPrivate.callInstallListeners("onDownloadCancelled", |
michael@0 | 4812 | this.listeners, this.wrapper); |
michael@0 | 4813 | this.removeTemporaryFile(); |
michael@0 | 4814 | break; |
michael@0 | 4815 | case AddonManager.STATE_INSTALLED: |
michael@0 | 4816 | logger.debug("Cancelling install of " + this.addon.id); |
michael@0 | 4817 | let xpi = this.installLocation.getStagingDir(); |
michael@0 | 4818 | xpi.append(this.addon.id + ".xpi"); |
michael@0 | 4819 | flushJarCache(xpi); |
michael@0 | 4820 | this.installLocation.cleanStagingDir([this.addon.id, this.addon.id + ".xpi", |
michael@0 | 4821 | this.addon.id + ".json"]); |
michael@0 | 4822 | this.state = AddonManager.STATE_CANCELLED; |
michael@0 | 4823 | XPIProvider.removeActiveInstall(this); |
michael@0 | 4824 | |
michael@0 | 4825 | if (this.existingAddon) { |
michael@0 | 4826 | delete this.existingAddon.pendingUpgrade; |
michael@0 | 4827 | this.existingAddon.pendingUpgrade = null; |
michael@0 | 4828 | } |
michael@0 | 4829 | |
michael@0 | 4830 | AddonManagerPrivate.callAddonListeners("onOperationCancelled", createWrapper(this.addon)); |
michael@0 | 4831 | |
michael@0 | 4832 | AddonManagerPrivate.callInstallListeners("onInstallCancelled", |
michael@0 | 4833 | this.listeners, this.wrapper); |
michael@0 | 4834 | break; |
michael@0 | 4835 | default: |
michael@0 | 4836 | throw new Error("Cannot cancel install of " + this.sourceURI.spec + |
michael@0 | 4837 | " from this state (" + this.state + ")"); |
michael@0 | 4838 | } |
michael@0 | 4839 | }, |
michael@0 | 4840 | |
michael@0 | 4841 | /** |
michael@0 | 4842 | * Adds an InstallListener for this instance if the listener is not already |
michael@0 | 4843 | * registered. |
michael@0 | 4844 | * |
michael@0 | 4845 | * @param aListener |
michael@0 | 4846 | * The InstallListener to add |
michael@0 | 4847 | */ |
michael@0 | 4848 | addListener: function AI_addListener(aListener) { |
michael@0 | 4849 | if (!this.listeners.some(function addListener_matchListener(i) { return i == aListener; })) |
michael@0 | 4850 | this.listeners.push(aListener); |
michael@0 | 4851 | }, |
michael@0 | 4852 | |
michael@0 | 4853 | /** |
michael@0 | 4854 | * Removes an InstallListener for this instance if it is registered. |
michael@0 | 4855 | * |
michael@0 | 4856 | * @param aListener |
michael@0 | 4857 | * The InstallListener to remove |
michael@0 | 4858 | */ |
michael@0 | 4859 | removeListener: function AI_removeListener(aListener) { |
michael@0 | 4860 | this.listeners = this.listeners.filter(function removeListener_filterListener(i) { |
michael@0 | 4861 | return i != aListener; |
michael@0 | 4862 | }); |
michael@0 | 4863 | }, |
michael@0 | 4864 | |
michael@0 | 4865 | /** |
michael@0 | 4866 | * Removes the temporary file owned by this AddonInstall if there is one. |
michael@0 | 4867 | */ |
michael@0 | 4868 | removeTemporaryFile: function AI_removeTemporaryFile() { |
michael@0 | 4869 | // Only proceed if this AddonInstall owns its XPI file |
michael@0 | 4870 | if (!this.ownsTempFile) { |
michael@0 | 4871 | this.logger.debug("removeTemporaryFile: " + this.sourceURI.spec + " does not own temp file"); |
michael@0 | 4872 | return; |
michael@0 | 4873 | } |
michael@0 | 4874 | |
michael@0 | 4875 | try { |
michael@0 | 4876 | this.logger.debug("removeTemporaryFile: " + this.sourceURI.spec + " removing temp file " + |
michael@0 | 4877 | this.file.path); |
michael@0 | 4878 | this.file.remove(true); |
michael@0 | 4879 | this.ownsTempFile = false; |
michael@0 | 4880 | } |
michael@0 | 4881 | catch (e) { |
michael@0 | 4882 | this.logger.warn("Failed to remove temporary file " + this.file.path + " for addon " + |
michael@0 | 4883 | this.sourceURI.spec, |
michael@0 | 4884 | e); |
michael@0 | 4885 | } |
michael@0 | 4886 | }, |
michael@0 | 4887 | |
michael@0 | 4888 | /** |
michael@0 | 4889 | * Updates the sourceURI and releaseNotesURI values on the Addon being |
michael@0 | 4890 | * installed by this AddonInstall instance. |
michael@0 | 4891 | */ |
michael@0 | 4892 | updateAddonURIs: function AI_updateAddonURIs() { |
michael@0 | 4893 | this.addon.sourceURI = this.sourceURI.spec; |
michael@0 | 4894 | if (this.releaseNotesURI) |
michael@0 | 4895 | this.addon.releaseNotesURI = this.releaseNotesURI.spec; |
michael@0 | 4896 | }, |
michael@0 | 4897 | |
michael@0 | 4898 | /** |
michael@0 | 4899 | * Loads add-on manifests from a multi-package XPI file. Each of the |
michael@0 | 4900 | * XPI and JAR files contained in the XPI will be extracted. Any that |
michael@0 | 4901 | * do not contain valid add-ons will be ignored. The first valid add-on will |
michael@0 | 4902 | * be installed by this AddonInstall instance, the rest will have new |
michael@0 | 4903 | * AddonInstall instances created for them. |
michael@0 | 4904 | * |
michael@0 | 4905 | * @param aZipReader |
michael@0 | 4906 | * An open nsIZipReader for the multi-package XPI's files. This will |
michael@0 | 4907 | * be closed before this method returns. |
michael@0 | 4908 | * @param aCallback |
michael@0 | 4909 | * A function to call when all of the add-on manifests have been |
michael@0 | 4910 | * loaded. Because this loadMultipackageManifests is an internal API |
michael@0 | 4911 | * we don't exception-wrap this callback |
michael@0 | 4912 | */ |
michael@0 | 4913 | _loadMultipackageManifests: function AI_loadMultipackageManifests(aZipReader, |
michael@0 | 4914 | aCallback) { |
michael@0 | 4915 | let files = []; |
michael@0 | 4916 | let entries = aZipReader.findEntries("(*.[Xx][Pp][Ii]|*.[Jj][Aa][Rr])"); |
michael@0 | 4917 | while (entries.hasMore()) { |
michael@0 | 4918 | let entryName = entries.getNext(); |
michael@0 | 4919 | var target = getTemporaryFile(); |
michael@0 | 4920 | try { |
michael@0 | 4921 | aZipReader.extract(entryName, target); |
michael@0 | 4922 | files.push(target); |
michael@0 | 4923 | } |
michael@0 | 4924 | catch (e) { |
michael@0 | 4925 | logger.warn("Failed to extract " + entryName + " from multi-package " + |
michael@0 | 4926 | "XPI", e); |
michael@0 | 4927 | target.remove(false); |
michael@0 | 4928 | } |
michael@0 | 4929 | } |
michael@0 | 4930 | |
michael@0 | 4931 | aZipReader.close(); |
michael@0 | 4932 | |
michael@0 | 4933 | if (files.length == 0) { |
michael@0 | 4934 | throw new Error("Multi-package XPI does not contain any packages " + |
michael@0 | 4935 | "to install"); |
michael@0 | 4936 | } |
michael@0 | 4937 | |
michael@0 | 4938 | let addon = null; |
michael@0 | 4939 | |
michael@0 | 4940 | // Find the first file that has a valid install manifest and use it for |
michael@0 | 4941 | // the add-on that this AddonInstall instance will install. |
michael@0 | 4942 | while (files.length > 0) { |
michael@0 | 4943 | this.removeTemporaryFile(); |
michael@0 | 4944 | this.file = files.shift(); |
michael@0 | 4945 | this.ownsTempFile = true; |
michael@0 | 4946 | try { |
michael@0 | 4947 | addon = loadManifestFromZipFile(this.file); |
michael@0 | 4948 | break; |
michael@0 | 4949 | } |
michael@0 | 4950 | catch (e) { |
michael@0 | 4951 | logger.warn(this.file.leafName + " cannot be installed from multi-package " + |
michael@0 | 4952 | "XPI", e); |
michael@0 | 4953 | } |
michael@0 | 4954 | } |
michael@0 | 4955 | |
michael@0 | 4956 | if (!addon) { |
michael@0 | 4957 | // No valid add-on was found |
michael@0 | 4958 | aCallback(); |
michael@0 | 4959 | return; |
michael@0 | 4960 | } |
michael@0 | 4961 | |
michael@0 | 4962 | this.addon = addon; |
michael@0 | 4963 | |
michael@0 | 4964 | this.updateAddonURIs(); |
michael@0 | 4965 | |
michael@0 | 4966 | this.addon._install = this; |
michael@0 | 4967 | this.name = this.addon.selectedLocale.name; |
michael@0 | 4968 | this.type = this.addon.type; |
michael@0 | 4969 | this.version = this.addon.version; |
michael@0 | 4970 | |
michael@0 | 4971 | // Setting the iconURL to something inside the XPI locks the XPI and |
michael@0 | 4972 | // makes it impossible to delete on Windows. |
michael@0 | 4973 | //let newIcon = createWrapper(this.addon).iconURL; |
michael@0 | 4974 | //if (newIcon) |
michael@0 | 4975 | // this.iconURL = newIcon; |
michael@0 | 4976 | |
michael@0 | 4977 | // Create new AddonInstall instances for every remaining file |
michael@0 | 4978 | if (files.length > 0) { |
michael@0 | 4979 | this.linkedInstalls = []; |
michael@0 | 4980 | let count = 0; |
michael@0 | 4981 | let self = this; |
michael@0 | 4982 | files.forEach(function(file) { |
michael@0 | 4983 | AddonInstall.createInstall(function loadMultipackageManifests_createInstall(aInstall) { |
michael@0 | 4984 | // Ignore bad add-ons (createInstall will have logged the error) |
michael@0 | 4985 | if (aInstall.state == AddonManager.STATE_DOWNLOAD_FAILED) { |
michael@0 | 4986 | // Manually remove the temporary file |
michael@0 | 4987 | file.remove(true); |
michael@0 | 4988 | } |
michael@0 | 4989 | else { |
michael@0 | 4990 | // Make the new install own its temporary file |
michael@0 | 4991 | aInstall.ownsTempFile = true; |
michael@0 | 4992 | |
michael@0 | 4993 | self.linkedInstalls.push(aInstall) |
michael@0 | 4994 | |
michael@0 | 4995 | aInstall.sourceURI = self.sourceURI; |
michael@0 | 4996 | aInstall.releaseNotesURI = self.releaseNotesURI; |
michael@0 | 4997 | aInstall.updateAddonURIs(); |
michael@0 | 4998 | } |
michael@0 | 4999 | |
michael@0 | 5000 | count++; |
michael@0 | 5001 | if (count == files.length) |
michael@0 | 5002 | aCallback(); |
michael@0 | 5003 | }, file); |
michael@0 | 5004 | }, this); |
michael@0 | 5005 | } |
michael@0 | 5006 | else { |
michael@0 | 5007 | aCallback(); |
michael@0 | 5008 | } |
michael@0 | 5009 | }, |
michael@0 | 5010 | |
michael@0 | 5011 | /** |
michael@0 | 5012 | * Called after the add-on is a local file and the signature and install |
michael@0 | 5013 | * manifest can be read. |
michael@0 | 5014 | * |
michael@0 | 5015 | * @param aCallback |
michael@0 | 5016 | * A function to call when the manifest has been loaded |
michael@0 | 5017 | * @throws if the add-on does not contain a valid install manifest or the |
michael@0 | 5018 | * XPI is incorrectly signed |
michael@0 | 5019 | */ |
michael@0 | 5020 | loadManifest: function AI_loadManifest(aCallback) { |
michael@0 | 5021 | aCallback = makeSafe(aCallback); |
michael@0 | 5022 | let self = this; |
michael@0 | 5023 | function addRepositoryData(aAddon) { |
michael@0 | 5024 | // Try to load from the existing cache first |
michael@0 | 5025 | AddonRepository.getCachedAddonByID(aAddon.id, function loadManifest_getCachedAddonByID(aRepoAddon) { |
michael@0 | 5026 | if (aRepoAddon) { |
michael@0 | 5027 | aAddon._repositoryAddon = aRepoAddon; |
michael@0 | 5028 | self.name = self.name || aAddon._repositoryAddon.name; |
michael@0 | 5029 | aAddon.compatibilityOverrides = aRepoAddon.compatibilityOverrides; |
michael@0 | 5030 | aAddon.appDisabled = !isUsableAddon(aAddon); |
michael@0 | 5031 | aCallback(); |
michael@0 | 5032 | return; |
michael@0 | 5033 | } |
michael@0 | 5034 | |
michael@0 | 5035 | // It wasn't there so try to re-download it |
michael@0 | 5036 | AddonRepository.cacheAddons([aAddon.id], function loadManifest_cacheAddons() { |
michael@0 | 5037 | AddonRepository.getCachedAddonByID(aAddon.id, function loadManifest_getCachedAddonByID(aRepoAddon) { |
michael@0 | 5038 | aAddon._repositoryAddon = aRepoAddon; |
michael@0 | 5039 | self.name = self.name || aAddon._repositoryAddon.name; |
michael@0 | 5040 | aAddon.compatibilityOverrides = aRepoAddon ? |
michael@0 | 5041 | aRepoAddon.compatibilityOverrides : |
michael@0 | 5042 | null; |
michael@0 | 5043 | aAddon.appDisabled = !isUsableAddon(aAddon); |
michael@0 | 5044 | aCallback(); |
michael@0 | 5045 | }); |
michael@0 | 5046 | }); |
michael@0 | 5047 | }); |
michael@0 | 5048 | } |
michael@0 | 5049 | |
michael@0 | 5050 | let zipreader = Cc["@mozilla.org/libjar/zip-reader;1"]. |
michael@0 | 5051 | createInstance(Ci.nsIZipReader); |
michael@0 | 5052 | try { |
michael@0 | 5053 | zipreader.open(this.file); |
michael@0 | 5054 | } |
michael@0 | 5055 | catch (e) { |
michael@0 | 5056 | zipreader.close(); |
michael@0 | 5057 | throw e; |
michael@0 | 5058 | } |
michael@0 | 5059 | |
michael@0 | 5060 | let principal = zipreader.getCertificatePrincipal(null); |
michael@0 | 5061 | if (principal && principal.hasCertificate) { |
michael@0 | 5062 | logger.debug("Verifying XPI signature"); |
michael@0 | 5063 | if (verifyZipSigning(zipreader, principal)) { |
michael@0 | 5064 | let x509 = principal.certificate; |
michael@0 | 5065 | if (x509 instanceof Ci.nsIX509Cert) |
michael@0 | 5066 | this.certificate = x509; |
michael@0 | 5067 | if (this.certificate && this.certificate.commonName.length > 0) |
michael@0 | 5068 | this.certName = this.certificate.commonName; |
michael@0 | 5069 | else |
michael@0 | 5070 | this.certName = principal.prettyName; |
michael@0 | 5071 | } |
michael@0 | 5072 | else { |
michael@0 | 5073 | zipreader.close(); |
michael@0 | 5074 | throw new Error("XPI is incorrectly signed"); |
michael@0 | 5075 | } |
michael@0 | 5076 | } |
michael@0 | 5077 | |
michael@0 | 5078 | try { |
michael@0 | 5079 | this.addon = loadManifestFromZipReader(zipreader); |
michael@0 | 5080 | } |
michael@0 | 5081 | catch (e) { |
michael@0 | 5082 | zipreader.close(); |
michael@0 | 5083 | throw e; |
michael@0 | 5084 | } |
michael@0 | 5085 | |
michael@0 | 5086 | if (this.addon.type == "multipackage") { |
michael@0 | 5087 | this._loadMultipackageManifests(zipreader, function loadManifest_loadMultipackageManifests() { |
michael@0 | 5088 | addRepositoryData(self.addon); |
michael@0 | 5089 | }); |
michael@0 | 5090 | return; |
michael@0 | 5091 | } |
michael@0 | 5092 | |
michael@0 | 5093 | zipreader.close(); |
michael@0 | 5094 | |
michael@0 | 5095 | this.updateAddonURIs(); |
michael@0 | 5096 | |
michael@0 | 5097 | this.addon._install = this; |
michael@0 | 5098 | this.name = this.addon.selectedLocale.name; |
michael@0 | 5099 | this.type = this.addon.type; |
michael@0 | 5100 | this.version = this.addon.version; |
michael@0 | 5101 | |
michael@0 | 5102 | // Setting the iconURL to something inside the XPI locks the XPI and |
michael@0 | 5103 | // makes it impossible to delete on Windows. |
michael@0 | 5104 | //let newIcon = createWrapper(this.addon).iconURL; |
michael@0 | 5105 | //if (newIcon) |
michael@0 | 5106 | // this.iconURL = newIcon; |
michael@0 | 5107 | |
michael@0 | 5108 | addRepositoryData(this.addon); |
michael@0 | 5109 | }, |
michael@0 | 5110 | |
michael@0 | 5111 | observe: function AI_observe(aSubject, aTopic, aData) { |
michael@0 | 5112 | // Network is going offline |
michael@0 | 5113 | this.cancel(); |
michael@0 | 5114 | }, |
michael@0 | 5115 | |
michael@0 | 5116 | /** |
michael@0 | 5117 | * Starts downloading the add-on's XPI file. |
michael@0 | 5118 | */ |
michael@0 | 5119 | startDownload: function AI_startDownload() { |
michael@0 | 5120 | this.state = AddonManager.STATE_DOWNLOADING; |
michael@0 | 5121 | if (!AddonManagerPrivate.callInstallListeners("onDownloadStarted", |
michael@0 | 5122 | this.listeners, this.wrapper)) { |
michael@0 | 5123 | logger.debug("onDownloadStarted listeners cancelled installation of addon " + this.sourceURI.spec); |
michael@0 | 5124 | this.state = AddonManager.STATE_CANCELLED; |
michael@0 | 5125 | XPIProvider.removeActiveInstall(this); |
michael@0 | 5126 | AddonManagerPrivate.callInstallListeners("onDownloadCancelled", |
michael@0 | 5127 | this.listeners, this.wrapper) |
michael@0 | 5128 | return; |
michael@0 | 5129 | } |
michael@0 | 5130 | |
michael@0 | 5131 | // If a listener changed our state then do not proceed with the download |
michael@0 | 5132 | if (this.state != AddonManager.STATE_DOWNLOADING) |
michael@0 | 5133 | return; |
michael@0 | 5134 | |
michael@0 | 5135 | if (this.channel) { |
michael@0 | 5136 | // A previous download attempt hasn't finished cleaning up yet, signal |
michael@0 | 5137 | // that it should restart when complete |
michael@0 | 5138 | logger.debug("Waiting for previous download to complete"); |
michael@0 | 5139 | this.restartDownload = true; |
michael@0 | 5140 | return; |
michael@0 | 5141 | } |
michael@0 | 5142 | |
michael@0 | 5143 | this.openChannel(); |
michael@0 | 5144 | }, |
michael@0 | 5145 | |
michael@0 | 5146 | openChannel: function AI_openChannel() { |
michael@0 | 5147 | this.restartDownload = false; |
michael@0 | 5148 | |
michael@0 | 5149 | try { |
michael@0 | 5150 | this.file = getTemporaryFile(); |
michael@0 | 5151 | this.ownsTempFile = true; |
michael@0 | 5152 | this.stream = Cc["@mozilla.org/network/file-output-stream;1"]. |
michael@0 | 5153 | createInstance(Ci.nsIFileOutputStream); |
michael@0 | 5154 | this.stream.init(this.file, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | |
michael@0 | 5155 | FileUtils.MODE_TRUNCATE, FileUtils.PERMS_FILE, 0); |
michael@0 | 5156 | } |
michael@0 | 5157 | catch (e) { |
michael@0 | 5158 | logger.warn("Failed to start download for addon " + this.sourceURI.spec, e); |
michael@0 | 5159 | this.state = AddonManager.STATE_DOWNLOAD_FAILED; |
michael@0 | 5160 | this.error = AddonManager.ERROR_FILE_ACCESS; |
michael@0 | 5161 | XPIProvider.removeActiveInstall(this); |
michael@0 | 5162 | AddonManagerPrivate.callInstallListeners("onDownloadFailed", |
michael@0 | 5163 | this.listeners, this.wrapper); |
michael@0 | 5164 | return; |
michael@0 | 5165 | } |
michael@0 | 5166 | |
michael@0 | 5167 | let listener = Cc["@mozilla.org/network/stream-listener-tee;1"]. |
michael@0 | 5168 | createInstance(Ci.nsIStreamListenerTee); |
michael@0 | 5169 | listener.init(this, this.stream); |
michael@0 | 5170 | try { |
michael@0 | 5171 | Components.utils.import("resource://gre/modules/CertUtils.jsm"); |
michael@0 | 5172 | let requireBuiltIn = Prefs.getBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, true); |
michael@0 | 5173 | this.badCertHandler = new BadCertHandler(!requireBuiltIn); |
michael@0 | 5174 | |
michael@0 | 5175 | this.channel = NetUtil.newChannel(this.sourceURI); |
michael@0 | 5176 | this.channel.notificationCallbacks = this; |
michael@0 | 5177 | if (this.channel instanceof Ci.nsIHttpChannelInternal) |
michael@0 | 5178 | this.channel.forceAllowThirdPartyCookie = true; |
michael@0 | 5179 | this.channel.asyncOpen(listener, null); |
michael@0 | 5180 | |
michael@0 | 5181 | Services.obs.addObserver(this, "network:offline-about-to-go-offline", false); |
michael@0 | 5182 | } |
michael@0 | 5183 | catch (e) { |
michael@0 | 5184 | logger.warn("Failed to start download for addon " + this.sourceURI.spec, e); |
michael@0 | 5185 | this.state = AddonManager.STATE_DOWNLOAD_FAILED; |
michael@0 | 5186 | this.error = AddonManager.ERROR_NETWORK_FAILURE; |
michael@0 | 5187 | XPIProvider.removeActiveInstall(this); |
michael@0 | 5188 | AddonManagerPrivate.callInstallListeners("onDownloadFailed", |
michael@0 | 5189 | this.listeners, this.wrapper); |
michael@0 | 5190 | } |
michael@0 | 5191 | }, |
michael@0 | 5192 | |
michael@0 | 5193 | /** |
michael@0 | 5194 | * Update the crypto hasher with the new data and call the progress listeners. |
michael@0 | 5195 | * |
michael@0 | 5196 | * @see nsIStreamListener |
michael@0 | 5197 | */ |
michael@0 | 5198 | onDataAvailable: function AI_onDataAvailable(aRequest, aContext, aInputstream, |
michael@0 | 5199 | aOffset, aCount) { |
michael@0 | 5200 | this.crypto.updateFromStream(aInputstream, aCount); |
michael@0 | 5201 | this.progress += aCount; |
michael@0 | 5202 | if (!AddonManagerPrivate.callInstallListeners("onDownloadProgress", |
michael@0 | 5203 | this.listeners, this.wrapper)) { |
michael@0 | 5204 | // TODO cancel the download and make it available again (bug 553024) |
michael@0 | 5205 | } |
michael@0 | 5206 | }, |
michael@0 | 5207 | |
michael@0 | 5208 | /** |
michael@0 | 5209 | * Check the redirect response for a hash of the target XPI and verify that |
michael@0 | 5210 | * we don't end up on an insecure channel. |
michael@0 | 5211 | * |
michael@0 | 5212 | * @see nsIChannelEventSink |
michael@0 | 5213 | */ |
michael@0 | 5214 | asyncOnChannelRedirect: function AI_asyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, aCallback) { |
michael@0 | 5215 | if (!this.hash && aOldChannel.originalURI.schemeIs("https") && |
michael@0 | 5216 | aOldChannel instanceof Ci.nsIHttpChannel) { |
michael@0 | 5217 | try { |
michael@0 | 5218 | let hashStr = aOldChannel.getResponseHeader("X-Target-Digest"); |
michael@0 | 5219 | let hashSplit = hashStr.toLowerCase().split(":"); |
michael@0 | 5220 | this.hash = { |
michael@0 | 5221 | algorithm: hashSplit[0], |
michael@0 | 5222 | data: hashSplit[1] |
michael@0 | 5223 | }; |
michael@0 | 5224 | } |
michael@0 | 5225 | catch (e) { |
michael@0 | 5226 | } |
michael@0 | 5227 | } |
michael@0 | 5228 | |
michael@0 | 5229 | // Verify that we don't end up on an insecure channel if we haven't got a |
michael@0 | 5230 | // hash to verify with (see bug 537761 for discussion) |
michael@0 | 5231 | if (!this.hash) |
michael@0 | 5232 | this.badCertHandler.asyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, aCallback); |
michael@0 | 5233 | else |
michael@0 | 5234 | aCallback.onRedirectVerifyCallback(Cr.NS_OK); |
michael@0 | 5235 | |
michael@0 | 5236 | this.channel = aNewChannel; |
michael@0 | 5237 | }, |
michael@0 | 5238 | |
michael@0 | 5239 | /** |
michael@0 | 5240 | * This is the first chance to get at real headers on the channel. |
michael@0 | 5241 | * |
michael@0 | 5242 | * @see nsIStreamListener |
michael@0 | 5243 | */ |
michael@0 | 5244 | onStartRequest: function AI_onStartRequest(aRequest, aContext) { |
michael@0 | 5245 | this.crypto = Cc["@mozilla.org/security/hash;1"]. |
michael@0 | 5246 | createInstance(Ci.nsICryptoHash); |
michael@0 | 5247 | if (this.hash) { |
michael@0 | 5248 | try { |
michael@0 | 5249 | this.crypto.initWithString(this.hash.algorithm); |
michael@0 | 5250 | } |
michael@0 | 5251 | catch (e) { |
michael@0 | 5252 | logger.warn("Unknown hash algorithm '" + this.hash.algorithm + "' for addon " + this.sourceURI.spec, e); |
michael@0 | 5253 | this.state = AddonManager.STATE_DOWNLOAD_FAILED; |
michael@0 | 5254 | this.error = AddonManager.ERROR_INCORRECT_HASH; |
michael@0 | 5255 | XPIProvider.removeActiveInstall(this); |
michael@0 | 5256 | AddonManagerPrivate.callInstallListeners("onDownloadFailed", |
michael@0 | 5257 | this.listeners, this.wrapper); |
michael@0 | 5258 | aRequest.cancel(Cr.NS_BINDING_ABORTED); |
michael@0 | 5259 | return; |
michael@0 | 5260 | } |
michael@0 | 5261 | } |
michael@0 | 5262 | else { |
michael@0 | 5263 | // We always need something to consume data from the inputstream passed |
michael@0 | 5264 | // to onDataAvailable so just create a dummy cryptohasher to do that. |
michael@0 | 5265 | this.crypto.initWithString("sha1"); |
michael@0 | 5266 | } |
michael@0 | 5267 | |
michael@0 | 5268 | this.progress = 0; |
michael@0 | 5269 | if (aRequest instanceof Ci.nsIChannel) { |
michael@0 | 5270 | try { |
michael@0 | 5271 | this.maxProgress = aRequest.contentLength; |
michael@0 | 5272 | } |
michael@0 | 5273 | catch (e) { |
michael@0 | 5274 | } |
michael@0 | 5275 | logger.debug("Download started for " + this.sourceURI.spec + " to file " + |
michael@0 | 5276 | this.file.path); |
michael@0 | 5277 | } |
michael@0 | 5278 | }, |
michael@0 | 5279 | |
michael@0 | 5280 | /** |
michael@0 | 5281 | * The download is complete. |
michael@0 | 5282 | * |
michael@0 | 5283 | * @see nsIStreamListener |
michael@0 | 5284 | */ |
michael@0 | 5285 | onStopRequest: function AI_onStopRequest(aRequest, aContext, aStatus) { |
michael@0 | 5286 | this.stream.close(); |
michael@0 | 5287 | this.channel = null; |
michael@0 | 5288 | this.badCerthandler = null; |
michael@0 | 5289 | Services.obs.removeObserver(this, "network:offline-about-to-go-offline"); |
michael@0 | 5290 | |
michael@0 | 5291 | // If the download was cancelled then all events will have already been sent |
michael@0 | 5292 | if (aStatus == Cr.NS_BINDING_ABORTED) { |
michael@0 | 5293 | this.removeTemporaryFile(); |
michael@0 | 5294 | if (this.restartDownload) |
michael@0 | 5295 | this.openChannel(); |
michael@0 | 5296 | return; |
michael@0 | 5297 | } |
michael@0 | 5298 | |
michael@0 | 5299 | logger.debug("Download of " + this.sourceURI.spec + " completed."); |
michael@0 | 5300 | |
michael@0 | 5301 | if (Components.isSuccessCode(aStatus)) { |
michael@0 | 5302 | if (!(aRequest instanceof Ci.nsIHttpChannel) || aRequest.requestSucceeded) { |
michael@0 | 5303 | if (!this.hash && (aRequest instanceof Ci.nsIChannel)) { |
michael@0 | 5304 | try { |
michael@0 | 5305 | checkCert(aRequest, |
michael@0 | 5306 | !Prefs.getBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, true)); |
michael@0 | 5307 | } |
michael@0 | 5308 | catch (e) { |
michael@0 | 5309 | this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, e); |
michael@0 | 5310 | return; |
michael@0 | 5311 | } |
michael@0 | 5312 | } |
michael@0 | 5313 | |
michael@0 | 5314 | // convert the binary hash data to a hex string. |
michael@0 | 5315 | let calculatedHash = getHashStringForCrypto(this.crypto); |
michael@0 | 5316 | this.crypto = null; |
michael@0 | 5317 | if (this.hash && calculatedHash != this.hash.data) { |
michael@0 | 5318 | this.downloadFailed(AddonManager.ERROR_INCORRECT_HASH, |
michael@0 | 5319 | "Downloaded file hash (" + calculatedHash + |
michael@0 | 5320 | ") did not match provided hash (" + this.hash.data + ")"); |
michael@0 | 5321 | return; |
michael@0 | 5322 | } |
michael@0 | 5323 | try { |
michael@0 | 5324 | let self = this; |
michael@0 | 5325 | this.loadManifest(function onStopRequest_loadManifest() { |
michael@0 | 5326 | if (self.addon.isCompatible) { |
michael@0 | 5327 | self.downloadCompleted(); |
michael@0 | 5328 | } |
michael@0 | 5329 | else { |
michael@0 | 5330 | // TODO Should we send some event here (bug 557716)? |
michael@0 | 5331 | self.state = AddonManager.STATE_CHECKING; |
michael@0 | 5332 | new UpdateChecker(self.addon, { |
michael@0 | 5333 | onUpdateFinished: function onStopRequest_onUpdateFinished(aAddon) { |
michael@0 | 5334 | self.downloadCompleted(); |
michael@0 | 5335 | } |
michael@0 | 5336 | }, AddonManager.UPDATE_WHEN_ADDON_INSTALLED); |
michael@0 | 5337 | } |
michael@0 | 5338 | }); |
michael@0 | 5339 | } |
michael@0 | 5340 | catch (e) { |
michael@0 | 5341 | this.downloadFailed(AddonManager.ERROR_CORRUPT_FILE, e); |
michael@0 | 5342 | } |
michael@0 | 5343 | } |
michael@0 | 5344 | else { |
michael@0 | 5345 | if (aRequest instanceof Ci.nsIHttpChannel) |
michael@0 | 5346 | this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, |
michael@0 | 5347 | aRequest.responseStatus + " " + |
michael@0 | 5348 | aRequest.responseStatusText); |
michael@0 | 5349 | else |
michael@0 | 5350 | this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, aStatus); |
michael@0 | 5351 | } |
michael@0 | 5352 | } |
michael@0 | 5353 | else { |
michael@0 | 5354 | this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, aStatus); |
michael@0 | 5355 | } |
michael@0 | 5356 | }, |
michael@0 | 5357 | |
michael@0 | 5358 | /** |
michael@0 | 5359 | * Notify listeners that the download failed. |
michael@0 | 5360 | * |
michael@0 | 5361 | * @param aReason |
michael@0 | 5362 | * Something to log about the failure |
michael@0 | 5363 | * @param error |
michael@0 | 5364 | * The error code to pass to the listeners |
michael@0 | 5365 | */ |
michael@0 | 5366 | downloadFailed: function AI_downloadFailed(aReason, aError) { |
michael@0 | 5367 | logger.warn("Download of " + this.sourceURI.spec + " failed", aError); |
michael@0 | 5368 | this.state = AddonManager.STATE_DOWNLOAD_FAILED; |
michael@0 | 5369 | this.error = aReason; |
michael@0 | 5370 | XPIProvider.removeActiveInstall(this); |
michael@0 | 5371 | AddonManagerPrivate.callInstallListeners("onDownloadFailed", this.listeners, |
michael@0 | 5372 | this.wrapper); |
michael@0 | 5373 | |
michael@0 | 5374 | // If the listener hasn't restarted the download then remove any temporary |
michael@0 | 5375 | // file |
michael@0 | 5376 | if (this.state == AddonManager.STATE_DOWNLOAD_FAILED) { |
michael@0 | 5377 | logger.debug("downloadFailed: removing temp file for " + this.sourceURI.spec); |
michael@0 | 5378 | this.removeTemporaryFile(); |
michael@0 | 5379 | } |
michael@0 | 5380 | else |
michael@0 | 5381 | logger.debug("downloadFailed: listener changed AddonInstall state for " + |
michael@0 | 5382 | this.sourceURI.spec + " to " + this.state); |
michael@0 | 5383 | }, |
michael@0 | 5384 | |
michael@0 | 5385 | /** |
michael@0 | 5386 | * Notify listeners that the download completed. |
michael@0 | 5387 | */ |
michael@0 | 5388 | downloadCompleted: function AI_downloadCompleted() { |
michael@0 | 5389 | let self = this; |
michael@0 | 5390 | XPIDatabase.getVisibleAddonForID(this.addon.id, function downloadCompleted_getVisibleAddonForID(aAddon) { |
michael@0 | 5391 | if (aAddon) |
michael@0 | 5392 | self.existingAddon = aAddon; |
michael@0 | 5393 | |
michael@0 | 5394 | self.state = AddonManager.STATE_DOWNLOADED; |
michael@0 | 5395 | self.addon.updateDate = Date.now(); |
michael@0 | 5396 | |
michael@0 | 5397 | if (self.existingAddon) { |
michael@0 | 5398 | self.addon.existingAddonID = self.existingAddon.id; |
michael@0 | 5399 | self.addon.installDate = self.existingAddon.installDate; |
michael@0 | 5400 | applyBlocklistChanges(self.existingAddon, self.addon); |
michael@0 | 5401 | } |
michael@0 | 5402 | else { |
michael@0 | 5403 | self.addon.installDate = self.addon.updateDate; |
michael@0 | 5404 | } |
michael@0 | 5405 | |
michael@0 | 5406 | if (AddonManagerPrivate.callInstallListeners("onDownloadEnded", |
michael@0 | 5407 | self.listeners, |
michael@0 | 5408 | self.wrapper)) { |
michael@0 | 5409 | // If a listener changed our state then do not proceed with the install |
michael@0 | 5410 | if (self.state != AddonManager.STATE_DOWNLOADED) |
michael@0 | 5411 | return; |
michael@0 | 5412 | |
michael@0 | 5413 | self.install(); |
michael@0 | 5414 | |
michael@0 | 5415 | if (self.linkedInstalls) { |
michael@0 | 5416 | self.linkedInstalls.forEach(function(aInstall) { |
michael@0 | 5417 | aInstall.install(); |
michael@0 | 5418 | }); |
michael@0 | 5419 | } |
michael@0 | 5420 | } |
michael@0 | 5421 | }); |
michael@0 | 5422 | }, |
michael@0 | 5423 | |
michael@0 | 5424 | // TODO This relies on the assumption that we are always installing into the |
michael@0 | 5425 | // highest priority install location so the resulting add-on will be visible |
michael@0 | 5426 | // overriding any existing copy in another install location (bug 557710). |
michael@0 | 5427 | /** |
michael@0 | 5428 | * Installs the add-on into the install location. |
michael@0 | 5429 | */ |
michael@0 | 5430 | startInstall: function AI_startInstall() { |
michael@0 | 5431 | this.state = AddonManager.STATE_INSTALLING; |
michael@0 | 5432 | if (!AddonManagerPrivate.callInstallListeners("onInstallStarted", |
michael@0 | 5433 | this.listeners, this.wrapper)) { |
michael@0 | 5434 | this.state = AddonManager.STATE_DOWNLOADED; |
michael@0 | 5435 | XPIProvider.removeActiveInstall(this); |
michael@0 | 5436 | AddonManagerPrivate.callInstallListeners("onInstallCancelled", |
michael@0 | 5437 | this.listeners, this.wrapper) |
michael@0 | 5438 | return; |
michael@0 | 5439 | } |
michael@0 | 5440 | |
michael@0 | 5441 | // Find and cancel any pending installs for the same add-on in the same |
michael@0 | 5442 | // install location |
michael@0 | 5443 | for (let aInstall of XPIProvider.installs) { |
michael@0 | 5444 | if (aInstall.state == AddonManager.STATE_INSTALLED && |
michael@0 | 5445 | aInstall.installLocation == this.installLocation && |
michael@0 | 5446 | aInstall.addon.id == this.addon.id) { |
michael@0 | 5447 | logger.debug("Cancelling previous pending install of " + aInstall.addon.id); |
michael@0 | 5448 | aInstall.cancel(); |
michael@0 | 5449 | } |
michael@0 | 5450 | } |
michael@0 | 5451 | |
michael@0 | 5452 | let isUpgrade = this.existingAddon && |
michael@0 | 5453 | this.existingAddon._installLocation == this.installLocation; |
michael@0 | 5454 | let requiresRestart = XPIProvider.installRequiresRestart(this.addon); |
michael@0 | 5455 | |
michael@0 | 5456 | logger.debug("Starting install of " + this.addon.id + " from " + this.sourceURI.spec); |
michael@0 | 5457 | AddonManagerPrivate.callAddonListeners("onInstalling", |
michael@0 | 5458 | createWrapper(this.addon), |
michael@0 | 5459 | requiresRestart); |
michael@0 | 5460 | |
michael@0 | 5461 | let stagingDir = this.installLocation.getStagingDir(); |
michael@0 | 5462 | let stagedAddon = stagingDir.clone(); |
michael@0 | 5463 | |
michael@0 | 5464 | Task.spawn((function() { |
michael@0 | 5465 | let installedUnpacked = 0; |
michael@0 | 5466 | yield this.installLocation.requestStagingDir(); |
michael@0 | 5467 | |
michael@0 | 5468 | // First stage the file regardless of whether restarting is necessary |
michael@0 | 5469 | if (this.addon.unpack || Prefs.getBoolPref(PREF_XPI_UNPACK, false)) { |
michael@0 | 5470 | logger.debug("Addon " + this.addon.id + " will be installed as " + |
michael@0 | 5471 | "an unpacked directory"); |
michael@0 | 5472 | stagedAddon.append(this.addon.id); |
michael@0 | 5473 | yield removeAsync(stagedAddon); |
michael@0 | 5474 | yield OS.File.makeDir(stagedAddon.path); |
michael@0 | 5475 | yield ZipUtils.extractFilesAsync(this.file, stagedAddon); |
michael@0 | 5476 | installedUnpacked = 1; |
michael@0 | 5477 | } |
michael@0 | 5478 | else { |
michael@0 | 5479 | logger.debug("Addon " + this.addon.id + " will be installed as " + |
michael@0 | 5480 | "a packed xpi"); |
michael@0 | 5481 | stagedAddon.append(this.addon.id + ".xpi"); |
michael@0 | 5482 | yield removeAsync(stagedAddon); |
michael@0 | 5483 | yield OS.File.copy(this.file.path, stagedAddon.path); |
michael@0 | 5484 | } |
michael@0 | 5485 | |
michael@0 | 5486 | if (requiresRestart) { |
michael@0 | 5487 | // Point the add-on to its extracted files as the xpi may get deleted |
michael@0 | 5488 | this.addon._sourceBundle = stagedAddon; |
michael@0 | 5489 | |
michael@0 | 5490 | // Cache the AddonInternal as it may have updated compatibility info |
michael@0 | 5491 | let stagedJSON = stagedAddon.clone(); |
michael@0 | 5492 | stagedJSON.leafName = this.addon.id + ".json"; |
michael@0 | 5493 | if (stagedJSON.exists()) |
michael@0 | 5494 | stagedJSON.remove(true); |
michael@0 | 5495 | let stream = Cc["@mozilla.org/network/file-output-stream;1"]. |
michael@0 | 5496 | createInstance(Ci.nsIFileOutputStream); |
michael@0 | 5497 | let converter = Cc["@mozilla.org/intl/converter-output-stream;1"]. |
michael@0 | 5498 | createInstance(Ci.nsIConverterOutputStream); |
michael@0 | 5499 | |
michael@0 | 5500 | try { |
michael@0 | 5501 | stream.init(stagedJSON, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | |
michael@0 | 5502 | FileUtils.MODE_TRUNCATE, FileUtils.PERMS_FILE, |
michael@0 | 5503 | 0); |
michael@0 | 5504 | converter.init(stream, "UTF-8", 0, 0x0000); |
michael@0 | 5505 | converter.writeString(JSON.stringify(this.addon)); |
michael@0 | 5506 | } |
michael@0 | 5507 | finally { |
michael@0 | 5508 | converter.close(); |
michael@0 | 5509 | stream.close(); |
michael@0 | 5510 | } |
michael@0 | 5511 | |
michael@0 | 5512 | logger.debug("Staged install of " + this.addon.id + " from " + this.sourceURI.spec + " ready; waiting for restart."); |
michael@0 | 5513 | this.state = AddonManager.STATE_INSTALLED; |
michael@0 | 5514 | if (isUpgrade) { |
michael@0 | 5515 | delete this.existingAddon.pendingUpgrade; |
michael@0 | 5516 | this.existingAddon.pendingUpgrade = this.addon; |
michael@0 | 5517 | } |
michael@0 | 5518 | AddonManagerPrivate.callInstallListeners("onInstallEnded", |
michael@0 | 5519 | this.listeners, this.wrapper, |
michael@0 | 5520 | createWrapper(this.addon)); |
michael@0 | 5521 | } |
michael@0 | 5522 | else { |
michael@0 | 5523 | // The install is completed so it should be removed from the active list |
michael@0 | 5524 | XPIProvider.removeActiveInstall(this); |
michael@0 | 5525 | |
michael@0 | 5526 | // TODO We can probably reduce the number of DB operations going on here |
michael@0 | 5527 | // We probably also want to support rolling back failed upgrades etc. |
michael@0 | 5528 | // See bug 553015. |
michael@0 | 5529 | |
michael@0 | 5530 | // Deactivate and remove the old add-on as necessary |
michael@0 | 5531 | let reason = BOOTSTRAP_REASONS.ADDON_INSTALL; |
michael@0 | 5532 | if (this.existingAddon) { |
michael@0 | 5533 | if (Services.vc.compare(this.existingAddon.version, this.addon.version) < 0) |
michael@0 | 5534 | reason = BOOTSTRAP_REASONS.ADDON_UPGRADE; |
michael@0 | 5535 | else |
michael@0 | 5536 | reason = BOOTSTRAP_REASONS.ADDON_DOWNGRADE; |
michael@0 | 5537 | |
michael@0 | 5538 | if (this.existingAddon.bootstrap) { |
michael@0 | 5539 | let file = this.existingAddon._installLocation |
michael@0 | 5540 | .getLocationForID(this.existingAddon.id); |
michael@0 | 5541 | if (this.existingAddon.active) { |
michael@0 | 5542 | XPIProvider.callBootstrapMethod(this.existingAddon.id, |
michael@0 | 5543 | this.existingAddon.version, |
michael@0 | 5544 | this.existingAddon.type, file, |
michael@0 | 5545 | "shutdown", reason, |
michael@0 | 5546 | { newVersion: this.addon.version }); |
michael@0 | 5547 | } |
michael@0 | 5548 | |
michael@0 | 5549 | XPIProvider.callBootstrapMethod(this.existingAddon.id, |
michael@0 | 5550 | this.existingAddon.version, |
michael@0 | 5551 | this.existingAddon.type, file, |
michael@0 | 5552 | "uninstall", reason, |
michael@0 | 5553 | { newVersion: this.addon.version }); |
michael@0 | 5554 | XPIProvider.unloadBootstrapScope(this.existingAddon.id); |
michael@0 | 5555 | flushStartupCache(); |
michael@0 | 5556 | } |
michael@0 | 5557 | |
michael@0 | 5558 | if (!isUpgrade && this.existingAddon.active) { |
michael@0 | 5559 | XPIDatabase.updateAddonActive(this.existingAddon, false); |
michael@0 | 5560 | } |
michael@0 | 5561 | } |
michael@0 | 5562 | |
michael@0 | 5563 | // Install the new add-on into its final location |
michael@0 | 5564 | let existingAddonID = this.existingAddon ? this.existingAddon.id : null; |
michael@0 | 5565 | let file = this.installLocation.installAddon(this.addon.id, stagedAddon, |
michael@0 | 5566 | existingAddonID); |
michael@0 | 5567 | |
michael@0 | 5568 | // Update the metadata in the database |
michael@0 | 5569 | this.addon._sourceBundle = file; |
michael@0 | 5570 | this.addon._installLocation = this.installLocation; |
michael@0 | 5571 | let scanStarted = Date.now(); |
michael@0 | 5572 | let [, mTime, scanItems] = recursiveLastModifiedTime(file); |
michael@0 | 5573 | let scanTime = Date.now() - scanStarted; |
michael@0 | 5574 | this.addon.updateDate = mTime; |
michael@0 | 5575 | this.addon.visible = true; |
michael@0 | 5576 | if (isUpgrade) { |
michael@0 | 5577 | this.addon = XPIDatabase.updateAddonMetadata(this.existingAddon, this.addon, |
michael@0 | 5578 | file.persistentDescriptor); |
michael@0 | 5579 | } |
michael@0 | 5580 | else { |
michael@0 | 5581 | this.addon.installDate = this.addon.updateDate; |
michael@0 | 5582 | this.addon.active = (this.addon.visible && !isAddonDisabled(this.addon)) |
michael@0 | 5583 | this.addon = XPIDatabase.addAddonMetadata(this.addon, file.persistentDescriptor); |
michael@0 | 5584 | } |
michael@0 | 5585 | |
michael@0 | 5586 | let extraParams = {}; |
michael@0 | 5587 | if (this.existingAddon) { |
michael@0 | 5588 | extraParams.oldVersion = this.existingAddon.version; |
michael@0 | 5589 | } |
michael@0 | 5590 | |
michael@0 | 5591 | if (this.addon.bootstrap) { |
michael@0 | 5592 | XPIProvider.callBootstrapMethod(this.addon.id, this.addon.version, |
michael@0 | 5593 | this.addon.type, file, "install", |
michael@0 | 5594 | reason, extraParams); |
michael@0 | 5595 | } |
michael@0 | 5596 | |
michael@0 | 5597 | AddonManagerPrivate.callAddonListeners("onInstalled", |
michael@0 | 5598 | createWrapper(this.addon)); |
michael@0 | 5599 | |
michael@0 | 5600 | logger.debug("Install of " + this.sourceURI.spec + " completed."); |
michael@0 | 5601 | this.state = AddonManager.STATE_INSTALLED; |
michael@0 | 5602 | AddonManagerPrivate.callInstallListeners("onInstallEnded", |
michael@0 | 5603 | this.listeners, this.wrapper, |
michael@0 | 5604 | createWrapper(this.addon)); |
michael@0 | 5605 | |
michael@0 | 5606 | if (this.addon.bootstrap) { |
michael@0 | 5607 | if (this.addon.active) { |
michael@0 | 5608 | XPIProvider.callBootstrapMethod(this.addon.id, this.addon.version, |
michael@0 | 5609 | this.addon.type, file, "startup", |
michael@0 | 5610 | reason, extraParams); |
michael@0 | 5611 | } |
michael@0 | 5612 | else { |
michael@0 | 5613 | // XXX this makes it dangerous to do some things in onInstallEnded |
michael@0 | 5614 | // listeners because important cleanup hasn't been done yet |
michael@0 | 5615 | XPIProvider.unloadBootstrapScope(this.addon.id); |
michael@0 | 5616 | } |
michael@0 | 5617 | } |
michael@0 | 5618 | XPIProvider.setTelemetry(this.addon.id, "unpacked", installedUnpacked); |
michael@0 | 5619 | XPIProvider.setTelemetry(this.addon.id, "location", this.installLocation.name); |
michael@0 | 5620 | XPIProvider.setTelemetry(this.addon.id, "scan_MS", scanTime); |
michael@0 | 5621 | XPIProvider.setTelemetry(this.addon.id, "scan_items", scanItems); |
michael@0 | 5622 | let loc = this.addon.defaultLocale; |
michael@0 | 5623 | if (loc) { |
michael@0 | 5624 | XPIProvider.setTelemetry(this.addon.id, "name", loc.name); |
michael@0 | 5625 | XPIProvider.setTelemetry(this.addon.id, "creator", loc.creator); |
michael@0 | 5626 | } |
michael@0 | 5627 | } |
michael@0 | 5628 | }).bind(this)).then(null, (e) => { |
michael@0 | 5629 | logger.warn("Failed to install " + this.file.path + " from " + this.sourceURI.spec, e); |
michael@0 | 5630 | if (stagedAddon.exists()) |
michael@0 | 5631 | recursiveRemove(stagedAddon); |
michael@0 | 5632 | this.state = AddonManager.STATE_INSTALL_FAILED; |
michael@0 | 5633 | this.error = AddonManager.ERROR_FILE_ACCESS; |
michael@0 | 5634 | XPIProvider.removeActiveInstall(this); |
michael@0 | 5635 | AddonManagerPrivate.callAddonListeners("onOperationCancelled", |
michael@0 | 5636 | createWrapper(this.addon)); |
michael@0 | 5637 | AddonManagerPrivate.callInstallListeners("onInstallFailed", |
michael@0 | 5638 | this.listeners, |
michael@0 | 5639 | this.wrapper); |
michael@0 | 5640 | }).then(() => { |
michael@0 | 5641 | this.removeTemporaryFile(); |
michael@0 | 5642 | return this.installLocation.releaseStagingDir(); |
michael@0 | 5643 | }); |
michael@0 | 5644 | }, |
michael@0 | 5645 | |
michael@0 | 5646 | getInterface: function AI_getInterface(iid) { |
michael@0 | 5647 | if (iid.equals(Ci.nsIAuthPrompt2)) { |
michael@0 | 5648 | var factory = Cc["@mozilla.org/prompter;1"]. |
michael@0 | 5649 | getService(Ci.nsIPromptFactory); |
michael@0 | 5650 | return factory.getPrompt(this.window, Ci.nsIAuthPrompt); |
michael@0 | 5651 | } |
michael@0 | 5652 | else if (iid.equals(Ci.nsIChannelEventSink)) { |
michael@0 | 5653 | return this; |
michael@0 | 5654 | } |
michael@0 | 5655 | |
michael@0 | 5656 | return this.badCertHandler.getInterface(iid); |
michael@0 | 5657 | } |
michael@0 | 5658 | } |
michael@0 | 5659 | |
michael@0 | 5660 | /** |
michael@0 | 5661 | * Creates a new AddonInstall for an already staged install. Used when |
michael@0 | 5662 | * installing the staged install failed for some reason. |
michael@0 | 5663 | * |
michael@0 | 5664 | * @param aDir |
michael@0 | 5665 | * The directory holding the staged install |
michael@0 | 5666 | * @param aManifest |
michael@0 | 5667 | * The cached manifest for the install |
michael@0 | 5668 | */ |
michael@0 | 5669 | AddonInstall.createStagedInstall = function AI_createStagedInstall(aInstallLocation, aDir, aManifest) { |
michael@0 | 5670 | let url = Services.io.newFileURI(aDir); |
michael@0 | 5671 | |
michael@0 | 5672 | let install = new AddonInstall(aInstallLocation, aDir); |
michael@0 | 5673 | install.initStagedInstall(aManifest); |
michael@0 | 5674 | }; |
michael@0 | 5675 | |
michael@0 | 5676 | /** |
michael@0 | 5677 | * Creates a new AddonInstall to install an add-on from a local file. Installs |
michael@0 | 5678 | * always go into the profile install location. |
michael@0 | 5679 | * |
michael@0 | 5680 | * @param aCallback |
michael@0 | 5681 | * The callback to pass the new AddonInstall to |
michael@0 | 5682 | * @param aFile |
michael@0 | 5683 | * The file to install |
michael@0 | 5684 | */ |
michael@0 | 5685 | AddonInstall.createInstall = function AI_createInstall(aCallback, aFile) { |
michael@0 | 5686 | let location = XPIProvider.installLocationsByName[KEY_APP_PROFILE]; |
michael@0 | 5687 | let url = Services.io.newFileURI(aFile); |
michael@0 | 5688 | |
michael@0 | 5689 | try { |
michael@0 | 5690 | let install = new AddonInstall(location, url); |
michael@0 | 5691 | install.initLocalInstall(aCallback); |
michael@0 | 5692 | } |
michael@0 | 5693 | catch(e) { |
michael@0 | 5694 | logger.error("Error creating install", e); |
michael@0 | 5695 | makeSafe(aCallback)(null); |
michael@0 | 5696 | } |
michael@0 | 5697 | }; |
michael@0 | 5698 | |
michael@0 | 5699 | /** |
michael@0 | 5700 | * Creates a new AddonInstall to download and install a URL. |
michael@0 | 5701 | * |
michael@0 | 5702 | * @param aCallback |
michael@0 | 5703 | * The callback to pass the new AddonInstall to |
michael@0 | 5704 | * @param aUri |
michael@0 | 5705 | * The URI to download |
michael@0 | 5706 | * @param aHash |
michael@0 | 5707 | * A hash for the add-on |
michael@0 | 5708 | * @param aName |
michael@0 | 5709 | * A name for the add-on |
michael@0 | 5710 | * @param aIcons |
michael@0 | 5711 | * An icon URLs for the add-on |
michael@0 | 5712 | * @param aVersion |
michael@0 | 5713 | * A version for the add-on |
michael@0 | 5714 | * @param aLoadGroup |
michael@0 | 5715 | * An nsILoadGroup to associate the download with |
michael@0 | 5716 | */ |
michael@0 | 5717 | AddonInstall.createDownload = function AI_createDownload(aCallback, aUri, aHash, aName, aIcons, |
michael@0 | 5718 | aVersion, aLoadGroup) { |
michael@0 | 5719 | let location = XPIProvider.installLocationsByName[KEY_APP_PROFILE]; |
michael@0 | 5720 | let url = NetUtil.newURI(aUri); |
michael@0 | 5721 | |
michael@0 | 5722 | let install = new AddonInstall(location, url, aHash, null, null, aLoadGroup); |
michael@0 | 5723 | if (url instanceof Ci.nsIFileURL) |
michael@0 | 5724 | install.initLocalInstall(aCallback); |
michael@0 | 5725 | else |
michael@0 | 5726 | install.initAvailableDownload(aName, null, aIcons, aVersion, aCallback); |
michael@0 | 5727 | }; |
michael@0 | 5728 | |
michael@0 | 5729 | /** |
michael@0 | 5730 | * Creates a new AddonInstall for an update. |
michael@0 | 5731 | * |
michael@0 | 5732 | * @param aCallback |
michael@0 | 5733 | * The callback to pass the new AddonInstall to |
michael@0 | 5734 | * @param aAddon |
michael@0 | 5735 | * The add-on being updated |
michael@0 | 5736 | * @param aUpdate |
michael@0 | 5737 | * The metadata about the new version from the update manifest |
michael@0 | 5738 | */ |
michael@0 | 5739 | AddonInstall.createUpdate = function AI_createUpdate(aCallback, aAddon, aUpdate) { |
michael@0 | 5740 | let url = NetUtil.newURI(aUpdate.updateURL); |
michael@0 | 5741 | let releaseNotesURI = null; |
michael@0 | 5742 | try { |
michael@0 | 5743 | if (aUpdate.updateInfoURL) |
michael@0 | 5744 | releaseNotesURI = NetUtil.newURI(escapeAddonURI(aAddon, aUpdate.updateInfoURL)); |
michael@0 | 5745 | } |
michael@0 | 5746 | catch (e) { |
michael@0 | 5747 | // If the releaseNotesURI cannot be parsed then just ignore it. |
michael@0 | 5748 | } |
michael@0 | 5749 | |
michael@0 | 5750 | let install = new AddonInstall(aAddon._installLocation, url, |
michael@0 | 5751 | aUpdate.updateHash, releaseNotesURI, aAddon); |
michael@0 | 5752 | if (url instanceof Ci.nsIFileURL) { |
michael@0 | 5753 | install.initLocalInstall(aCallback); |
michael@0 | 5754 | } |
michael@0 | 5755 | else { |
michael@0 | 5756 | install.initAvailableDownload(aAddon.selectedLocale.name, aAddon.type, |
michael@0 | 5757 | aAddon.icons, aUpdate.version, aCallback); |
michael@0 | 5758 | } |
michael@0 | 5759 | }; |
michael@0 | 5760 | |
michael@0 | 5761 | /** |
michael@0 | 5762 | * Creates a wrapper for an AddonInstall that only exposes the public API |
michael@0 | 5763 | * |
michael@0 | 5764 | * @param install |
michael@0 | 5765 | * The AddonInstall to create a wrapper for |
michael@0 | 5766 | */ |
michael@0 | 5767 | function AddonInstallWrapper(aInstall) { |
michael@0 | 5768 | #ifdef MOZ_EM_DEBUG |
michael@0 | 5769 | this.__defineGetter__("__AddonInstallInternal__", function AIW_debugGetter() { |
michael@0 | 5770 | return aInstall; |
michael@0 | 5771 | }); |
michael@0 | 5772 | #endif |
michael@0 | 5773 | |
michael@0 | 5774 | ["name", "type", "version", "icons", "releaseNotesURI", "file", "state", "error", |
michael@0 | 5775 | "progress", "maxProgress", "certificate", "certName"].forEach(function(aProp) { |
michael@0 | 5776 | this.__defineGetter__(aProp, function AIW_propertyGetter() aInstall[aProp]); |
michael@0 | 5777 | }, this); |
michael@0 | 5778 | |
michael@0 | 5779 | this.__defineGetter__("iconURL", function AIW_iconURL() aInstall.icons[32]); |
michael@0 | 5780 | |
michael@0 | 5781 | this.__defineGetter__("existingAddon", function AIW_existingAddonGetter() { |
michael@0 | 5782 | return createWrapper(aInstall.existingAddon); |
michael@0 | 5783 | }); |
michael@0 | 5784 | this.__defineGetter__("addon", function AIW_addonGetter() createWrapper(aInstall.addon)); |
michael@0 | 5785 | this.__defineGetter__("sourceURI", function AIW_sourceURIGetter() aInstall.sourceURI); |
michael@0 | 5786 | |
michael@0 | 5787 | this.__defineGetter__("linkedInstalls", function AIW_linkedInstallsGetter() { |
michael@0 | 5788 | if (!aInstall.linkedInstalls) |
michael@0 | 5789 | return null; |
michael@0 | 5790 | return [i.wrapper for each (i in aInstall.linkedInstalls)]; |
michael@0 | 5791 | }); |
michael@0 | 5792 | |
michael@0 | 5793 | this.install = function AIW_install() { |
michael@0 | 5794 | aInstall.install(); |
michael@0 | 5795 | } |
michael@0 | 5796 | |
michael@0 | 5797 | this.cancel = function AIW_cancel() { |
michael@0 | 5798 | aInstall.cancel(); |
michael@0 | 5799 | } |
michael@0 | 5800 | |
michael@0 | 5801 | this.addListener = function AIW_addListener(listener) { |
michael@0 | 5802 | aInstall.addListener(listener); |
michael@0 | 5803 | } |
michael@0 | 5804 | |
michael@0 | 5805 | this.removeListener = function AIW_removeListener(listener) { |
michael@0 | 5806 | aInstall.removeListener(listener); |
michael@0 | 5807 | } |
michael@0 | 5808 | } |
michael@0 | 5809 | |
michael@0 | 5810 | AddonInstallWrapper.prototype = {}; |
michael@0 | 5811 | |
michael@0 | 5812 | /** |
michael@0 | 5813 | * Creates a new update checker. |
michael@0 | 5814 | * |
michael@0 | 5815 | * @param aAddon |
michael@0 | 5816 | * The add-on to check for updates |
michael@0 | 5817 | * @param aListener |
michael@0 | 5818 | * An UpdateListener to notify of updates |
michael@0 | 5819 | * @param aReason |
michael@0 | 5820 | * The reason for the update check |
michael@0 | 5821 | * @param aAppVersion |
michael@0 | 5822 | * An optional application version to check for updates for |
michael@0 | 5823 | * @param aPlatformVersion |
michael@0 | 5824 | * An optional platform version to check for updates for |
michael@0 | 5825 | * @throws if the aListener or aReason arguments are not valid |
michael@0 | 5826 | */ |
michael@0 | 5827 | function UpdateChecker(aAddon, aListener, aReason, aAppVersion, aPlatformVersion) { |
michael@0 | 5828 | if (!aListener || !aReason) |
michael@0 | 5829 | throw Cr.NS_ERROR_INVALID_ARG; |
michael@0 | 5830 | |
michael@0 | 5831 | Components.utils.import("resource://gre/modules/addons/AddonUpdateChecker.jsm"); |
michael@0 | 5832 | |
michael@0 | 5833 | this.addon = aAddon; |
michael@0 | 5834 | aAddon._updateCheck = this; |
michael@0 | 5835 | XPIProvider.doing(this); |
michael@0 | 5836 | this.listener = aListener; |
michael@0 | 5837 | this.appVersion = aAppVersion; |
michael@0 | 5838 | this.platformVersion = aPlatformVersion; |
michael@0 | 5839 | this.syncCompatibility = (aReason == AddonManager.UPDATE_WHEN_NEW_APP_INSTALLED); |
michael@0 | 5840 | |
michael@0 | 5841 | let updateURL = aAddon.updateURL; |
michael@0 | 5842 | if (!updateURL) { |
michael@0 | 5843 | if (aReason == AddonManager.UPDATE_WHEN_PERIODIC_UPDATE && |
michael@0 | 5844 | Services.prefs.getPrefType(PREF_EM_UPDATE_BACKGROUND_URL) == Services.prefs.PREF_STRING) { |
michael@0 | 5845 | updateURL = Services.prefs.getCharPref(PREF_EM_UPDATE_BACKGROUND_URL); |
michael@0 | 5846 | } else { |
michael@0 | 5847 | updateURL = Services.prefs.getCharPref(PREF_EM_UPDATE_URL); |
michael@0 | 5848 | } |
michael@0 | 5849 | } |
michael@0 | 5850 | |
michael@0 | 5851 | const UPDATE_TYPE_COMPATIBILITY = 32; |
michael@0 | 5852 | const UPDATE_TYPE_NEWVERSION = 64; |
michael@0 | 5853 | |
michael@0 | 5854 | aReason |= UPDATE_TYPE_COMPATIBILITY; |
michael@0 | 5855 | if ("onUpdateAvailable" in this.listener) |
michael@0 | 5856 | aReason |= UPDATE_TYPE_NEWVERSION; |
michael@0 | 5857 | |
michael@0 | 5858 | let url = escapeAddonURI(aAddon, updateURL, aReason, aAppVersion); |
michael@0 | 5859 | this._parser = AddonUpdateChecker.checkForUpdates(aAddon.id, aAddon.updateKey, |
michael@0 | 5860 | url, this); |
michael@0 | 5861 | } |
michael@0 | 5862 | |
michael@0 | 5863 | UpdateChecker.prototype = { |
michael@0 | 5864 | addon: null, |
michael@0 | 5865 | listener: null, |
michael@0 | 5866 | appVersion: null, |
michael@0 | 5867 | platformVersion: null, |
michael@0 | 5868 | syncCompatibility: null, |
michael@0 | 5869 | |
michael@0 | 5870 | /** |
michael@0 | 5871 | * Calls a method on the listener passing any number of arguments and |
michael@0 | 5872 | * consuming any exceptions. |
michael@0 | 5873 | * |
michael@0 | 5874 | * @param aMethod |
michael@0 | 5875 | * The method to call on the listener |
michael@0 | 5876 | */ |
michael@0 | 5877 | callListener: function UC_callListener(aMethod, ...aArgs) { |
michael@0 | 5878 | if (!(aMethod in this.listener)) |
michael@0 | 5879 | return; |
michael@0 | 5880 | |
michael@0 | 5881 | try { |
michael@0 | 5882 | this.listener[aMethod].apply(this.listener, aArgs); |
michael@0 | 5883 | } |
michael@0 | 5884 | catch (e) { |
michael@0 | 5885 | logger.warn("Exception calling UpdateListener method " + aMethod, e); |
michael@0 | 5886 | } |
michael@0 | 5887 | }, |
michael@0 | 5888 | |
michael@0 | 5889 | /** |
michael@0 | 5890 | * Called when AddonUpdateChecker completes the update check |
michael@0 | 5891 | * |
michael@0 | 5892 | * @param updates |
michael@0 | 5893 | * The list of update details for the add-on |
michael@0 | 5894 | */ |
michael@0 | 5895 | onUpdateCheckComplete: function UC_onUpdateCheckComplete(aUpdates) { |
michael@0 | 5896 | XPIProvider.done(this.addon._updateCheck); |
michael@0 | 5897 | this.addon._updateCheck = null; |
michael@0 | 5898 | let AUC = AddonUpdateChecker; |
michael@0 | 5899 | |
michael@0 | 5900 | let ignoreMaxVersion = false; |
michael@0 | 5901 | let ignoreStrictCompat = false; |
michael@0 | 5902 | if (!AddonManager.checkCompatibility) { |
michael@0 | 5903 | ignoreMaxVersion = true; |
michael@0 | 5904 | ignoreStrictCompat = true; |
michael@0 | 5905 | } else if (this.addon.type in COMPATIBLE_BY_DEFAULT_TYPES && |
michael@0 | 5906 | !AddonManager.strictCompatibility && |
michael@0 | 5907 | !this.addon.strictCompatibility && |
michael@0 | 5908 | !this.addon.hasBinaryComponents) { |
michael@0 | 5909 | ignoreMaxVersion = true; |
michael@0 | 5910 | } |
michael@0 | 5911 | |
michael@0 | 5912 | // Always apply any compatibility update for the current version |
michael@0 | 5913 | let compatUpdate = AUC.getCompatibilityUpdate(aUpdates, this.addon.version, |
michael@0 | 5914 | this.syncCompatibility, |
michael@0 | 5915 | null, null, |
michael@0 | 5916 | ignoreMaxVersion, |
michael@0 | 5917 | ignoreStrictCompat); |
michael@0 | 5918 | // Apply the compatibility update to the database |
michael@0 | 5919 | if (compatUpdate) |
michael@0 | 5920 | this.addon.applyCompatibilityUpdate(compatUpdate, this.syncCompatibility); |
michael@0 | 5921 | |
michael@0 | 5922 | // If the request is for an application or platform version that is |
michael@0 | 5923 | // different to the current application or platform version then look for a |
michael@0 | 5924 | // compatibility update for those versions. |
michael@0 | 5925 | if ((this.appVersion && |
michael@0 | 5926 | Services.vc.compare(this.appVersion, Services.appinfo.version) != 0) || |
michael@0 | 5927 | (this.platformVersion && |
michael@0 | 5928 | Services.vc.compare(this.platformVersion, Services.appinfo.platformVersion) != 0)) { |
michael@0 | 5929 | compatUpdate = AUC.getCompatibilityUpdate(aUpdates, this.addon.version, |
michael@0 | 5930 | false, this.appVersion, |
michael@0 | 5931 | this.platformVersion, |
michael@0 | 5932 | ignoreMaxVersion, |
michael@0 | 5933 | ignoreStrictCompat); |
michael@0 | 5934 | } |
michael@0 | 5935 | |
michael@0 | 5936 | if (compatUpdate) |
michael@0 | 5937 | this.callListener("onCompatibilityUpdateAvailable", createWrapper(this.addon)); |
michael@0 | 5938 | else |
michael@0 | 5939 | this.callListener("onNoCompatibilityUpdateAvailable", createWrapper(this.addon)); |
michael@0 | 5940 | |
michael@0 | 5941 | function sendUpdateAvailableMessages(aSelf, aInstall) { |
michael@0 | 5942 | if (aInstall) { |
michael@0 | 5943 | aSelf.callListener("onUpdateAvailable", createWrapper(aSelf.addon), |
michael@0 | 5944 | aInstall.wrapper); |
michael@0 | 5945 | } |
michael@0 | 5946 | else { |
michael@0 | 5947 | aSelf.callListener("onNoUpdateAvailable", createWrapper(aSelf.addon)); |
michael@0 | 5948 | } |
michael@0 | 5949 | aSelf.callListener("onUpdateFinished", createWrapper(aSelf.addon), |
michael@0 | 5950 | AddonManager.UPDATE_STATUS_NO_ERROR); |
michael@0 | 5951 | } |
michael@0 | 5952 | |
michael@0 | 5953 | let compatOverrides = AddonManager.strictCompatibility ? |
michael@0 | 5954 | null : |
michael@0 | 5955 | this.addon.compatibilityOverrides; |
michael@0 | 5956 | |
michael@0 | 5957 | let update = AUC.getNewestCompatibleUpdate(aUpdates, |
michael@0 | 5958 | this.appVersion, |
michael@0 | 5959 | this.platformVersion, |
michael@0 | 5960 | ignoreMaxVersion, |
michael@0 | 5961 | ignoreStrictCompat, |
michael@0 | 5962 | compatOverrides); |
michael@0 | 5963 | |
michael@0 | 5964 | if (update && Services.vc.compare(this.addon.version, update.version) < 0) { |
michael@0 | 5965 | for (let currentInstall of XPIProvider.installs) { |
michael@0 | 5966 | // Skip installs that don't match the available update |
michael@0 | 5967 | if (currentInstall.existingAddon != this.addon || |
michael@0 | 5968 | currentInstall.version != update.version) |
michael@0 | 5969 | continue; |
michael@0 | 5970 | |
michael@0 | 5971 | // If the existing install has not yet started downloading then send an |
michael@0 | 5972 | // available update notification. If it is already downloading then |
michael@0 | 5973 | // don't send any available update notification |
michael@0 | 5974 | if (currentInstall.state == AddonManager.STATE_AVAILABLE) { |
michael@0 | 5975 | logger.debug("Found an existing AddonInstall for " + this.addon.id); |
michael@0 | 5976 | sendUpdateAvailableMessages(this, currentInstall); |
michael@0 | 5977 | } |
michael@0 | 5978 | else |
michael@0 | 5979 | sendUpdateAvailableMessages(this, null); |
michael@0 | 5980 | return; |
michael@0 | 5981 | } |
michael@0 | 5982 | |
michael@0 | 5983 | let self = this; |
michael@0 | 5984 | AddonInstall.createUpdate(function onUpdateCheckComplete_createUpdate(aInstall) { |
michael@0 | 5985 | sendUpdateAvailableMessages(self, aInstall); |
michael@0 | 5986 | }, this.addon, update); |
michael@0 | 5987 | } |
michael@0 | 5988 | else { |
michael@0 | 5989 | sendUpdateAvailableMessages(this, null); |
michael@0 | 5990 | } |
michael@0 | 5991 | }, |
michael@0 | 5992 | |
michael@0 | 5993 | /** |
michael@0 | 5994 | * Called when AddonUpdateChecker fails the update check |
michael@0 | 5995 | * |
michael@0 | 5996 | * @param aError |
michael@0 | 5997 | * An error status |
michael@0 | 5998 | */ |
michael@0 | 5999 | onUpdateCheckError: function UC_onUpdateCheckError(aError) { |
michael@0 | 6000 | XPIProvider.done(this.addon._updateCheck); |
michael@0 | 6001 | this.addon._updateCheck = null; |
michael@0 | 6002 | this.callListener("onNoCompatibilityUpdateAvailable", createWrapper(this.addon)); |
michael@0 | 6003 | this.callListener("onNoUpdateAvailable", createWrapper(this.addon)); |
michael@0 | 6004 | this.callListener("onUpdateFinished", createWrapper(this.addon), aError); |
michael@0 | 6005 | }, |
michael@0 | 6006 | |
michael@0 | 6007 | /** |
michael@0 | 6008 | * Called to cancel an in-progress update check |
michael@0 | 6009 | */ |
michael@0 | 6010 | cancel: function UC_cancel() { |
michael@0 | 6011 | let parser = this._parser; |
michael@0 | 6012 | if (parser) { |
michael@0 | 6013 | this._parser = null; |
michael@0 | 6014 | // This will call back to onUpdateCheckError with a CANCELLED error |
michael@0 | 6015 | parser.cancel(); |
michael@0 | 6016 | } |
michael@0 | 6017 | } |
michael@0 | 6018 | }; |
michael@0 | 6019 | |
michael@0 | 6020 | /** |
michael@0 | 6021 | * The AddonInternal is an internal only representation of add-ons. It may |
michael@0 | 6022 | * have come from the database (see DBAddonInternal in XPIProviderUtils.jsm) |
michael@0 | 6023 | * or an install manifest. |
michael@0 | 6024 | */ |
michael@0 | 6025 | function AddonInternal() { |
michael@0 | 6026 | } |
michael@0 | 6027 | |
michael@0 | 6028 | AddonInternal.prototype = { |
michael@0 | 6029 | _selectedLocale: null, |
michael@0 | 6030 | active: false, |
michael@0 | 6031 | visible: false, |
michael@0 | 6032 | userDisabled: false, |
michael@0 | 6033 | appDisabled: false, |
michael@0 | 6034 | softDisabled: false, |
michael@0 | 6035 | sourceURI: null, |
michael@0 | 6036 | releaseNotesURI: null, |
michael@0 | 6037 | foreignInstall: false, |
michael@0 | 6038 | |
michael@0 | 6039 | get selectedLocale() { |
michael@0 | 6040 | if (this._selectedLocale) |
michael@0 | 6041 | return this._selectedLocale; |
michael@0 | 6042 | let locale = findClosestLocale(this.locales); |
michael@0 | 6043 | this._selectedLocale = locale ? locale : this.defaultLocale; |
michael@0 | 6044 | return this._selectedLocale; |
michael@0 | 6045 | }, |
michael@0 | 6046 | |
michael@0 | 6047 | get providesUpdatesSecurely() { |
michael@0 | 6048 | return !!(this.updateKey || !this.updateURL || |
michael@0 | 6049 | this.updateURL.substring(0, 6) == "https:"); |
michael@0 | 6050 | }, |
michael@0 | 6051 | |
michael@0 | 6052 | get isCompatible() { |
michael@0 | 6053 | return this.isCompatibleWith(); |
michael@0 | 6054 | }, |
michael@0 | 6055 | |
michael@0 | 6056 | get isPlatformCompatible() { |
michael@0 | 6057 | if (this.targetPlatforms.length == 0) |
michael@0 | 6058 | return true; |
michael@0 | 6059 | |
michael@0 | 6060 | let matchedOS = false; |
michael@0 | 6061 | |
michael@0 | 6062 | // If any targetPlatform matches the OS and contains an ABI then we will |
michael@0 | 6063 | // only match a targetPlatform that contains both the current OS and ABI |
michael@0 | 6064 | let needsABI = false; |
michael@0 | 6065 | |
michael@0 | 6066 | // Some platforms do not specify an ABI, test against null in that case. |
michael@0 | 6067 | let abi = null; |
michael@0 | 6068 | try { |
michael@0 | 6069 | abi = Services.appinfo.XPCOMABI; |
michael@0 | 6070 | } |
michael@0 | 6071 | catch (e) { } |
michael@0 | 6072 | |
michael@0 | 6073 | for (let platform of this.targetPlatforms) { |
michael@0 | 6074 | if (platform.os == Services.appinfo.OS) { |
michael@0 | 6075 | if (platform.abi) { |
michael@0 | 6076 | needsABI = true; |
michael@0 | 6077 | if (platform.abi === abi) |
michael@0 | 6078 | return true; |
michael@0 | 6079 | } |
michael@0 | 6080 | else { |
michael@0 | 6081 | matchedOS = true; |
michael@0 | 6082 | } |
michael@0 | 6083 | } |
michael@0 | 6084 | } |
michael@0 | 6085 | |
michael@0 | 6086 | return matchedOS && !needsABI; |
michael@0 | 6087 | }, |
michael@0 | 6088 | |
michael@0 | 6089 | isCompatibleWith: function AddonInternal_isCompatibleWith(aAppVersion, aPlatformVersion) { |
michael@0 | 6090 | // Experiments are installed through an external mechanism that |
michael@0 | 6091 | // limits target audience to compatible clients. We trust it knows what |
michael@0 | 6092 | // it's doing and skip compatibility checks. |
michael@0 | 6093 | // |
michael@0 | 6094 | // This decision does forfeit defense in depth. If the experiments system |
michael@0 | 6095 | // is ever wrong about targeting an add-on to a specific application |
michael@0 | 6096 | // or platform, the client will likely see errors. |
michael@0 | 6097 | if (this.type == "experiment") { |
michael@0 | 6098 | return true; |
michael@0 | 6099 | } |
michael@0 | 6100 | |
michael@0 | 6101 | let app = this.matchingTargetApplication; |
michael@0 | 6102 | if (!app) |
michael@0 | 6103 | return false; |
michael@0 | 6104 | |
michael@0 | 6105 | if (!aAppVersion) |
michael@0 | 6106 | aAppVersion = Services.appinfo.version; |
michael@0 | 6107 | if (!aPlatformVersion) |
michael@0 | 6108 | aPlatformVersion = Services.appinfo.platformVersion; |
michael@0 | 6109 | |
michael@0 | 6110 | let version; |
michael@0 | 6111 | if (app.id == Services.appinfo.ID) |
michael@0 | 6112 | version = aAppVersion; |
michael@0 | 6113 | else if (app.id == TOOLKIT_ID) |
michael@0 | 6114 | version = aPlatformVersion |
michael@0 | 6115 | |
michael@0 | 6116 | // Only extensions and dictionaries can be compatible by default; themes |
michael@0 | 6117 | // and language packs always use strict compatibility checking. |
michael@0 | 6118 | if (this.type in COMPATIBLE_BY_DEFAULT_TYPES && |
michael@0 | 6119 | !AddonManager.strictCompatibility && !this.strictCompatibility && |
michael@0 | 6120 | !this.hasBinaryComponents) { |
michael@0 | 6121 | |
michael@0 | 6122 | // The repository can specify compatibility overrides. |
michael@0 | 6123 | // Note: For now, only blacklisting is supported by overrides. |
michael@0 | 6124 | if (this._repositoryAddon && |
michael@0 | 6125 | this._repositoryAddon.compatibilityOverrides) { |
michael@0 | 6126 | let overrides = this._repositoryAddon.compatibilityOverrides; |
michael@0 | 6127 | let override = AddonRepository.findMatchingCompatOverride(this.version, |
michael@0 | 6128 | overrides); |
michael@0 | 6129 | if (override && override.type == "incompatible") |
michael@0 | 6130 | return false; |
michael@0 | 6131 | } |
michael@0 | 6132 | |
michael@0 | 6133 | // Extremely old extensions should not be compatible by default. |
michael@0 | 6134 | let minCompatVersion; |
michael@0 | 6135 | if (app.id == Services.appinfo.ID) |
michael@0 | 6136 | minCompatVersion = XPIProvider.minCompatibleAppVersion; |
michael@0 | 6137 | else if (app.id == TOOLKIT_ID) |
michael@0 | 6138 | minCompatVersion = XPIProvider.minCompatiblePlatformVersion; |
michael@0 | 6139 | |
michael@0 | 6140 | if (minCompatVersion && |
michael@0 | 6141 | Services.vc.compare(minCompatVersion, app.maxVersion) > 0) |
michael@0 | 6142 | return false; |
michael@0 | 6143 | |
michael@0 | 6144 | return Services.vc.compare(version, app.minVersion) >= 0; |
michael@0 | 6145 | } |
michael@0 | 6146 | |
michael@0 | 6147 | return (Services.vc.compare(version, app.minVersion) >= 0) && |
michael@0 | 6148 | (Services.vc.compare(version, app.maxVersion) <= 0) |
michael@0 | 6149 | }, |
michael@0 | 6150 | |
michael@0 | 6151 | get matchingTargetApplication() { |
michael@0 | 6152 | let app = null; |
michael@0 | 6153 | for (let targetApp of this.targetApplications) { |
michael@0 | 6154 | if (targetApp.id == Services.appinfo.ID) |
michael@0 | 6155 | return targetApp; |
michael@0 | 6156 | if (targetApp.id == TOOLKIT_ID) |
michael@0 | 6157 | app = targetApp; |
michael@0 | 6158 | } |
michael@0 | 6159 | return app; |
michael@0 | 6160 | }, |
michael@0 | 6161 | |
michael@0 | 6162 | get blocklistState() { |
michael@0 | 6163 | let staticItem = findMatchingStaticBlocklistItem(this); |
michael@0 | 6164 | if (staticItem) |
michael@0 | 6165 | return staticItem.level; |
michael@0 | 6166 | |
michael@0 | 6167 | let bs = Cc["@mozilla.org/extensions/blocklist;1"]. |
michael@0 | 6168 | getService(Ci.nsIBlocklistService); |
michael@0 | 6169 | return bs.getAddonBlocklistState(createWrapper(this)); |
michael@0 | 6170 | }, |
michael@0 | 6171 | |
michael@0 | 6172 | get blocklistURL() { |
michael@0 | 6173 | let staticItem = findMatchingStaticBlocklistItem(this); |
michael@0 | 6174 | if (staticItem) { |
michael@0 | 6175 | let url = Services.urlFormatter.formatURLPref("extensions.blocklist.itemURL"); |
michael@0 | 6176 | return url.replace(/%blockID%/g, staticItem.blockID); |
michael@0 | 6177 | } |
michael@0 | 6178 | |
michael@0 | 6179 | let bs = Cc["@mozilla.org/extensions/blocklist;1"]. |
michael@0 | 6180 | getService(Ci.nsIBlocklistService); |
michael@0 | 6181 | return bs.getAddonBlocklistURL(createWrapper(this)); |
michael@0 | 6182 | }, |
michael@0 | 6183 | |
michael@0 | 6184 | applyCompatibilityUpdate: function AddonInternal_applyCompatibilityUpdate(aUpdate, aSyncCompatibility) { |
michael@0 | 6185 | this.targetApplications.forEach(function(aTargetApp) { |
michael@0 | 6186 | aUpdate.targetApplications.forEach(function(aUpdateTarget) { |
michael@0 | 6187 | if (aTargetApp.id == aUpdateTarget.id && (aSyncCompatibility || |
michael@0 | 6188 | Services.vc.compare(aTargetApp.maxVersion, aUpdateTarget.maxVersion) < 0)) { |
michael@0 | 6189 | aTargetApp.minVersion = aUpdateTarget.minVersion; |
michael@0 | 6190 | aTargetApp.maxVersion = aUpdateTarget.maxVersion; |
michael@0 | 6191 | } |
michael@0 | 6192 | }); |
michael@0 | 6193 | }); |
michael@0 | 6194 | this.appDisabled = !isUsableAddon(this); |
michael@0 | 6195 | }, |
michael@0 | 6196 | |
michael@0 | 6197 | /** |
michael@0 | 6198 | * toJSON is called by JSON.stringify in order to create a filtered version |
michael@0 | 6199 | * of this object to be serialized to a JSON file. A new object is returned |
michael@0 | 6200 | * with copies of all non-private properties. Functions, getters and setters |
michael@0 | 6201 | * are not copied. |
michael@0 | 6202 | * |
michael@0 | 6203 | * @param aKey |
michael@0 | 6204 | * The key that this object is being serialized as in the JSON. |
michael@0 | 6205 | * Unused here since this is always the main object serialized |
michael@0 | 6206 | * |
michael@0 | 6207 | * @return an object containing copies of the properties of this object |
michael@0 | 6208 | * ignoring private properties, functions, getters and setters |
michael@0 | 6209 | */ |
michael@0 | 6210 | toJSON: function AddonInternal_toJSON(aKey) { |
michael@0 | 6211 | let obj = {}; |
michael@0 | 6212 | for (let prop in this) { |
michael@0 | 6213 | // Ignore private properties |
michael@0 | 6214 | if (prop.substring(0, 1) == "_") |
michael@0 | 6215 | continue; |
michael@0 | 6216 | |
michael@0 | 6217 | // Ignore getters |
michael@0 | 6218 | if (this.__lookupGetter__(prop)) |
michael@0 | 6219 | continue; |
michael@0 | 6220 | |
michael@0 | 6221 | // Ignore setters |
michael@0 | 6222 | if (this.__lookupSetter__(prop)) |
michael@0 | 6223 | continue; |
michael@0 | 6224 | |
michael@0 | 6225 | // Ignore functions |
michael@0 | 6226 | if (typeof this[prop] == "function") |
michael@0 | 6227 | continue; |
michael@0 | 6228 | |
michael@0 | 6229 | obj[prop] = this[prop]; |
michael@0 | 6230 | } |
michael@0 | 6231 | |
michael@0 | 6232 | return obj; |
michael@0 | 6233 | }, |
michael@0 | 6234 | |
michael@0 | 6235 | /** |
michael@0 | 6236 | * When an add-on install is pending its metadata will be cached in a file. |
michael@0 | 6237 | * This method reads particular properties of that metadata that may be newer |
michael@0 | 6238 | * than that in the install manifest, like compatibility information. |
michael@0 | 6239 | * |
michael@0 | 6240 | * @param aObj |
michael@0 | 6241 | * A JS object containing the cached metadata |
michael@0 | 6242 | */ |
michael@0 | 6243 | importMetadata: function AddonInternal_importMetaData(aObj) { |
michael@0 | 6244 | PENDING_INSTALL_METADATA.forEach(function(aProp) { |
michael@0 | 6245 | if (!(aProp in aObj)) |
michael@0 | 6246 | return; |
michael@0 | 6247 | |
michael@0 | 6248 | this[aProp] = aObj[aProp]; |
michael@0 | 6249 | }, this); |
michael@0 | 6250 | |
michael@0 | 6251 | // Compatibility info may have changed so update appDisabled |
michael@0 | 6252 | this.appDisabled = !isUsableAddon(this); |
michael@0 | 6253 | } |
michael@0 | 6254 | }; |
michael@0 | 6255 | |
michael@0 | 6256 | /** |
michael@0 | 6257 | * Creates an AddonWrapper for an AddonInternal. |
michael@0 | 6258 | * |
michael@0 | 6259 | * @param addon |
michael@0 | 6260 | * The AddonInternal to wrap |
michael@0 | 6261 | * @return an AddonWrapper or null if addon was null |
michael@0 | 6262 | */ |
michael@0 | 6263 | function createWrapper(aAddon) { |
michael@0 | 6264 | if (!aAddon) |
michael@0 | 6265 | return null; |
michael@0 | 6266 | if (!aAddon._wrapper) { |
michael@0 | 6267 | aAddon._hasResourceCache = new Map(); |
michael@0 | 6268 | aAddon._wrapper = new AddonWrapper(aAddon); |
michael@0 | 6269 | } |
michael@0 | 6270 | return aAddon._wrapper; |
michael@0 | 6271 | } |
michael@0 | 6272 | |
michael@0 | 6273 | /** |
michael@0 | 6274 | * The AddonWrapper wraps an Addon to provide the data visible to consumers of |
michael@0 | 6275 | * the public API. |
michael@0 | 6276 | */ |
michael@0 | 6277 | function AddonWrapper(aAddon) { |
michael@0 | 6278 | #ifdef MOZ_EM_DEBUG |
michael@0 | 6279 | this.__defineGetter__("__AddonInternal__", function AW_debugGetter() { |
michael@0 | 6280 | return aAddon; |
michael@0 | 6281 | }); |
michael@0 | 6282 | #endif |
michael@0 | 6283 | |
michael@0 | 6284 | function chooseValue(aObj, aProp) { |
michael@0 | 6285 | let repositoryAddon = aAddon._repositoryAddon; |
michael@0 | 6286 | let objValue = aObj[aProp]; |
michael@0 | 6287 | |
michael@0 | 6288 | if (repositoryAddon && (aProp in repositoryAddon) && |
michael@0 | 6289 | (objValue === undefined || objValue === null)) { |
michael@0 | 6290 | return [repositoryAddon[aProp], true]; |
michael@0 | 6291 | } |
michael@0 | 6292 | |
michael@0 | 6293 | return [objValue, false]; |
michael@0 | 6294 | } |
michael@0 | 6295 | |
michael@0 | 6296 | ["id", "syncGUID", "version", "type", "isCompatible", "isPlatformCompatible", |
michael@0 | 6297 | "providesUpdatesSecurely", "blocklistState", "blocklistURL", "appDisabled", |
michael@0 | 6298 | "softDisabled", "skinnable", "size", "foreignInstall", "hasBinaryComponents", |
michael@0 | 6299 | "strictCompatibility", "compatibilityOverrides", "updateURL"].forEach(function(aProp) { |
michael@0 | 6300 | this.__defineGetter__(aProp, function AddonWrapper_propertyGetter() aAddon[aProp]); |
michael@0 | 6301 | }, this); |
michael@0 | 6302 | |
michael@0 | 6303 | ["fullDescription", "developerComments", "eula", "supportURL", |
michael@0 | 6304 | "contributionURL", "contributionAmount", "averageRating", "reviewCount", |
michael@0 | 6305 | "reviewURL", "totalDownloads", "weeklyDownloads", "dailyUsers", |
michael@0 | 6306 | "repositoryStatus"].forEach(function(aProp) { |
michael@0 | 6307 | this.__defineGetter__(aProp, function AddonWrapper_repoPropertyGetter() { |
michael@0 | 6308 | if (aAddon._repositoryAddon) |
michael@0 | 6309 | return aAddon._repositoryAddon[aProp]; |
michael@0 | 6310 | |
michael@0 | 6311 | return null; |
michael@0 | 6312 | }); |
michael@0 | 6313 | }, this); |
michael@0 | 6314 | |
michael@0 | 6315 | this.__defineGetter__("aboutURL", function AddonWrapper_aboutURLGetter() { |
michael@0 | 6316 | return this.isActive ? aAddon["aboutURL"] : null; |
michael@0 | 6317 | }); |
michael@0 | 6318 | |
michael@0 | 6319 | ["installDate", "updateDate"].forEach(function(aProp) { |
michael@0 | 6320 | this.__defineGetter__(aProp, function AddonWrapper_datePropertyGetter() new Date(aAddon[aProp])); |
michael@0 | 6321 | }, this); |
michael@0 | 6322 | |
michael@0 | 6323 | ["sourceURI", "releaseNotesURI"].forEach(function(aProp) { |
michael@0 | 6324 | this.__defineGetter__(aProp, function AddonWrapper_URIPropertyGetter() { |
michael@0 | 6325 | let [target, fromRepo] = chooseValue(aAddon, aProp); |
michael@0 | 6326 | if (!target) |
michael@0 | 6327 | return null; |
michael@0 | 6328 | if (fromRepo) |
michael@0 | 6329 | return target; |
michael@0 | 6330 | return NetUtil.newURI(target); |
michael@0 | 6331 | }); |
michael@0 | 6332 | }, this); |
michael@0 | 6333 | |
michael@0 | 6334 | this.__defineGetter__("optionsURL", function AddonWrapper_optionsURLGetter() { |
michael@0 | 6335 | if (this.isActive && aAddon.optionsURL) |
michael@0 | 6336 | return aAddon.optionsURL; |
michael@0 | 6337 | |
michael@0 | 6338 | if (this.isActive && this.hasResource("options.xul")) |
michael@0 | 6339 | return this.getResourceURI("options.xul").spec; |
michael@0 | 6340 | |
michael@0 | 6341 | return null; |
michael@0 | 6342 | }, this); |
michael@0 | 6343 | |
michael@0 | 6344 | this.__defineGetter__("optionsType", function AddonWrapper_optionsTypeGetter() { |
michael@0 | 6345 | if (!this.isActive) |
michael@0 | 6346 | return null; |
michael@0 | 6347 | |
michael@0 | 6348 | let hasOptionsXUL = this.hasResource("options.xul"); |
michael@0 | 6349 | let hasOptionsURL = !!this.optionsURL; |
michael@0 | 6350 | |
michael@0 | 6351 | if (aAddon.optionsType) { |
michael@0 | 6352 | switch (parseInt(aAddon.optionsType, 10)) { |
michael@0 | 6353 | case AddonManager.OPTIONS_TYPE_DIALOG: |
michael@0 | 6354 | case AddonManager.OPTIONS_TYPE_TAB: |
michael@0 | 6355 | return hasOptionsURL ? aAddon.optionsType : null; |
michael@0 | 6356 | case AddonManager.OPTIONS_TYPE_INLINE: |
michael@0 | 6357 | case AddonManager.OPTIONS_TYPE_INLINE_INFO: |
michael@0 | 6358 | return (hasOptionsXUL || hasOptionsURL) ? aAddon.optionsType : null; |
michael@0 | 6359 | } |
michael@0 | 6360 | return null; |
michael@0 | 6361 | } |
michael@0 | 6362 | |
michael@0 | 6363 | if (hasOptionsXUL) |
michael@0 | 6364 | return AddonManager.OPTIONS_TYPE_INLINE; |
michael@0 | 6365 | |
michael@0 | 6366 | if (hasOptionsURL) |
michael@0 | 6367 | return AddonManager.OPTIONS_TYPE_DIALOG; |
michael@0 | 6368 | |
michael@0 | 6369 | return null; |
michael@0 | 6370 | }, this); |
michael@0 | 6371 | |
michael@0 | 6372 | this.__defineGetter__("iconURL", function AddonWrapper_iconURLGetter() { |
michael@0 | 6373 | return this.icons[32]; |
michael@0 | 6374 | }, this); |
michael@0 | 6375 | |
michael@0 | 6376 | this.__defineGetter__("icon64URL", function AddonWrapper_icon64URLGetter() { |
michael@0 | 6377 | return this.icons[64]; |
michael@0 | 6378 | }, this); |
michael@0 | 6379 | |
michael@0 | 6380 | this.__defineGetter__("icons", function AddonWrapper_iconsGetter() { |
michael@0 | 6381 | let icons = {}; |
michael@0 | 6382 | if (aAddon._repositoryAddon) { |
michael@0 | 6383 | for (let size in aAddon._repositoryAddon.icons) { |
michael@0 | 6384 | icons[size] = aAddon._repositoryAddon.icons[size]; |
michael@0 | 6385 | } |
michael@0 | 6386 | } |
michael@0 | 6387 | if (this.isActive && aAddon.iconURL) { |
michael@0 | 6388 | icons[32] = aAddon.iconURL; |
michael@0 | 6389 | } else if (this.hasResource("icon.png")) { |
michael@0 | 6390 | icons[32] = this.getResourceURI("icon.png").spec; |
michael@0 | 6391 | } |
michael@0 | 6392 | if (this.isActive && aAddon.icon64URL) { |
michael@0 | 6393 | icons[64] = aAddon.icon64URL; |
michael@0 | 6394 | } else if (this.hasResource("icon64.png")) { |
michael@0 | 6395 | icons[64] = this.getResourceURI("icon64.png").spec; |
michael@0 | 6396 | } |
michael@0 | 6397 | Object.freeze(icons); |
michael@0 | 6398 | return icons; |
michael@0 | 6399 | }, this); |
michael@0 | 6400 | |
michael@0 | 6401 | PROP_LOCALE_SINGLE.forEach(function(aProp) { |
michael@0 | 6402 | this.__defineGetter__(aProp, function AddonWrapper_singleLocaleGetter() { |
michael@0 | 6403 | // Override XPI creator if repository creator is defined |
michael@0 | 6404 | if (aProp == "creator" && |
michael@0 | 6405 | aAddon._repositoryAddon && aAddon._repositoryAddon.creator) { |
michael@0 | 6406 | return aAddon._repositoryAddon.creator; |
michael@0 | 6407 | } |
michael@0 | 6408 | |
michael@0 | 6409 | let result = null; |
michael@0 | 6410 | |
michael@0 | 6411 | if (aAddon.active) { |
michael@0 | 6412 | try { |
michael@0 | 6413 | let pref = PREF_EM_EXTENSION_FORMAT + aAddon.id + "." + aProp; |
michael@0 | 6414 | let value = Services.prefs.getComplexValue(pref, |
michael@0 | 6415 | Ci.nsIPrefLocalizedString); |
michael@0 | 6416 | if (value.data) |
michael@0 | 6417 | result = value.data; |
michael@0 | 6418 | } |
michael@0 | 6419 | catch (e) { |
michael@0 | 6420 | } |
michael@0 | 6421 | } |
michael@0 | 6422 | |
michael@0 | 6423 | if (result == null) |
michael@0 | 6424 | [result, ] = chooseValue(aAddon.selectedLocale, aProp); |
michael@0 | 6425 | |
michael@0 | 6426 | if (aProp == "creator") |
michael@0 | 6427 | return result ? new AddonManagerPrivate.AddonAuthor(result) : null; |
michael@0 | 6428 | |
michael@0 | 6429 | return result; |
michael@0 | 6430 | }); |
michael@0 | 6431 | }, this); |
michael@0 | 6432 | |
michael@0 | 6433 | PROP_LOCALE_MULTI.forEach(function(aProp) { |
michael@0 | 6434 | this.__defineGetter__(aProp, function AddonWrapper_multiLocaleGetter() { |
michael@0 | 6435 | let results = null; |
michael@0 | 6436 | let usedRepository = false; |
michael@0 | 6437 | |
michael@0 | 6438 | if (aAddon.active) { |
michael@0 | 6439 | let pref = PREF_EM_EXTENSION_FORMAT + aAddon.id + "." + |
michael@0 | 6440 | aProp.substring(0, aProp.length - 1); |
michael@0 | 6441 | let list = Services.prefs.getChildList(pref, {}); |
michael@0 | 6442 | if (list.length > 0) { |
michael@0 | 6443 | list.sort(); |
michael@0 | 6444 | results = []; |
michael@0 | 6445 | list.forEach(function(aPref) { |
michael@0 | 6446 | let value = Services.prefs.getComplexValue(aPref, |
michael@0 | 6447 | Ci.nsIPrefLocalizedString); |
michael@0 | 6448 | if (value.data) |
michael@0 | 6449 | results.push(value.data); |
michael@0 | 6450 | }); |
michael@0 | 6451 | } |
michael@0 | 6452 | } |
michael@0 | 6453 | |
michael@0 | 6454 | if (results == null) |
michael@0 | 6455 | [results, usedRepository] = chooseValue(aAddon.selectedLocale, aProp); |
michael@0 | 6456 | |
michael@0 | 6457 | if (results && !usedRepository) { |
michael@0 | 6458 | results = results.map(function mapResult(aResult) { |
michael@0 | 6459 | return new AddonManagerPrivate.AddonAuthor(aResult); |
michael@0 | 6460 | }); |
michael@0 | 6461 | } |
michael@0 | 6462 | |
michael@0 | 6463 | return results; |
michael@0 | 6464 | }); |
michael@0 | 6465 | }, this); |
michael@0 | 6466 | |
michael@0 | 6467 | this.__defineGetter__("screenshots", function AddonWrapper_screenshotsGetter() { |
michael@0 | 6468 | let repositoryAddon = aAddon._repositoryAddon; |
michael@0 | 6469 | if (repositoryAddon && ("screenshots" in repositoryAddon)) { |
michael@0 | 6470 | let repositoryScreenshots = repositoryAddon.screenshots; |
michael@0 | 6471 | if (repositoryScreenshots && repositoryScreenshots.length > 0) |
michael@0 | 6472 | return repositoryScreenshots; |
michael@0 | 6473 | } |
michael@0 | 6474 | |
michael@0 | 6475 | if (aAddon.type == "theme" && this.hasResource("preview.png")) { |
michael@0 | 6476 | let url = this.getResourceURI("preview.png").spec; |
michael@0 | 6477 | return [new AddonManagerPrivate.AddonScreenshot(url)]; |
michael@0 | 6478 | } |
michael@0 | 6479 | |
michael@0 | 6480 | return null; |
michael@0 | 6481 | }); |
michael@0 | 6482 | |
michael@0 | 6483 | this.__defineGetter__("applyBackgroundUpdates", function AddonWrapper_applyBackgroundUpdatesGetter() { |
michael@0 | 6484 | return aAddon.applyBackgroundUpdates; |
michael@0 | 6485 | }); |
michael@0 | 6486 | this.__defineSetter__("applyBackgroundUpdates", function AddonWrapper_applyBackgroundUpdatesSetter(val) { |
michael@0 | 6487 | if (this.type == "experiment") { |
michael@0 | 6488 | logger.warn("Setting applyBackgroundUpdates on an experiment is not supported."); |
michael@0 | 6489 | return; |
michael@0 | 6490 | } |
michael@0 | 6491 | |
michael@0 | 6492 | if (val != AddonManager.AUTOUPDATE_DEFAULT && |
michael@0 | 6493 | val != AddonManager.AUTOUPDATE_DISABLE && |
michael@0 | 6494 | val != AddonManager.AUTOUPDATE_ENABLE) { |
michael@0 | 6495 | val = val ? AddonManager.AUTOUPDATE_DEFAULT : |
michael@0 | 6496 | AddonManager.AUTOUPDATE_DISABLE; |
michael@0 | 6497 | } |
michael@0 | 6498 | |
michael@0 | 6499 | if (val == aAddon.applyBackgroundUpdates) |
michael@0 | 6500 | return val; |
michael@0 | 6501 | |
michael@0 | 6502 | XPIDatabase.setAddonProperties(aAddon, { |
michael@0 | 6503 | applyBackgroundUpdates: val |
michael@0 | 6504 | }); |
michael@0 | 6505 | AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, ["applyBackgroundUpdates"]); |
michael@0 | 6506 | |
michael@0 | 6507 | return val; |
michael@0 | 6508 | }); |
michael@0 | 6509 | |
michael@0 | 6510 | this.__defineSetter__("syncGUID", function AddonWrapper_syncGUIDGetter(val) { |
michael@0 | 6511 | if (aAddon.syncGUID == val) |
michael@0 | 6512 | return val; |
michael@0 | 6513 | |
michael@0 | 6514 | if (aAddon.inDatabase) |
michael@0 | 6515 | XPIDatabase.setAddonSyncGUID(aAddon, val); |
michael@0 | 6516 | |
michael@0 | 6517 | aAddon.syncGUID = val; |
michael@0 | 6518 | |
michael@0 | 6519 | return val; |
michael@0 | 6520 | }); |
michael@0 | 6521 | |
michael@0 | 6522 | this.__defineGetter__("install", function AddonWrapper_installGetter() { |
michael@0 | 6523 | if (!("_install" in aAddon) || !aAddon._install) |
michael@0 | 6524 | return null; |
michael@0 | 6525 | return aAddon._install.wrapper; |
michael@0 | 6526 | }); |
michael@0 | 6527 | |
michael@0 | 6528 | this.__defineGetter__("pendingUpgrade", function AddonWrapper_pendingUpgradeGetter() { |
michael@0 | 6529 | return createWrapper(aAddon.pendingUpgrade); |
michael@0 | 6530 | }); |
michael@0 | 6531 | |
michael@0 | 6532 | this.__defineGetter__("scope", function AddonWrapper_scopeGetter() { |
michael@0 | 6533 | if (aAddon._installLocation) |
michael@0 | 6534 | return aAddon._installLocation.scope; |
michael@0 | 6535 | |
michael@0 | 6536 | return AddonManager.SCOPE_PROFILE; |
michael@0 | 6537 | }); |
michael@0 | 6538 | |
michael@0 | 6539 | this.__defineGetter__("pendingOperations", function AddonWrapper_pendingOperationsGetter() { |
michael@0 | 6540 | let pending = 0; |
michael@0 | 6541 | if (!(aAddon.inDatabase)) { |
michael@0 | 6542 | // Add-on is pending install if there is no associated install (shouldn't |
michael@0 | 6543 | // happen here) or if the install is in the process of or has successfully |
michael@0 | 6544 | // completed the install. If an add-on is pending install then we ignore |
michael@0 | 6545 | // any other pending operations. |
michael@0 | 6546 | if (!aAddon._install || aAddon._install.state == AddonManager.STATE_INSTALLING || |
michael@0 | 6547 | aAddon._install.state == AddonManager.STATE_INSTALLED) |
michael@0 | 6548 | return AddonManager.PENDING_INSTALL; |
michael@0 | 6549 | } |
michael@0 | 6550 | else if (aAddon.pendingUninstall) { |
michael@0 | 6551 | // If an add-on is pending uninstall then we ignore any other pending |
michael@0 | 6552 | // operations |
michael@0 | 6553 | return AddonManager.PENDING_UNINSTALL; |
michael@0 | 6554 | } |
michael@0 | 6555 | |
michael@0 | 6556 | if (aAddon.active && isAddonDisabled(aAddon)) |
michael@0 | 6557 | pending |= AddonManager.PENDING_DISABLE; |
michael@0 | 6558 | else if (!aAddon.active && !isAddonDisabled(aAddon)) |
michael@0 | 6559 | pending |= AddonManager.PENDING_ENABLE; |
michael@0 | 6560 | |
michael@0 | 6561 | if (aAddon.pendingUpgrade) |
michael@0 | 6562 | pending |= AddonManager.PENDING_UPGRADE; |
michael@0 | 6563 | |
michael@0 | 6564 | return pending; |
michael@0 | 6565 | }); |
michael@0 | 6566 | |
michael@0 | 6567 | this.__defineGetter__("operationsRequiringRestart", function AddonWrapper_operationsRequiringRestartGetter() { |
michael@0 | 6568 | let ops = 0; |
michael@0 | 6569 | if (XPIProvider.installRequiresRestart(aAddon)) |
michael@0 | 6570 | ops |= AddonManager.OP_NEEDS_RESTART_INSTALL; |
michael@0 | 6571 | if (XPIProvider.uninstallRequiresRestart(aAddon)) |
michael@0 | 6572 | ops |= AddonManager.OP_NEEDS_RESTART_UNINSTALL; |
michael@0 | 6573 | if (XPIProvider.enableRequiresRestart(aAddon)) |
michael@0 | 6574 | ops |= AddonManager.OP_NEEDS_RESTART_ENABLE; |
michael@0 | 6575 | if (XPIProvider.disableRequiresRestart(aAddon)) |
michael@0 | 6576 | ops |= AddonManager.OP_NEEDS_RESTART_DISABLE; |
michael@0 | 6577 | |
michael@0 | 6578 | return ops; |
michael@0 | 6579 | }); |
michael@0 | 6580 | |
michael@0 | 6581 | this.__defineGetter__("isDebuggable", function AddonWrapper_isDebuggable() { |
michael@0 | 6582 | return this.isActive && aAddon.bootstrap; |
michael@0 | 6583 | }); |
michael@0 | 6584 | |
michael@0 | 6585 | this.__defineGetter__("permissions", function AddonWrapper_permisionsGetter() { |
michael@0 | 6586 | let permissions = 0; |
michael@0 | 6587 | |
michael@0 | 6588 | // Add-ons that aren't installed cannot be modified in any way |
michael@0 | 6589 | if (!(aAddon.inDatabase)) |
michael@0 | 6590 | return permissions; |
michael@0 | 6591 | |
michael@0 | 6592 | // Experiments can only be uninstalled. An uninstall reflects the user |
michael@0 | 6593 | // intent of "disable this experiment." This is partially managed by the |
michael@0 | 6594 | // experiments manager. |
michael@0 | 6595 | if (aAddon.type == "experiment") { |
michael@0 | 6596 | return AddonManager.PERM_CAN_UNINSTALL; |
michael@0 | 6597 | } |
michael@0 | 6598 | |
michael@0 | 6599 | if (!aAddon.appDisabled) { |
michael@0 | 6600 | if (this.userDisabled) { |
michael@0 | 6601 | permissions |= AddonManager.PERM_CAN_ENABLE; |
michael@0 | 6602 | } |
michael@0 | 6603 | else if (aAddon.type != "theme") { |
michael@0 | 6604 | permissions |= AddonManager.PERM_CAN_DISABLE; |
michael@0 | 6605 | } |
michael@0 | 6606 | } |
michael@0 | 6607 | |
michael@0 | 6608 | // Add-ons that are in locked install locations, or are pending uninstall |
michael@0 | 6609 | // cannot be upgraded or uninstalled |
michael@0 | 6610 | if (!aAddon._installLocation.locked && !aAddon.pendingUninstall) { |
michael@0 | 6611 | // Add-ons that are installed by a file link cannot be upgraded |
michael@0 | 6612 | if (!aAddon._installLocation.isLinkedAddon(aAddon.id)) { |
michael@0 | 6613 | permissions |= AddonManager.PERM_CAN_UPGRADE; |
michael@0 | 6614 | } |
michael@0 | 6615 | |
michael@0 | 6616 | permissions |= AddonManager.PERM_CAN_UNINSTALL; |
michael@0 | 6617 | } |
michael@0 | 6618 | |
michael@0 | 6619 | return permissions; |
michael@0 | 6620 | }); |
michael@0 | 6621 | |
michael@0 | 6622 | this.__defineGetter__("isActive", function AddonWrapper_isActiveGetter() { |
michael@0 | 6623 | if (Services.appinfo.inSafeMode) |
michael@0 | 6624 | return false; |
michael@0 | 6625 | return aAddon.active; |
michael@0 | 6626 | }); |
michael@0 | 6627 | |
michael@0 | 6628 | this.__defineGetter__("userDisabled", function AddonWrapper_userDisabledGetter() { |
michael@0 | 6629 | if (XPIProvider._enabledExperiments.has(aAddon.id)) { |
michael@0 | 6630 | return false; |
michael@0 | 6631 | } |
michael@0 | 6632 | |
michael@0 | 6633 | return aAddon.softDisabled || aAddon.userDisabled; |
michael@0 | 6634 | }); |
michael@0 | 6635 | this.__defineSetter__("userDisabled", function AddonWrapper_userDisabledSetter(val) { |
michael@0 | 6636 | if (val == this.userDisabled) { |
michael@0 | 6637 | return val; |
michael@0 | 6638 | } |
michael@0 | 6639 | |
michael@0 | 6640 | if (aAddon.type == "experiment") { |
michael@0 | 6641 | if (val) { |
michael@0 | 6642 | XPIProvider._enabledExperiments.delete(aAddon.id); |
michael@0 | 6643 | } else { |
michael@0 | 6644 | XPIProvider._enabledExperiments.add(aAddon.id); |
michael@0 | 6645 | } |
michael@0 | 6646 | } |
michael@0 | 6647 | |
michael@0 | 6648 | if (aAddon.inDatabase) { |
michael@0 | 6649 | if (aAddon.type == "theme" && val) { |
michael@0 | 6650 | if (aAddon.internalName == XPIProvider.defaultSkin) |
michael@0 | 6651 | throw new Error("Cannot disable the default theme"); |
michael@0 | 6652 | XPIProvider.enableDefaultTheme(); |
michael@0 | 6653 | } |
michael@0 | 6654 | else { |
michael@0 | 6655 | XPIProvider.updateAddonDisabledState(aAddon, val); |
michael@0 | 6656 | } |
michael@0 | 6657 | } |
michael@0 | 6658 | else { |
michael@0 | 6659 | aAddon.userDisabled = val; |
michael@0 | 6660 | // When enabling remove the softDisabled flag |
michael@0 | 6661 | if (!val) |
michael@0 | 6662 | aAddon.softDisabled = false; |
michael@0 | 6663 | } |
michael@0 | 6664 | |
michael@0 | 6665 | return val; |
michael@0 | 6666 | }); |
michael@0 | 6667 | |
michael@0 | 6668 | this.__defineSetter__("softDisabled", function AddonWrapper_softDisabledSetter(val) { |
michael@0 | 6669 | if (val == aAddon.softDisabled) |
michael@0 | 6670 | return val; |
michael@0 | 6671 | |
michael@0 | 6672 | if (aAddon.inDatabase) { |
michael@0 | 6673 | // When softDisabling a theme just enable the active theme |
michael@0 | 6674 | if (aAddon.type == "theme" && val && !aAddon.userDisabled) { |
michael@0 | 6675 | if (aAddon.internalName == XPIProvider.defaultSkin) |
michael@0 | 6676 | throw new Error("Cannot disable the default theme"); |
michael@0 | 6677 | XPIProvider.enableDefaultTheme(); |
michael@0 | 6678 | } |
michael@0 | 6679 | else { |
michael@0 | 6680 | XPIProvider.updateAddonDisabledState(aAddon, undefined, val); |
michael@0 | 6681 | } |
michael@0 | 6682 | } |
michael@0 | 6683 | else { |
michael@0 | 6684 | // Only set softDisabled if not already disabled |
michael@0 | 6685 | if (!aAddon.userDisabled) |
michael@0 | 6686 | aAddon.softDisabled = val; |
michael@0 | 6687 | } |
michael@0 | 6688 | |
michael@0 | 6689 | return val; |
michael@0 | 6690 | }); |
michael@0 | 6691 | |
michael@0 | 6692 | this.isCompatibleWith = function AddonWrapper_isCompatiblewith(aAppVersion, aPlatformVersion) { |
michael@0 | 6693 | return aAddon.isCompatibleWith(aAppVersion, aPlatformVersion); |
michael@0 | 6694 | }; |
michael@0 | 6695 | |
michael@0 | 6696 | this.uninstall = function AddonWrapper_uninstall() { |
michael@0 | 6697 | if (!(aAddon.inDatabase)) |
michael@0 | 6698 | throw new Error("Cannot uninstall an add-on that isn't installed"); |
michael@0 | 6699 | if (aAddon.pendingUninstall) |
michael@0 | 6700 | throw new Error("Add-on is already marked to be uninstalled"); |
michael@0 | 6701 | XPIProvider.uninstallAddon(aAddon); |
michael@0 | 6702 | }; |
michael@0 | 6703 | |
michael@0 | 6704 | this.cancelUninstall = function AddonWrapper_cancelUninstall() { |
michael@0 | 6705 | if (!(aAddon.inDatabase)) |
michael@0 | 6706 | throw new Error("Cannot cancel uninstall for an add-on that isn't installed"); |
michael@0 | 6707 | if (!aAddon.pendingUninstall) |
michael@0 | 6708 | throw new Error("Add-on is not marked to be uninstalled"); |
michael@0 | 6709 | XPIProvider.cancelUninstallAddon(aAddon); |
michael@0 | 6710 | }; |
michael@0 | 6711 | |
michael@0 | 6712 | this.findUpdates = function AddonWrapper_findUpdates(aListener, aReason, aAppVersion, aPlatformVersion) { |
michael@0 | 6713 | // Short-circuit updates for experiments because updates are handled |
michael@0 | 6714 | // through the Experiments Manager. |
michael@0 | 6715 | if (this.type == "experiment") { |
michael@0 | 6716 | AddonManagerPrivate.callNoUpdateListeners(this, aListener, aReason, |
michael@0 | 6717 | aAppVersion, aPlatformVersion); |
michael@0 | 6718 | return; |
michael@0 | 6719 | } |
michael@0 | 6720 | |
michael@0 | 6721 | new UpdateChecker(aAddon, aListener, aReason, aAppVersion, aPlatformVersion); |
michael@0 | 6722 | }; |
michael@0 | 6723 | |
michael@0 | 6724 | // Returns true if there was an update in progress, false if there was no update to cancel |
michael@0 | 6725 | this.cancelUpdate = function AddonWrapper_cancelUpdate() { |
michael@0 | 6726 | if (aAddon._updateCheck) { |
michael@0 | 6727 | aAddon._updateCheck.cancel(); |
michael@0 | 6728 | return true; |
michael@0 | 6729 | } |
michael@0 | 6730 | return false; |
michael@0 | 6731 | }; |
michael@0 | 6732 | |
michael@0 | 6733 | this.hasResource = function AddonWrapper_hasResource(aPath) { |
michael@0 | 6734 | if (aAddon._hasResourceCache.has(aPath)) |
michael@0 | 6735 | return aAddon._hasResourceCache.get(aPath); |
michael@0 | 6736 | |
michael@0 | 6737 | let bundle = aAddon._sourceBundle.clone(); |
michael@0 | 6738 | |
michael@0 | 6739 | // Bundle may not exist any more if the addon has just been uninstalled, |
michael@0 | 6740 | // but explicitly first checking .exists() results in unneeded file I/O. |
michael@0 | 6741 | try { |
michael@0 | 6742 | var isDir = bundle.isDirectory(); |
michael@0 | 6743 | } catch (e) { |
michael@0 | 6744 | aAddon._hasResourceCache.set(aPath, false); |
michael@0 | 6745 | return false; |
michael@0 | 6746 | } |
michael@0 | 6747 | |
michael@0 | 6748 | if (isDir) { |
michael@0 | 6749 | if (aPath) { |
michael@0 | 6750 | aPath.split("/").forEach(function(aPart) { |
michael@0 | 6751 | bundle.append(aPart); |
michael@0 | 6752 | }); |
michael@0 | 6753 | } |
michael@0 | 6754 | let result = bundle.exists(); |
michael@0 | 6755 | aAddon._hasResourceCache.set(aPath, result); |
michael@0 | 6756 | return result; |
michael@0 | 6757 | } |
michael@0 | 6758 | |
michael@0 | 6759 | let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"]. |
michael@0 | 6760 | createInstance(Ci.nsIZipReader); |
michael@0 | 6761 | try { |
michael@0 | 6762 | zipReader.open(bundle); |
michael@0 | 6763 | let result = zipReader.hasEntry(aPath); |
michael@0 | 6764 | aAddon._hasResourceCache.set(aPath, result); |
michael@0 | 6765 | return result; |
michael@0 | 6766 | } |
michael@0 | 6767 | catch (e) { |
michael@0 | 6768 | aAddon._hasResourceCache.set(aPath, false); |
michael@0 | 6769 | return false; |
michael@0 | 6770 | } |
michael@0 | 6771 | finally { |
michael@0 | 6772 | zipReader.close(); |
michael@0 | 6773 | } |
michael@0 | 6774 | }, |
michael@0 | 6775 | |
michael@0 | 6776 | /** |
michael@0 | 6777 | * Returns a URI to the selected resource or to the add-on bundle if aPath |
michael@0 | 6778 | * is null. URIs to the bundle will always be file: URIs. URIs to resources |
michael@0 | 6779 | * will be file: URIs if the add-on is unpacked or jar: URIs if the add-on is |
michael@0 | 6780 | * still an XPI file. |
michael@0 | 6781 | * |
michael@0 | 6782 | * @param aPath |
michael@0 | 6783 | * The path in the add-on to get the URI for or null to get a URI to |
michael@0 | 6784 | * the file or directory the add-on is installed as. |
michael@0 | 6785 | * @return an nsIURI |
michael@0 | 6786 | */ |
michael@0 | 6787 | this.getResourceURI = function AddonWrapper_getResourceURI(aPath) { |
michael@0 | 6788 | if (!aPath) |
michael@0 | 6789 | return NetUtil.newURI(aAddon._sourceBundle); |
michael@0 | 6790 | |
michael@0 | 6791 | return getURIForResourceInFile(aAddon._sourceBundle, aPath); |
michael@0 | 6792 | } |
michael@0 | 6793 | } |
michael@0 | 6794 | |
michael@0 | 6795 | /** |
michael@0 | 6796 | * An object which identifies a directory install location for add-ons. The |
michael@0 | 6797 | * location consists of a directory which contains the add-ons installed in the |
michael@0 | 6798 | * location. |
michael@0 | 6799 | * |
michael@0 | 6800 | * Each add-on installed in the location is either a directory containing the |
michael@0 | 6801 | * add-on's files or a text file containing an absolute path to the directory |
michael@0 | 6802 | * containing the add-ons files. The directory or text file must have the same |
michael@0 | 6803 | * name as the add-on's ID. |
michael@0 | 6804 | * |
michael@0 | 6805 | * There may also a special directory named "staged" which can contain |
michael@0 | 6806 | * directories with the same name as an add-on ID. If the directory is empty |
michael@0 | 6807 | * then it means the add-on will be uninstalled from this location during the |
michael@0 | 6808 | * next startup. If the directory contains the add-on's files then they will be |
michael@0 | 6809 | * installed during the next startup. |
michael@0 | 6810 | * |
michael@0 | 6811 | * @param aName |
michael@0 | 6812 | * The string identifier for the install location |
michael@0 | 6813 | * @param aDirectory |
michael@0 | 6814 | * The nsIFile directory for the install location |
michael@0 | 6815 | * @param aScope |
michael@0 | 6816 | * The scope of add-ons installed in this location |
michael@0 | 6817 | * @param aLocked |
michael@0 | 6818 | * true if add-ons cannot be installed, uninstalled or upgraded in the |
michael@0 | 6819 | * install location |
michael@0 | 6820 | */ |
michael@0 | 6821 | function DirectoryInstallLocation(aName, aDirectory, aScope, aLocked) { |
michael@0 | 6822 | this._name = aName; |
michael@0 | 6823 | this.locked = aLocked; |
michael@0 | 6824 | this._directory = aDirectory; |
michael@0 | 6825 | this._scope = aScope |
michael@0 | 6826 | this._IDToFileMap = {}; |
michael@0 | 6827 | this._FileToIDMap = {}; |
michael@0 | 6828 | this._linkedAddons = []; |
michael@0 | 6829 | this._stagingDirLock = 0; |
michael@0 | 6830 | |
michael@0 | 6831 | if (!aDirectory.exists()) |
michael@0 | 6832 | return; |
michael@0 | 6833 | if (!aDirectory.isDirectory()) |
michael@0 | 6834 | throw new Error("Location must be a directory."); |
michael@0 | 6835 | |
michael@0 | 6836 | this._readAddons(); |
michael@0 | 6837 | } |
michael@0 | 6838 | |
michael@0 | 6839 | DirectoryInstallLocation.prototype = { |
michael@0 | 6840 | _name : "", |
michael@0 | 6841 | _directory : null, |
michael@0 | 6842 | _IDToFileMap : null, // mapping from add-on ID to nsIFile |
michael@0 | 6843 | _FileToIDMap : null, // mapping from add-on path to add-on ID |
michael@0 | 6844 | |
michael@0 | 6845 | /** |
michael@0 | 6846 | * Reads a directory linked to in a file. |
michael@0 | 6847 | * |
michael@0 | 6848 | * @param file |
michael@0 | 6849 | * The file containing the directory path |
michael@0 | 6850 | * @return An nsIFile object representing the linked directory. |
michael@0 | 6851 | */ |
michael@0 | 6852 | _readDirectoryFromFile: function DirInstallLocation__readDirectoryFromFile(aFile) { |
michael@0 | 6853 | let fis = Cc["@mozilla.org/network/file-input-stream;1"]. |
michael@0 | 6854 | createInstance(Ci.nsIFileInputStream); |
michael@0 | 6855 | fis.init(aFile, -1, -1, false); |
michael@0 | 6856 | let line = { value: "" }; |
michael@0 | 6857 | if (fis instanceof Ci.nsILineInputStream) |
michael@0 | 6858 | fis.readLine(line); |
michael@0 | 6859 | fis.close(); |
michael@0 | 6860 | if (line.value) { |
michael@0 | 6861 | let linkedDirectory = Cc["@mozilla.org/file/local;1"]. |
michael@0 | 6862 | createInstance(Ci.nsIFile); |
michael@0 | 6863 | |
michael@0 | 6864 | try { |
michael@0 | 6865 | linkedDirectory.initWithPath(line.value); |
michael@0 | 6866 | } |
michael@0 | 6867 | catch (e) { |
michael@0 | 6868 | linkedDirectory.setRelativeDescriptor(aFile.parent, line.value); |
michael@0 | 6869 | } |
michael@0 | 6870 | |
michael@0 | 6871 | if (!linkedDirectory.exists()) { |
michael@0 | 6872 | logger.warn("File pointer " + aFile.path + " points to " + linkedDirectory.path + |
michael@0 | 6873 | " which does not exist"); |
michael@0 | 6874 | return null; |
michael@0 | 6875 | } |
michael@0 | 6876 | |
michael@0 | 6877 | if (!linkedDirectory.isDirectory()) { |
michael@0 | 6878 | logger.warn("File pointer " + aFile.path + " points to " + linkedDirectory.path + |
michael@0 | 6879 | " which is not a directory"); |
michael@0 | 6880 | return null; |
michael@0 | 6881 | } |
michael@0 | 6882 | |
michael@0 | 6883 | return linkedDirectory; |
michael@0 | 6884 | } |
michael@0 | 6885 | |
michael@0 | 6886 | logger.warn("File pointer " + aFile.path + " does not contain a path"); |
michael@0 | 6887 | return null; |
michael@0 | 6888 | }, |
michael@0 | 6889 | |
michael@0 | 6890 | /** |
michael@0 | 6891 | * Finds all the add-ons installed in this location. |
michael@0 | 6892 | */ |
michael@0 | 6893 | _readAddons: function DirInstallLocation__readAddons() { |
michael@0 | 6894 | // Use a snapshot of the directory contents to avoid possible issues with |
michael@0 | 6895 | // iterating over a directory while removing files from it (the YAFFS2 |
michael@0 | 6896 | // embedded filesystem has this issue, see bug 772238). |
michael@0 | 6897 | let entries = getDirectoryEntries(this._directory); |
michael@0 | 6898 | for (let entry of entries) { |
michael@0 | 6899 | let id = entry.leafName; |
michael@0 | 6900 | |
michael@0 | 6901 | if (id == DIR_STAGE || id == DIR_XPI_STAGE || id == DIR_TRASH) |
michael@0 | 6902 | continue; |
michael@0 | 6903 | |
michael@0 | 6904 | let directLoad = false; |
michael@0 | 6905 | if (entry.isFile() && |
michael@0 | 6906 | id.substring(id.length - 4).toLowerCase() == ".xpi") { |
michael@0 | 6907 | directLoad = true; |
michael@0 | 6908 | id = id.substring(0, id.length - 4); |
michael@0 | 6909 | } |
michael@0 | 6910 | |
michael@0 | 6911 | if (!gIDTest.test(id)) { |
michael@0 | 6912 | logger.debug("Ignoring file entry whose name is not a valid add-on ID: " + |
michael@0 | 6913 | entry.path); |
michael@0 | 6914 | continue; |
michael@0 | 6915 | } |
michael@0 | 6916 | |
michael@0 | 6917 | if (entry.isFile() && !directLoad) { |
michael@0 | 6918 | let newEntry = this._readDirectoryFromFile(entry); |
michael@0 | 6919 | if (!newEntry) { |
michael@0 | 6920 | logger.debug("Deleting stale pointer file " + entry.path); |
michael@0 | 6921 | try { |
michael@0 | 6922 | entry.remove(true); |
michael@0 | 6923 | } |
michael@0 | 6924 | catch (e) { |
michael@0 | 6925 | logger.warn("Failed to remove stale pointer file " + entry.path, e); |
michael@0 | 6926 | // Failing to remove the stale pointer file is ignorable |
michael@0 | 6927 | } |
michael@0 | 6928 | continue; |
michael@0 | 6929 | } |
michael@0 | 6930 | |
michael@0 | 6931 | entry = newEntry; |
michael@0 | 6932 | this._linkedAddons.push(id); |
michael@0 | 6933 | } |
michael@0 | 6934 | |
michael@0 | 6935 | this._IDToFileMap[id] = entry; |
michael@0 | 6936 | this._FileToIDMap[entry.path] = id; |
michael@0 | 6937 | } |
michael@0 | 6938 | }, |
michael@0 | 6939 | |
michael@0 | 6940 | /** |
michael@0 | 6941 | * Gets the name of this install location. |
michael@0 | 6942 | */ |
michael@0 | 6943 | get name() { |
michael@0 | 6944 | return this._name; |
michael@0 | 6945 | }, |
michael@0 | 6946 | |
michael@0 | 6947 | /** |
michael@0 | 6948 | * Gets the scope of this install location. |
michael@0 | 6949 | */ |
michael@0 | 6950 | get scope() { |
michael@0 | 6951 | return this._scope; |
michael@0 | 6952 | }, |
michael@0 | 6953 | |
michael@0 | 6954 | /** |
michael@0 | 6955 | * Gets an array of nsIFiles for add-ons installed in this location. |
michael@0 | 6956 | */ |
michael@0 | 6957 | get addonLocations() { |
michael@0 | 6958 | let locations = []; |
michael@0 | 6959 | for (let id in this._IDToFileMap) { |
michael@0 | 6960 | locations.push(this._IDToFileMap[id].clone()); |
michael@0 | 6961 | } |
michael@0 | 6962 | return locations; |
michael@0 | 6963 | }, |
michael@0 | 6964 | |
michael@0 | 6965 | /** |
michael@0 | 6966 | * Gets the staging directory to put add-ons that are pending install and |
michael@0 | 6967 | * uninstall into. |
michael@0 | 6968 | * |
michael@0 | 6969 | * @return an nsIFile |
michael@0 | 6970 | */ |
michael@0 | 6971 | getStagingDir: function DirInstallLocation_getStagingDir() { |
michael@0 | 6972 | let dir = this._directory.clone(); |
michael@0 | 6973 | dir.append(DIR_STAGE); |
michael@0 | 6974 | return dir; |
michael@0 | 6975 | }, |
michael@0 | 6976 | |
michael@0 | 6977 | requestStagingDir: function() { |
michael@0 | 6978 | this._stagingDirLock++; |
michael@0 | 6979 | |
michael@0 | 6980 | if (this._stagingDirPromise) |
michael@0 | 6981 | return this._stagingDirPromise; |
michael@0 | 6982 | |
michael@0 | 6983 | OS.File.makeDir(this._directory.path); |
michael@0 | 6984 | let stagepath = OS.Path.join(this._directory.path, DIR_STAGE); |
michael@0 | 6985 | return this._stagingDirPromise = OS.File.makeDir(stagepath).then(null, (e) => { |
michael@0 | 6986 | if (e instanceof OS.File.Error && e.becauseExists) |
michael@0 | 6987 | return; |
michael@0 | 6988 | logger.error("Failed to create staging directory", e); |
michael@0 | 6989 | throw e; |
michael@0 | 6990 | }); |
michael@0 | 6991 | }, |
michael@0 | 6992 | |
michael@0 | 6993 | releaseStagingDir: function() { |
michael@0 | 6994 | this._stagingDirLock--; |
michael@0 | 6995 | |
michael@0 | 6996 | if (this._stagingDirLock == 0) { |
michael@0 | 6997 | this._stagingDirPromise = null; |
michael@0 | 6998 | this.cleanStagingDir(); |
michael@0 | 6999 | } |
michael@0 | 7000 | |
michael@0 | 7001 | return Promise.resolve(); |
michael@0 | 7002 | }, |
michael@0 | 7003 | |
michael@0 | 7004 | /** |
michael@0 | 7005 | * Removes the specified files or directories in the staging directory and |
michael@0 | 7006 | * then if the staging directory is empty attempts to remove it. |
michael@0 | 7007 | * |
michael@0 | 7008 | * @param aLeafNames |
michael@0 | 7009 | * An array of file or directory to remove from the directory, the |
michael@0 | 7010 | * array may be empty |
michael@0 | 7011 | */ |
michael@0 | 7012 | cleanStagingDir: function(aLeafNames = []) { |
michael@0 | 7013 | let dir = this.getStagingDir(); |
michael@0 | 7014 | |
michael@0 | 7015 | for (let name of aLeafNames) { |
michael@0 | 7016 | let file = dir.clone(); |
michael@0 | 7017 | file.append(name); |
michael@0 | 7018 | recursiveRemove(file); |
michael@0 | 7019 | } |
michael@0 | 7020 | |
michael@0 | 7021 | if (this._stagingDirLock > 0) |
michael@0 | 7022 | return; |
michael@0 | 7023 | |
michael@0 | 7024 | let dirEntries = dir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator); |
michael@0 | 7025 | try { |
michael@0 | 7026 | if (dirEntries.nextFile) |
michael@0 | 7027 | return; |
michael@0 | 7028 | } |
michael@0 | 7029 | finally { |
michael@0 | 7030 | dirEntries.close(); |
michael@0 | 7031 | } |
michael@0 | 7032 | |
michael@0 | 7033 | try { |
michael@0 | 7034 | setFilePermissions(dir, FileUtils.PERMS_DIRECTORY); |
michael@0 | 7035 | dir.remove(false); |
michael@0 | 7036 | } |
michael@0 | 7037 | catch (e) { |
michael@0 | 7038 | logger.warn("Failed to remove staging dir", e); |
michael@0 | 7039 | // Failing to remove the staging directory is ignorable |
michael@0 | 7040 | } |
michael@0 | 7041 | }, |
michael@0 | 7042 | |
michael@0 | 7043 | /** |
michael@0 | 7044 | * Gets the directory used by old versions for staging XPI and JAR files ready |
michael@0 | 7045 | * to be installed. |
michael@0 | 7046 | * |
michael@0 | 7047 | * @return an nsIFile |
michael@0 | 7048 | */ |
michael@0 | 7049 | getXPIStagingDir: function DirInstallLocation_getXPIStagingDir() { |
michael@0 | 7050 | let dir = this._directory.clone(); |
michael@0 | 7051 | dir.append(DIR_XPI_STAGE); |
michael@0 | 7052 | return dir; |
michael@0 | 7053 | }, |
michael@0 | 7054 | |
michael@0 | 7055 | /** |
michael@0 | 7056 | * Returns a directory that is normally on the same filesystem as the rest of |
michael@0 | 7057 | * the install location and can be used for temporarily storing files during |
michael@0 | 7058 | * safe move operations. Calling this method will delete the existing trash |
michael@0 | 7059 | * directory and its contents. |
michael@0 | 7060 | * |
michael@0 | 7061 | * @return an nsIFile |
michael@0 | 7062 | */ |
michael@0 | 7063 | getTrashDir: function DirInstallLocation_getTrashDir() { |
michael@0 | 7064 | let trashDir = this._directory.clone(); |
michael@0 | 7065 | trashDir.append(DIR_TRASH); |
michael@0 | 7066 | if (trashDir.exists()) |
michael@0 | 7067 | recursiveRemove(trashDir); |
michael@0 | 7068 | trashDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); |
michael@0 | 7069 | return trashDir; |
michael@0 | 7070 | }, |
michael@0 | 7071 | |
michael@0 | 7072 | /** |
michael@0 | 7073 | * Installs an add-on into the install location. |
michael@0 | 7074 | * |
michael@0 | 7075 | * @param aId |
michael@0 | 7076 | * The ID of the add-on to install |
michael@0 | 7077 | * @param aSource |
michael@0 | 7078 | * The source nsIFile to install from |
michael@0 | 7079 | * @param aExistingAddonID |
michael@0 | 7080 | * The ID of an existing add-on to uninstall at the same time |
michael@0 | 7081 | * @param aCopy |
michael@0 | 7082 | * If false the source files will be moved to the new location, |
michael@0 | 7083 | * otherwise they will only be copied |
michael@0 | 7084 | * @return an nsIFile indicating where the add-on was installed to |
michael@0 | 7085 | */ |
michael@0 | 7086 | installAddon: function DirInstallLocation_installAddon(aId, aSource, |
michael@0 | 7087 | aExistingAddonID, |
michael@0 | 7088 | aCopy) { |
michael@0 | 7089 | let trashDir = this.getTrashDir(); |
michael@0 | 7090 | |
michael@0 | 7091 | let transaction = new SafeInstallOperation(); |
michael@0 | 7092 | |
michael@0 | 7093 | let self = this; |
michael@0 | 7094 | function moveOldAddon(aId) { |
michael@0 | 7095 | let file = self._directory.clone(); |
michael@0 | 7096 | file.append(aId); |
michael@0 | 7097 | |
michael@0 | 7098 | if (file.exists()) |
michael@0 | 7099 | transaction.move(file, trashDir); |
michael@0 | 7100 | |
michael@0 | 7101 | file = self._directory.clone(); |
michael@0 | 7102 | file.append(aId + ".xpi"); |
michael@0 | 7103 | if (file.exists()) { |
michael@0 | 7104 | flushJarCache(file); |
michael@0 | 7105 | transaction.move(file, trashDir); |
michael@0 | 7106 | } |
michael@0 | 7107 | } |
michael@0 | 7108 | |
michael@0 | 7109 | // If any of these operations fails the finally block will clean up the |
michael@0 | 7110 | // temporary directory |
michael@0 | 7111 | try { |
michael@0 | 7112 | moveOldAddon(aId); |
michael@0 | 7113 | if (aExistingAddonID && aExistingAddonID != aId) |
michael@0 | 7114 | moveOldAddon(aExistingAddonID); |
michael@0 | 7115 | |
michael@0 | 7116 | if (aCopy) { |
michael@0 | 7117 | transaction.copy(aSource, this._directory); |
michael@0 | 7118 | } |
michael@0 | 7119 | else { |
michael@0 | 7120 | if (aSource.isFile()) |
michael@0 | 7121 | flushJarCache(aSource); |
michael@0 | 7122 | |
michael@0 | 7123 | transaction.move(aSource, this._directory); |
michael@0 | 7124 | } |
michael@0 | 7125 | } |
michael@0 | 7126 | finally { |
michael@0 | 7127 | // It isn't ideal if this cleanup fails but it isn't worth rolling back |
michael@0 | 7128 | // the install because of it. |
michael@0 | 7129 | try { |
michael@0 | 7130 | recursiveRemove(trashDir); |
michael@0 | 7131 | } |
michael@0 | 7132 | catch (e) { |
michael@0 | 7133 | logger.warn("Failed to remove trash directory when installing " + aId, e); |
michael@0 | 7134 | } |
michael@0 | 7135 | } |
michael@0 | 7136 | |
michael@0 | 7137 | let newFile = this._directory.clone(); |
michael@0 | 7138 | newFile.append(aSource.leafName); |
michael@0 | 7139 | try { |
michael@0 | 7140 | newFile.lastModifiedTime = Date.now(); |
michael@0 | 7141 | } catch (e) { |
michael@0 | 7142 | logger.warn("failed to set lastModifiedTime on " + newFile.path, e); |
michael@0 | 7143 | } |
michael@0 | 7144 | this._FileToIDMap[newFile.path] = aId; |
michael@0 | 7145 | this._IDToFileMap[aId] = newFile; |
michael@0 | 7146 | |
michael@0 | 7147 | if (aExistingAddonID && aExistingAddonID != aId && |
michael@0 | 7148 | aExistingAddonID in this._IDToFileMap) { |
michael@0 | 7149 | delete this._FileToIDMap[this._IDToFileMap[aExistingAddonID]]; |
michael@0 | 7150 | delete this._IDToFileMap[aExistingAddonID]; |
michael@0 | 7151 | } |
michael@0 | 7152 | |
michael@0 | 7153 | return newFile; |
michael@0 | 7154 | }, |
michael@0 | 7155 | |
michael@0 | 7156 | /** |
michael@0 | 7157 | * Uninstalls an add-on from this location. |
michael@0 | 7158 | * |
michael@0 | 7159 | * @param aId |
michael@0 | 7160 | * The ID of the add-on to uninstall |
michael@0 | 7161 | * @throws if the ID does not match any of the add-ons installed |
michael@0 | 7162 | */ |
michael@0 | 7163 | uninstallAddon: function DirInstallLocation_uninstallAddon(aId) { |
michael@0 | 7164 | let file = this._IDToFileMap[aId]; |
michael@0 | 7165 | if (!file) { |
michael@0 | 7166 | logger.warn("Attempted to remove " + aId + " from " + |
michael@0 | 7167 | this._name + " but it was already gone"); |
michael@0 | 7168 | return; |
michael@0 | 7169 | } |
michael@0 | 7170 | |
michael@0 | 7171 | file = this._directory.clone(); |
michael@0 | 7172 | file.append(aId); |
michael@0 | 7173 | if (!file.exists()) |
michael@0 | 7174 | file.leafName += ".xpi"; |
michael@0 | 7175 | |
michael@0 | 7176 | if (!file.exists()) { |
michael@0 | 7177 | logger.warn("Attempted to remove " + aId + " from " + |
michael@0 | 7178 | this._name + " but it was already gone"); |
michael@0 | 7179 | |
michael@0 | 7180 | delete this._FileToIDMap[file.path]; |
michael@0 | 7181 | delete this._IDToFileMap[aId]; |
michael@0 | 7182 | return; |
michael@0 | 7183 | } |
michael@0 | 7184 | |
michael@0 | 7185 | let trashDir = this.getTrashDir(); |
michael@0 | 7186 | |
michael@0 | 7187 | if (file.leafName != aId) { |
michael@0 | 7188 | logger.debug("uninstallAddon: flushing jar cache " + file.path + " for addon " + aId); |
michael@0 | 7189 | flushJarCache(file); |
michael@0 | 7190 | } |
michael@0 | 7191 | |
michael@0 | 7192 | let transaction = new SafeInstallOperation(); |
michael@0 | 7193 | |
michael@0 | 7194 | try { |
michael@0 | 7195 | transaction.move(file, trashDir); |
michael@0 | 7196 | } |
michael@0 | 7197 | finally { |
michael@0 | 7198 | // It isn't ideal if this cleanup fails, but it is probably better than |
michael@0 | 7199 | // rolling back the uninstall at this point |
michael@0 | 7200 | try { |
michael@0 | 7201 | recursiveRemove(trashDir); |
michael@0 | 7202 | } |
michael@0 | 7203 | catch (e) { |
michael@0 | 7204 | logger.warn("Failed to remove trash directory when uninstalling " + aId, e); |
michael@0 | 7205 | } |
michael@0 | 7206 | } |
michael@0 | 7207 | |
michael@0 | 7208 | delete this._FileToIDMap[file.path]; |
michael@0 | 7209 | delete this._IDToFileMap[aId]; |
michael@0 | 7210 | }, |
michael@0 | 7211 | |
michael@0 | 7212 | /** |
michael@0 | 7213 | * Gets the ID of the add-on installed in the given nsIFile. |
michael@0 | 7214 | * |
michael@0 | 7215 | * @param aFile |
michael@0 | 7216 | * The nsIFile to look in |
michael@0 | 7217 | * @return the ID |
michael@0 | 7218 | * @throws if the file does not represent an installed add-on |
michael@0 | 7219 | */ |
michael@0 | 7220 | getIDForLocation: function DirInstallLocation_getIDForLocation(aFile) { |
michael@0 | 7221 | if (aFile.path in this._FileToIDMap) |
michael@0 | 7222 | return this._FileToIDMap[aFile.path]; |
michael@0 | 7223 | throw new Error("Unknown add-on location " + aFile.path); |
michael@0 | 7224 | }, |
michael@0 | 7225 | |
michael@0 | 7226 | /** |
michael@0 | 7227 | * Gets the directory that the add-on with the given ID is installed in. |
michael@0 | 7228 | * |
michael@0 | 7229 | * @param aId |
michael@0 | 7230 | * The ID of the add-on |
michael@0 | 7231 | * @return The nsIFile |
michael@0 | 7232 | * @throws if the ID does not match any of the add-ons installed |
michael@0 | 7233 | */ |
michael@0 | 7234 | getLocationForID: function DirInstallLocation_getLocationForID(aId) { |
michael@0 | 7235 | if (aId in this._IDToFileMap) |
michael@0 | 7236 | return this._IDToFileMap[aId].clone(); |
michael@0 | 7237 | throw new Error("Unknown add-on ID " + aId); |
michael@0 | 7238 | }, |
michael@0 | 7239 | |
michael@0 | 7240 | /** |
michael@0 | 7241 | * Returns true if the given addon was installed in this location by a text |
michael@0 | 7242 | * file pointing to its real path. |
michael@0 | 7243 | * |
michael@0 | 7244 | * @param aId |
michael@0 | 7245 | * The ID of the addon |
michael@0 | 7246 | */ |
michael@0 | 7247 | isLinkedAddon: function DirInstallLocation__isLinkedAddon(aId) { |
michael@0 | 7248 | return this._linkedAddons.indexOf(aId) != -1; |
michael@0 | 7249 | } |
michael@0 | 7250 | }; |
michael@0 | 7251 | |
michael@0 | 7252 | #ifdef XP_WIN |
michael@0 | 7253 | /** |
michael@0 | 7254 | * An object that identifies a registry install location for add-ons. The location |
michael@0 | 7255 | * consists of a registry key which contains string values mapping ID to the |
michael@0 | 7256 | * path where an add-on is installed |
michael@0 | 7257 | * |
michael@0 | 7258 | * @param aName |
michael@0 | 7259 | * The string identifier of this Install Location. |
michael@0 | 7260 | * @param aRootKey |
michael@0 | 7261 | * The root key (one of the ROOT_KEY_ values from nsIWindowsRegKey). |
michael@0 | 7262 | * @param scope |
michael@0 | 7263 | * The scope of add-ons installed in this location |
michael@0 | 7264 | */ |
michael@0 | 7265 | function WinRegInstallLocation(aName, aRootKey, aScope) { |
michael@0 | 7266 | this.locked = true; |
michael@0 | 7267 | this._name = aName; |
michael@0 | 7268 | this._rootKey = aRootKey; |
michael@0 | 7269 | this._scope = aScope; |
michael@0 | 7270 | this._IDToFileMap = {}; |
michael@0 | 7271 | this._FileToIDMap = {}; |
michael@0 | 7272 | |
michael@0 | 7273 | let path = this._appKeyPath + "\\Extensions"; |
michael@0 | 7274 | let key = Cc["@mozilla.org/windows-registry-key;1"]. |
michael@0 | 7275 | createInstance(Ci.nsIWindowsRegKey); |
michael@0 | 7276 | |
michael@0 | 7277 | // Reading the registry may throw an exception, and that's ok. In error |
michael@0 | 7278 | // cases, we just leave ourselves in the empty state. |
michael@0 | 7279 | try { |
michael@0 | 7280 | key.open(this._rootKey, path, Ci.nsIWindowsRegKey.ACCESS_READ); |
michael@0 | 7281 | } |
michael@0 | 7282 | catch (e) { |
michael@0 | 7283 | return; |
michael@0 | 7284 | } |
michael@0 | 7285 | |
michael@0 | 7286 | this._readAddons(key); |
michael@0 | 7287 | key.close(); |
michael@0 | 7288 | } |
michael@0 | 7289 | |
michael@0 | 7290 | WinRegInstallLocation.prototype = { |
michael@0 | 7291 | _name : "", |
michael@0 | 7292 | _rootKey : null, |
michael@0 | 7293 | _scope : null, |
michael@0 | 7294 | _IDToFileMap : null, // mapping from ID to nsIFile |
michael@0 | 7295 | _FileToIDMap : null, // mapping from path to ID |
michael@0 | 7296 | |
michael@0 | 7297 | /** |
michael@0 | 7298 | * Retrieves the path of this Application's data key in the registry. |
michael@0 | 7299 | */ |
michael@0 | 7300 | get _appKeyPath() { |
michael@0 | 7301 | let appVendor = Services.appinfo.vendor; |
michael@0 | 7302 | let appName = Services.appinfo.name; |
michael@0 | 7303 | |
michael@0 | 7304 | #ifdef MOZ_THUNDERBIRD |
michael@0 | 7305 | // XXX Thunderbird doesn't specify a vendor string |
michael@0 | 7306 | if (appVendor == "") |
michael@0 | 7307 | appVendor = "Mozilla"; |
michael@0 | 7308 | #endif |
michael@0 | 7309 | |
michael@0 | 7310 | // XULRunner-based apps may intentionally not specify a vendor |
michael@0 | 7311 | if (appVendor != "") |
michael@0 | 7312 | appVendor += "\\"; |
michael@0 | 7313 | |
michael@0 | 7314 | return "SOFTWARE\\" + appVendor + appName; |
michael@0 | 7315 | }, |
michael@0 | 7316 | |
michael@0 | 7317 | /** |
michael@0 | 7318 | * Read the registry and build a mapping between ID and path for each |
michael@0 | 7319 | * installed add-on. |
michael@0 | 7320 | * |
michael@0 | 7321 | * @param key |
michael@0 | 7322 | * The key that contains the ID to path mapping |
michael@0 | 7323 | */ |
michael@0 | 7324 | _readAddons: function RegInstallLocation__readAddons(aKey) { |
michael@0 | 7325 | let count = aKey.valueCount; |
michael@0 | 7326 | for (let i = 0; i < count; ++i) { |
michael@0 | 7327 | let id = aKey.getValueName(i); |
michael@0 | 7328 | |
michael@0 | 7329 | let file = Cc["@mozilla.org/file/local;1"]. |
michael@0 | 7330 | createInstance(Ci.nsIFile); |
michael@0 | 7331 | file.initWithPath(aKey.readStringValue(id)); |
michael@0 | 7332 | |
michael@0 | 7333 | if (!file.exists()) { |
michael@0 | 7334 | logger.warn("Ignoring missing add-on in " + file.path); |
michael@0 | 7335 | continue; |
michael@0 | 7336 | } |
michael@0 | 7337 | |
michael@0 | 7338 | this._IDToFileMap[id] = file; |
michael@0 | 7339 | this._FileToIDMap[file.path] = id; |
michael@0 | 7340 | } |
michael@0 | 7341 | }, |
michael@0 | 7342 | |
michael@0 | 7343 | /** |
michael@0 | 7344 | * Gets the name of this install location. |
michael@0 | 7345 | */ |
michael@0 | 7346 | get name() { |
michael@0 | 7347 | return this._name; |
michael@0 | 7348 | }, |
michael@0 | 7349 | |
michael@0 | 7350 | /** |
michael@0 | 7351 | * Gets the scope of this install location. |
michael@0 | 7352 | */ |
michael@0 | 7353 | get scope() { |
michael@0 | 7354 | return this._scope; |
michael@0 | 7355 | }, |
michael@0 | 7356 | |
michael@0 | 7357 | /** |
michael@0 | 7358 | * Gets an array of nsIFiles for add-ons installed in this location. |
michael@0 | 7359 | */ |
michael@0 | 7360 | get addonLocations() { |
michael@0 | 7361 | let locations = []; |
michael@0 | 7362 | for (let id in this._IDToFileMap) { |
michael@0 | 7363 | locations.push(this._IDToFileMap[id].clone()); |
michael@0 | 7364 | } |
michael@0 | 7365 | return locations; |
michael@0 | 7366 | }, |
michael@0 | 7367 | |
michael@0 | 7368 | /** |
michael@0 | 7369 | * Gets the ID of the add-on installed in the given nsIFile. |
michael@0 | 7370 | * |
michael@0 | 7371 | * @param aFile |
michael@0 | 7372 | * The nsIFile to look in |
michael@0 | 7373 | * @return the ID |
michael@0 | 7374 | * @throws if the file does not represent an installed add-on |
michael@0 | 7375 | */ |
michael@0 | 7376 | getIDForLocation: function RegInstallLocation_getIDForLocation(aFile) { |
michael@0 | 7377 | if (aFile.path in this._FileToIDMap) |
michael@0 | 7378 | return this._FileToIDMap[aFile.path]; |
michael@0 | 7379 | throw new Error("Unknown add-on location"); |
michael@0 | 7380 | }, |
michael@0 | 7381 | |
michael@0 | 7382 | /** |
michael@0 | 7383 | * Gets the nsIFile that the add-on with the given ID is installed in. |
michael@0 | 7384 | * |
michael@0 | 7385 | * @param aId |
michael@0 | 7386 | * The ID of the add-on |
michael@0 | 7387 | * @return the nsIFile |
michael@0 | 7388 | */ |
michael@0 | 7389 | getLocationForID: function RegInstallLocation_getLocationForID(aId) { |
michael@0 | 7390 | if (aId in this._IDToFileMap) |
michael@0 | 7391 | return this._IDToFileMap[aId].clone(); |
michael@0 | 7392 | throw new Error("Unknown add-on ID"); |
michael@0 | 7393 | }, |
michael@0 | 7394 | |
michael@0 | 7395 | /** |
michael@0 | 7396 | * @see DirectoryInstallLocation |
michael@0 | 7397 | */ |
michael@0 | 7398 | isLinkedAddon: function RegInstallLocation_isLinkedAddon(aId) { |
michael@0 | 7399 | return true; |
michael@0 | 7400 | } |
michael@0 | 7401 | }; |
michael@0 | 7402 | #endif |
michael@0 | 7403 | |
michael@0 | 7404 | let addonTypes = [ |
michael@0 | 7405 | new AddonManagerPrivate.AddonType("extension", URI_EXTENSION_STRINGS, |
michael@0 | 7406 | STRING_TYPE_NAME, |
michael@0 | 7407 | AddonManager.VIEW_TYPE_LIST, 4000), |
michael@0 | 7408 | new AddonManagerPrivate.AddonType("theme", URI_EXTENSION_STRINGS, |
michael@0 | 7409 | STRING_TYPE_NAME, |
michael@0 | 7410 | AddonManager.VIEW_TYPE_LIST, 5000), |
michael@0 | 7411 | new AddonManagerPrivate.AddonType("dictionary", URI_EXTENSION_STRINGS, |
michael@0 | 7412 | STRING_TYPE_NAME, |
michael@0 | 7413 | AddonManager.VIEW_TYPE_LIST, 7000, |
michael@0 | 7414 | AddonManager.TYPE_UI_HIDE_EMPTY), |
michael@0 | 7415 | new AddonManagerPrivate.AddonType("locale", URI_EXTENSION_STRINGS, |
michael@0 | 7416 | STRING_TYPE_NAME, |
michael@0 | 7417 | AddonManager.VIEW_TYPE_LIST, 8000, |
michael@0 | 7418 | AddonManager.TYPE_UI_HIDE_EMPTY), |
michael@0 | 7419 | ]; |
michael@0 | 7420 | |
michael@0 | 7421 | // We only register experiments support if the application supports them. |
michael@0 | 7422 | // Ideally, we would install an observer to watch the pref. Installing |
michael@0 | 7423 | // an observer for this pref is not necessary here and may be buggy with |
michael@0 | 7424 | // regards to registering this XPIProvider twice. |
michael@0 | 7425 | if (Prefs.getBoolPref("experiments.supported", false)) { |
michael@0 | 7426 | addonTypes.push( |
michael@0 | 7427 | new AddonManagerPrivate.AddonType("experiment", |
michael@0 | 7428 | URI_EXTENSION_STRINGS, |
michael@0 | 7429 | STRING_TYPE_NAME, |
michael@0 | 7430 | AddonManager.VIEW_TYPE_LIST, 11000, |
michael@0 | 7431 | AddonManager.TYPE_UI_HIDE_EMPTY)); |
michael@0 | 7432 | } |
michael@0 | 7433 | |
michael@0 | 7434 | AddonManagerPrivate.registerProvider(XPIProvider, addonTypes); |