1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1505 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +const Cc = Components.classes; 1.11 +const Ci = Components.interfaces; 1.12 +const Cr = Components.results; 1.13 +const Cu = Components.utils; 1.14 + 1.15 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.16 +Cu.import("resource://gre/modules/Services.jsm"); 1.17 +Cu.import("resource://gre/modules/AddonManager.jsm"); 1.18 + 1.19 +XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository", 1.20 + "resource://gre/modules/addons/AddonRepository.jsm"); 1.21 +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", 1.22 + "resource://gre/modules/FileUtils.jsm"); 1.23 +XPCOMUtils.defineLazyModuleGetter(this, "DeferredSave", 1.24 + "resource://gre/modules/DeferredSave.jsm"); 1.25 +XPCOMUtils.defineLazyModuleGetter(this, "Promise", 1.26 + "resource://gre/modules/Promise.jsm"); 1.27 +XPCOMUtils.defineLazyModuleGetter(this, "OS", 1.28 + "resource://gre/modules/osfile.jsm"); 1.29 + 1.30 +Cu.import("resource://gre/modules/Log.jsm"); 1.31 +const LOGGER_ID = "addons.xpi-utils"; 1.32 + 1.33 +// Create a new logger for use by the Addons XPI Provider Utils 1.34 +// (Requires AddonManager.jsm) 1.35 +let logger = Log.repository.getLogger(LOGGER_ID); 1.36 + 1.37 +const KEY_PROFILEDIR = "ProfD"; 1.38 +const FILE_DATABASE = "extensions.sqlite"; 1.39 +const FILE_JSON_DB = "extensions.json"; 1.40 +const FILE_OLD_DATABASE = "extensions.rdf"; 1.41 +const FILE_XPI_ADDONS_LIST = "extensions.ini"; 1.42 + 1.43 +// The value for this is in Makefile.in 1.44 +#expand const DB_SCHEMA = __MOZ_EXTENSIONS_DB_SCHEMA__; 1.45 + 1.46 +// The last version of DB_SCHEMA implemented in SQLITE 1.47 +const LAST_SQLITE_DB_SCHEMA = 14; 1.48 +const PREF_DB_SCHEMA = "extensions.databaseSchema"; 1.49 +const PREF_PENDING_OPERATIONS = "extensions.pendingOperations"; 1.50 +const PREF_EM_ENABLED_ADDONS = "extensions.enabledAddons"; 1.51 +const PREF_EM_DSS_ENABLED = "extensions.dss.enabled"; 1.52 + 1.53 + 1.54 +// Properties that only exist in the database 1.55 +const DB_METADATA = ["syncGUID", 1.56 + "installDate", 1.57 + "updateDate", 1.58 + "size", 1.59 + "sourceURI", 1.60 + "releaseNotesURI", 1.61 + "applyBackgroundUpdates"]; 1.62 +const DB_BOOL_METADATA = ["visible", "active", "userDisabled", "appDisabled", 1.63 + "pendingUninstall", "bootstrap", "skinnable", 1.64 + "softDisabled", "isForeignInstall", 1.65 + "hasBinaryComponents", "strictCompatibility"]; 1.66 + 1.67 +// Properties to save in JSON file 1.68 +const PROP_JSON_FIELDS = ["id", "syncGUID", "location", "version", "type", 1.69 + "internalName", "updateURL", "updateKey", "optionsURL", 1.70 + "optionsType", "aboutURL", "iconURL", "icon64URL", 1.71 + "defaultLocale", "visible", "active", "userDisabled", 1.72 + "appDisabled", "pendingUninstall", "descriptor", "installDate", 1.73 + "updateDate", "applyBackgroundUpdates", "bootstrap", 1.74 + "skinnable", "size", "sourceURI", "releaseNotesURI", 1.75 + "softDisabled", "foreignInstall", "hasBinaryComponents", 1.76 + "strictCompatibility", "locales", "targetApplications", 1.77 + "targetPlatforms"]; 1.78 + 1.79 +// Time to wait before async save of XPI JSON database, in milliseconds 1.80 +const ASYNC_SAVE_DELAY_MS = 20; 1.81 + 1.82 +const PREFIX_ITEM_URI = "urn:mozilla:item:"; 1.83 +const RDFURI_ITEM_ROOT = "urn:mozilla:item:root" 1.84 +const PREFIX_NS_EM = "http://www.mozilla.org/2004/em-rdf#"; 1.85 + 1.86 +XPCOMUtils.defineLazyServiceGetter(this, "gRDF", "@mozilla.org/rdf/rdf-service;1", 1.87 + Ci.nsIRDFService); 1.88 + 1.89 +function EM_R(aProperty) { 1.90 + return gRDF.GetResource(PREFIX_NS_EM + aProperty); 1.91 +} 1.92 + 1.93 +/** 1.94 + * Converts an RDF literal, resource or integer into a string. 1.95 + * 1.96 + * @param aLiteral 1.97 + * The RDF object to convert 1.98 + * @return a string if the object could be converted or null 1.99 + */ 1.100 +function getRDFValue(aLiteral) { 1.101 + if (aLiteral instanceof Ci.nsIRDFLiteral) 1.102 + return aLiteral.Value; 1.103 + if (aLiteral instanceof Ci.nsIRDFResource) 1.104 + return aLiteral.Value; 1.105 + if (aLiteral instanceof Ci.nsIRDFInt) 1.106 + return aLiteral.Value; 1.107 + return null; 1.108 +} 1.109 + 1.110 +/** 1.111 + * Gets an RDF property as a string 1.112 + * 1.113 + * @param aDs 1.114 + * The RDF datasource to read the property from 1.115 + * @param aResource 1.116 + * The RDF resource to read the property from 1.117 + * @param aProperty 1.118 + * The property to read 1.119 + * @return a string if the property existed or null 1.120 + */ 1.121 +function getRDFProperty(aDs, aResource, aProperty) { 1.122 + return getRDFValue(aDs.GetTarget(aResource, EM_R(aProperty), true)); 1.123 +} 1.124 + 1.125 +/** 1.126 + * Asynchronously fill in the _repositoryAddon field for one addon 1.127 + */ 1.128 +function getRepositoryAddon(aAddon, aCallback) { 1.129 + if (!aAddon) { 1.130 + aCallback(aAddon); 1.131 + return; 1.132 + } 1.133 + function completeAddon(aRepositoryAddon) { 1.134 + aAddon._repositoryAddon = aRepositoryAddon; 1.135 + aAddon.compatibilityOverrides = aRepositoryAddon ? 1.136 + aRepositoryAddon.compatibilityOverrides : 1.137 + null; 1.138 + aCallback(aAddon); 1.139 + } 1.140 + AddonRepository.getCachedAddonByID(aAddon.id, completeAddon); 1.141 +} 1.142 + 1.143 +/** 1.144 + * Wrap an API-supplied function in an exception handler to make it safe to call 1.145 + */ 1.146 +function makeSafe(aCallback) { 1.147 + return function(...aArgs) { 1.148 + try { 1.149 + aCallback(...aArgs); 1.150 + } 1.151 + catch(ex) { 1.152 + logger.warn("XPI Database callback failed", ex); 1.153 + } 1.154 + } 1.155 +} 1.156 + 1.157 +/** 1.158 + * A helper method to asynchronously call a function on an array 1.159 + * of objects, calling a callback when function(x) has been gathered 1.160 + * for every element of the array. 1.161 + * WARNING: not currently error-safe; if the async function does not call 1.162 + * our internal callback for any of the array elements, asyncMap will not 1.163 + * call the callback parameter. 1.164 + * 1.165 + * @param aObjects 1.166 + * The array of objects to process asynchronously 1.167 + * @param aMethod 1.168 + * Function with signature function(object, function aCallback(f_of_object)) 1.169 + * @param aCallback 1.170 + * Function with signature f([aMethod(object)]), called when all values 1.171 + * are available 1.172 + */ 1.173 +function asyncMap(aObjects, aMethod, aCallback) { 1.174 + var resultsPending = aObjects.length; 1.175 + var results = [] 1.176 + if (resultsPending == 0) { 1.177 + aCallback(results); 1.178 + return; 1.179 + } 1.180 + 1.181 + function asyncMap_gotValue(aIndex, aValue) { 1.182 + results[aIndex] = aValue; 1.183 + if (--resultsPending == 0) { 1.184 + aCallback(results); 1.185 + } 1.186 + } 1.187 + 1.188 + aObjects.map(function asyncMap_each(aObject, aIndex, aArray) { 1.189 + try { 1.190 + aMethod(aObject, function asyncMap_callback(aResult) { 1.191 + asyncMap_gotValue(aIndex, aResult); 1.192 + }); 1.193 + } 1.194 + catch (e) { 1.195 + logger.warn("Async map function failed", e); 1.196 + asyncMap_gotValue(aIndex, undefined); 1.197 + } 1.198 + }); 1.199 +} 1.200 + 1.201 +/** 1.202 + * A generator to synchronously return result rows from an mozIStorageStatement. 1.203 + * 1.204 + * @param aStatement 1.205 + * The statement to execute 1.206 + */ 1.207 +function resultRows(aStatement) { 1.208 + try { 1.209 + while (stepStatement(aStatement)) 1.210 + yield aStatement.row; 1.211 + } 1.212 + finally { 1.213 + aStatement.reset(); 1.214 + } 1.215 +} 1.216 + 1.217 + 1.218 +/** 1.219 + * A helper function to log an SQL error. 1.220 + * 1.221 + * @param aError 1.222 + * The storage error code associated with the error 1.223 + * @param aErrorString 1.224 + * An error message 1.225 + */ 1.226 +function logSQLError(aError, aErrorString) { 1.227 + logger.error("SQL error " + aError + ": " + aErrorString); 1.228 +} 1.229 + 1.230 +/** 1.231 + * A helper function to log any errors that occur during async statements. 1.232 + * 1.233 + * @param aError 1.234 + * A mozIStorageError to log 1.235 + */ 1.236 +function asyncErrorLogger(aError) { 1.237 + logSQLError(aError.result, aError.message); 1.238 +} 1.239 + 1.240 +/** 1.241 + * A helper function to step a statement synchronously and log any error that 1.242 + * occurs. 1.243 + * 1.244 + * @param aStatement 1.245 + * A mozIStorageStatement to execute 1.246 + */ 1.247 +function stepStatement(aStatement) { 1.248 + try { 1.249 + return aStatement.executeStep(); 1.250 + } 1.251 + catch (e) { 1.252 + logSQLError(XPIDatabase.connection.lastError, 1.253 + XPIDatabase.connection.lastErrorString); 1.254 + throw e; 1.255 + } 1.256 +} 1.257 + 1.258 + 1.259 +/** 1.260 + * Copies properties from one object to another. If no target object is passed 1.261 + * a new object will be created and returned. 1.262 + * 1.263 + * @param aObject 1.264 + * An object to copy from 1.265 + * @param aProperties 1.266 + * An array of properties to be copied 1.267 + * @param aTarget 1.268 + * An optional target object to copy the properties to 1.269 + * @return the object that the properties were copied onto 1.270 + */ 1.271 +function copyProperties(aObject, aProperties, aTarget) { 1.272 + if (!aTarget) 1.273 + aTarget = {}; 1.274 + aProperties.forEach(function(aProp) { 1.275 + aTarget[aProp] = aObject[aProp]; 1.276 + }); 1.277 + return aTarget; 1.278 +} 1.279 + 1.280 +/** 1.281 + * Copies properties from a mozIStorageRow to an object. If no target object is 1.282 + * passed a new object will be created and returned. 1.283 + * 1.284 + * @param aRow 1.285 + * A mozIStorageRow to copy from 1.286 + * @param aProperties 1.287 + * An array of properties to be copied 1.288 + * @param aTarget 1.289 + * An optional target object to copy the properties to 1.290 + * @return the object that the properties were copied onto 1.291 + */ 1.292 +function copyRowProperties(aRow, aProperties, aTarget) { 1.293 + if (!aTarget) 1.294 + aTarget = {}; 1.295 + aProperties.forEach(function(aProp) { 1.296 + aTarget[aProp] = aRow.getResultByName(aProp); 1.297 + }); 1.298 + return aTarget; 1.299 +} 1.300 + 1.301 +/** 1.302 + * The DBAddonInternal is a special AddonInternal that has been retrieved from 1.303 + * the database. The constructor will initialize the DBAddonInternal with a set 1.304 + * of fields, which could come from either the JSON store or as an 1.305 + * XPIProvider.AddonInternal created from an addon's manifest 1.306 + * @constructor 1.307 + * @param aLoaded 1.308 + * Addon data fields loaded from JSON or the addon manifest. 1.309 + */ 1.310 +function DBAddonInternal(aLoaded) { 1.311 + copyProperties(aLoaded, PROP_JSON_FIELDS, this); 1.312 + 1.313 + if (aLoaded._installLocation) { 1.314 + this._installLocation = aLoaded._installLocation; 1.315 + this.location = aLoaded._installLocation._name; 1.316 + } 1.317 + else if (aLoaded.location) { 1.318 + this._installLocation = XPIProvider.installLocationsByName[this.location]; 1.319 + } 1.320 + 1.321 + this._key = this.location + ":" + this.id; 1.322 + 1.323 + try { 1.324 + this._sourceBundle = this._installLocation.getLocationForID(this.id); 1.325 + } 1.326 + catch (e) { 1.327 + // An exception will be thrown if the add-on appears in the database but 1.328 + // not on disk. In general this should only happen during startup as 1.329 + // this change is being detected. 1.330 + } 1.331 + 1.332 + XPCOMUtils.defineLazyGetter(this, "pendingUpgrade", 1.333 + function DBA_pendingUpgradeGetter() { 1.334 + for (let install of XPIProvider.installs) { 1.335 + if (install.state == AddonManager.STATE_INSTALLED && 1.336 + !(install.addon.inDatabase) && 1.337 + install.addon.id == this.id && 1.338 + install.installLocation == this._installLocation) { 1.339 + delete this.pendingUpgrade; 1.340 + return this.pendingUpgrade = install.addon; 1.341 + } 1.342 + }; 1.343 + return null; 1.344 + }); 1.345 +} 1.346 + 1.347 +function DBAddonInternalPrototype() 1.348 +{ 1.349 + this.applyCompatibilityUpdate = 1.350 + function(aUpdate, aSyncCompatibility) { 1.351 + this.targetApplications.forEach(function(aTargetApp) { 1.352 + aUpdate.targetApplications.forEach(function(aUpdateTarget) { 1.353 + if (aTargetApp.id == aUpdateTarget.id && (aSyncCompatibility || 1.354 + Services.vc.compare(aTargetApp.maxVersion, aUpdateTarget.maxVersion) < 0)) { 1.355 + aTargetApp.minVersion = aUpdateTarget.minVersion; 1.356 + aTargetApp.maxVersion = aUpdateTarget.maxVersion; 1.357 + XPIDatabase.saveChanges(); 1.358 + } 1.359 + }); 1.360 + }); 1.361 + XPIProvider.updateAddonDisabledState(this); 1.362 + }; 1.363 + 1.364 + this.toJSON = 1.365 + function() { 1.366 + return copyProperties(this, PROP_JSON_FIELDS); 1.367 + }; 1.368 + 1.369 + Object.defineProperty(this, "inDatabase", 1.370 + { get: function() { return true; }, 1.371 + enumerable: true, 1.372 + configurable: true }); 1.373 +} 1.374 +DBAddonInternalPrototype.prototype = AddonInternal.prototype; 1.375 + 1.376 +DBAddonInternal.prototype = new DBAddonInternalPrototype(); 1.377 + 1.378 +/** 1.379 + * Internal interface: find an addon from an already loaded addonDB 1.380 + */ 1.381 +function _findAddon(addonDB, aFilter) { 1.382 + for (let [, addon] of addonDB) { 1.383 + if (aFilter(addon)) { 1.384 + return addon; 1.385 + } 1.386 + } 1.387 + return null; 1.388 +} 1.389 + 1.390 +/** 1.391 + * Internal interface to get a filtered list of addons from a loaded addonDB 1.392 + */ 1.393 +function _filterDB(addonDB, aFilter) { 1.394 + let addonList = []; 1.395 + for (let [, addon] of addonDB) { 1.396 + if (aFilter(addon)) { 1.397 + addonList.push(addon); 1.398 + } 1.399 + } 1.400 + 1.401 + return addonList; 1.402 +} 1.403 + 1.404 +this.XPIDatabase = { 1.405 + // true if the database connection has been opened 1.406 + initialized: false, 1.407 + // The database file 1.408 + jsonFile: FileUtils.getFile(KEY_PROFILEDIR, [FILE_JSON_DB], true), 1.409 + // Migration data loaded from an old version of the database. 1.410 + migrateData: null, 1.411 + // Active add-on directories loaded from extensions.ini and prefs at startup. 1.412 + activeBundles: null, 1.413 + 1.414 + // Saved error object if we fail to read an existing database 1.415 + _loadError: null, 1.416 + 1.417 + // Error reported by our most recent attempt to read or write the database, if any 1.418 + get lastError() { 1.419 + if (this._loadError) 1.420 + return this._loadError; 1.421 + if (this._deferredSave) 1.422 + return this._deferredSave.lastError; 1.423 + return null; 1.424 + }, 1.425 + 1.426 + /** 1.427 + * Mark the current stored data dirty, and schedule a flush to disk 1.428 + */ 1.429 + saveChanges: function() { 1.430 + if (!this.initialized) { 1.431 + throw new Error("Attempt to use XPI database when it is not initialized"); 1.432 + } 1.433 + 1.434 + if (!this._deferredSave) { 1.435 + this._deferredSave = new DeferredSave(this.jsonFile.path, 1.436 + () => JSON.stringify(this), 1.437 + ASYNC_SAVE_DELAY_MS); 1.438 + } 1.439 + 1.440 + let promise = this._deferredSave.saveChanges(); 1.441 + if (!this._schemaVersionSet) { 1.442 + this._schemaVersionSet = true; 1.443 + promise.then( 1.444 + count => { 1.445 + // Update the XPIDB schema version preference the first time we successfully 1.446 + // save the database. 1.447 + logger.debug("XPI Database saved, setting schema version preference to " + DB_SCHEMA); 1.448 + Services.prefs.setIntPref(PREF_DB_SCHEMA, DB_SCHEMA); 1.449 + // Reading the DB worked once, so we don't need the load error 1.450 + this._loadError = null; 1.451 + }, 1.452 + error => { 1.453 + // Need to try setting the schema version again later 1.454 + this._schemaVersionSet = false; 1.455 + logger.warn("Failed to save XPI database", error); 1.456 + // this._deferredSave.lastError has the most recent error so we don't 1.457 + // need this any more 1.458 + this._loadError = null; 1.459 + }); 1.460 + } 1.461 + }, 1.462 + 1.463 + flush: function() { 1.464 + // handle the "in memory only" and "saveChanges never called" cases 1.465 + if (!this._deferredSave) { 1.466 + return Promise.resolve(0); 1.467 + } 1.468 + 1.469 + return this._deferredSave.flush(); 1.470 + }, 1.471 + 1.472 + /** 1.473 + * Converts the current internal state of the XPI addon database to 1.474 + * a JSON.stringify()-ready structure 1.475 + */ 1.476 + toJSON: function() { 1.477 + if (!this.addonDB) { 1.478 + // We never loaded the database? 1.479 + throw new Error("Attempt to save database without loading it first"); 1.480 + } 1.481 + 1.482 + let toSave = { 1.483 + schemaVersion: DB_SCHEMA, 1.484 + addons: [...this.addonDB.values()] 1.485 + }; 1.486 + return toSave; 1.487 + }, 1.488 + 1.489 + /** 1.490 + * Pull upgrade information from an existing SQLITE database 1.491 + * 1.492 + * @return false if there is no SQLITE database 1.493 + * true and sets this.migrateData to null if the SQLITE DB exists 1.494 + * but does not contain useful information 1.495 + * true and sets this.migrateData to 1.496 + * {location: {id1:{addon1}, id2:{addon2}}, location2:{...}, ...} 1.497 + * if there is useful information 1.498 + */ 1.499 + getMigrateDataFromSQLITE: function XPIDB_getMigrateDataFromSQLITE() { 1.500 + let connection = null; 1.501 + let dbfile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_DATABASE], true); 1.502 + // Attempt to open the database 1.503 + try { 1.504 + connection = Services.storage.openUnsharedDatabase(dbfile); 1.505 + } 1.506 + catch (e) { 1.507 + logger.warn("Failed to open sqlite database " + dbfile.path + " for upgrade", e); 1.508 + return null; 1.509 + } 1.510 + logger.debug("Migrating data from sqlite"); 1.511 + let migrateData = this.getMigrateDataFromDatabase(connection); 1.512 + connection.close(); 1.513 + return migrateData; 1.514 + }, 1.515 + 1.516 + /** 1.517 + * Synchronously opens and reads the database file, upgrading from old 1.518 + * databases or making a new DB if needed. 1.519 + * 1.520 + * The possibilities, in order of priority, are: 1.521 + * 1) Perfectly good, up to date database 1.522 + * 2) Out of date JSON database needs to be upgraded => upgrade 1.523 + * 3) JSON database exists but is mangled somehow => build new JSON 1.524 + * 4) no JSON DB, but a useable SQLITE db we can upgrade from => upgrade 1.525 + * 5) useless SQLITE DB => build new JSON 1.526 + * 6) useable RDF DB => upgrade 1.527 + * 7) useless RDF DB => build new JSON 1.528 + * 8) Nothing at all => build new JSON 1.529 + * @param aRebuildOnError 1.530 + * A boolean indicating whether add-on information should be loaded 1.531 + * from the install locations if the database needs to be rebuilt. 1.532 + * (if false, caller is XPIProvider.checkForChanges() which will rebuild) 1.533 + */ 1.534 + syncLoadDB: function XPIDB_syncLoadDB(aRebuildOnError) { 1.535 + this.migrateData = null; 1.536 + let fstream = null; 1.537 + let data = ""; 1.538 + try { 1.539 + let readTimer = AddonManagerPrivate.simpleTimer("XPIDB_syncRead_MS"); 1.540 + logger.debug("Opening XPI database " + this.jsonFile.path); 1.541 + fstream = Components.classes["@mozilla.org/network/file-input-stream;1"]. 1.542 + createInstance(Components.interfaces.nsIFileInputStream); 1.543 + fstream.init(this.jsonFile, -1, 0, 0); 1.544 + let cstream = null; 1.545 + try { 1.546 + cstream = Components.classes["@mozilla.org/intl/converter-input-stream;1"]. 1.547 + createInstance(Components.interfaces.nsIConverterInputStream); 1.548 + cstream.init(fstream, "UTF-8", 0, 0); 1.549 + let (str = {}) { 1.550 + let read = 0; 1.551 + do { 1.552 + read = cstream.readString(0xffffffff, str); // read as much as we can and put it in str.value 1.553 + data += str.value; 1.554 + } while (read != 0); 1.555 + } 1.556 + readTimer.done(); 1.557 + this.parseDB(data, aRebuildOnError); 1.558 + } 1.559 + catch(e) { 1.560 + logger.error("Failed to load XPI JSON data from profile", e); 1.561 + let rebuildTimer = AddonManagerPrivate.simpleTimer("XPIDB_rebuildReadFailed_MS"); 1.562 + this.rebuildDatabase(aRebuildOnError); 1.563 + rebuildTimer.done(); 1.564 + } 1.565 + finally { 1.566 + if (cstream) 1.567 + cstream.close(); 1.568 + } 1.569 + } 1.570 + catch (e) { 1.571 + if (e.result === Cr.NS_ERROR_FILE_NOT_FOUND) { 1.572 + this.upgradeDB(aRebuildOnError); 1.573 + } 1.574 + else { 1.575 + this.rebuildUnreadableDB(e, aRebuildOnError); 1.576 + } 1.577 + } 1.578 + finally { 1.579 + if (fstream) 1.580 + fstream.close(); 1.581 + } 1.582 + // If an async load was also in progress, resolve that promise with our DB; 1.583 + // otherwise create a resolved promise 1.584 + if (this._dbPromise) { 1.585 + AddonManagerPrivate.recordSimpleMeasure("XPIDB_overlapped_load", 1); 1.586 + this._dbPromise.resolve(this.addonDB); 1.587 + } 1.588 + else 1.589 + this._dbPromise = Promise.resolve(this.addonDB); 1.590 + }, 1.591 + 1.592 + /** 1.593 + * Parse loaded data, reconstructing the database if the loaded data is not valid 1.594 + * @param aRebuildOnError 1.595 + * If true, synchronously reconstruct the database from installed add-ons 1.596 + */ 1.597 + parseDB: function(aData, aRebuildOnError) { 1.598 + let parseTimer = AddonManagerPrivate.simpleTimer("XPIDB_parseDB_MS"); 1.599 + try { 1.600 + // dump("Loaded JSON:\n" + aData + "\n"); 1.601 + let inputAddons = JSON.parse(aData); 1.602 + // Now do some sanity checks on our JSON db 1.603 + if (!("schemaVersion" in inputAddons) || !("addons" in inputAddons)) { 1.604 + parseTimer.done(); 1.605 + // Content of JSON file is bad, need to rebuild from scratch 1.606 + logger.error("bad JSON file contents"); 1.607 + AddonManagerPrivate.recordSimpleMeasure("XPIDB_startupError", "badJSON"); 1.608 + let rebuildTimer = AddonManagerPrivate.simpleTimer("XPIDB_rebuildBadJSON_MS"); 1.609 + this.rebuildDatabase(aRebuildOnError); 1.610 + rebuildTimer.done(); 1.611 + return; 1.612 + } 1.613 + if (inputAddons.schemaVersion != DB_SCHEMA) { 1.614 + // Handle mismatched JSON schema version. For now, we assume 1.615 + // compatibility for JSON data, though we throw away any fields we 1.616 + // don't know about (bug 902956) 1.617 + AddonManagerPrivate.recordSimpleMeasure("XPIDB_startupError", 1.618 + "schemaMismatch-" + inputAddons.schemaVersion); 1.619 + logger.debug("JSON schema mismatch: expected " + DB_SCHEMA + 1.620 + ", actual " + inputAddons.schemaVersion); 1.621 + // When we rev the schema of the JSON database, we need to make sure we 1.622 + // force the DB to save so that the DB_SCHEMA value in the JSON file and 1.623 + // the preference are updated. 1.624 + } 1.625 + // If we got here, we probably have good data 1.626 + // Make AddonInternal instances from the loaded data and save them 1.627 + let addonDB = new Map(); 1.628 + for (let loadedAddon of inputAddons.addons) { 1.629 + let newAddon = new DBAddonInternal(loadedAddon); 1.630 + addonDB.set(newAddon._key, newAddon); 1.631 + }; 1.632 + parseTimer.done(); 1.633 + this.addonDB = addonDB; 1.634 + logger.debug("Successfully read XPI database"); 1.635 + this.initialized = true; 1.636 + } 1.637 + catch(e) { 1.638 + // If we catch and log a SyntaxError from the JSON 1.639 + // parser, the xpcshell test harness fails the test for us: bug 870828 1.640 + parseTimer.done(); 1.641 + if (e.name == "SyntaxError") { 1.642 + logger.error("Syntax error parsing saved XPI JSON data"); 1.643 + AddonManagerPrivate.recordSimpleMeasure("XPIDB_startupError", "syntax"); 1.644 + } 1.645 + else { 1.646 + logger.error("Failed to load XPI JSON data from profile", e); 1.647 + AddonManagerPrivate.recordSimpleMeasure("XPIDB_startupError", "other"); 1.648 + } 1.649 + let rebuildTimer = AddonManagerPrivate.simpleTimer("XPIDB_rebuildReadFailed_MS"); 1.650 + this.rebuildDatabase(aRebuildOnError); 1.651 + rebuildTimer.done(); 1.652 + } 1.653 + }, 1.654 + 1.655 + /** 1.656 + * Upgrade database from earlier (sqlite or RDF) version if available 1.657 + */ 1.658 + upgradeDB: function(aRebuildOnError) { 1.659 + let upgradeTimer = AddonManagerPrivate.simpleTimer("XPIDB_upgradeDB_MS"); 1.660 + try { 1.661 + let schemaVersion = Services.prefs.getIntPref(PREF_DB_SCHEMA); 1.662 + if (schemaVersion <= LAST_SQLITE_DB_SCHEMA) { 1.663 + // we should have an older SQLITE database 1.664 + logger.debug("Attempting to upgrade from SQLITE database"); 1.665 + this.migrateData = this.getMigrateDataFromSQLITE(); 1.666 + } 1.667 + else { 1.668 + // we've upgraded before but the JSON file is gone, fall through 1.669 + // and rebuild from scratch 1.670 + AddonManagerPrivate.recordSimpleMeasure("XPIDB_startupError", "dbMissing"); 1.671 + } 1.672 + } 1.673 + catch(e) { 1.674 + // No schema version pref means either a really old upgrade (RDF) or 1.675 + // a new profile 1.676 + this.migrateData = this.getMigrateDataFromRDF(); 1.677 + } 1.678 + 1.679 + this.rebuildDatabase(aRebuildOnError); 1.680 + upgradeTimer.done(); 1.681 + }, 1.682 + 1.683 + /** 1.684 + * Reconstruct when the DB file exists but is unreadable 1.685 + * (for example because read permission is denied) 1.686 + */ 1.687 + rebuildUnreadableDB: function(aError, aRebuildOnError) { 1.688 + let rebuildTimer = AddonManagerPrivate.simpleTimer("XPIDB_rebuildUnreadableDB_MS"); 1.689 + logger.warn("Extensions database " + this.jsonFile.path + 1.690 + " exists but is not readable; rebuilding", aError); 1.691 + // Remember the error message until we try and write at least once, so 1.692 + // we know at shutdown time that there was a problem 1.693 + this._loadError = aError; 1.694 + AddonManagerPrivate.recordSimpleMeasure("XPIDB_startupError", "unreadable"); 1.695 + this.rebuildDatabase(aRebuildOnError); 1.696 + rebuildTimer.done(); 1.697 + }, 1.698 + 1.699 + /** 1.700 + * Open and read the XPI database asynchronously, upgrading if 1.701 + * necessary. If any DB load operation fails, we need to 1.702 + * synchronously rebuild the DB from the installed extensions. 1.703 + * 1.704 + * @return Promise<Map> resolves to the Map of loaded JSON data stored 1.705 + * in this.addonDB; never rejects. 1.706 + */ 1.707 + asyncLoadDB: function XPIDB_asyncLoadDB() { 1.708 + // Already started (and possibly finished) loading 1.709 + if (this._dbPromise) { 1.710 + return this._dbPromise; 1.711 + } 1.712 + 1.713 + logger.debug("Starting async load of XPI database " + this.jsonFile.path); 1.714 + AddonManagerPrivate.recordSimpleMeasure("XPIDB_async_load", XPIProvider.runPhase); 1.715 + let readOptions = { 1.716 + outExecutionDuration: 0 1.717 + }; 1.718 + return this._dbPromise = OS.File.read(this.jsonFile.path, null, readOptions).then( 1.719 + byteArray => { 1.720 + logger.debug("Async JSON file read took " + readOptions.outExecutionDuration + " MS"); 1.721 + AddonManagerPrivate.recordSimpleMeasure("XPIDB_asyncRead_MS", 1.722 + readOptions.outExecutionDuration); 1.723 + if (this._addonDB) { 1.724 + logger.debug("Synchronous load completed while waiting for async load"); 1.725 + return this.addonDB; 1.726 + } 1.727 + logger.debug("Finished async read of XPI database, parsing..."); 1.728 + let decodeTimer = AddonManagerPrivate.simpleTimer("XPIDB_decode_MS"); 1.729 + let decoder = new TextDecoder(); 1.730 + let data = decoder.decode(byteArray); 1.731 + decodeTimer.done(); 1.732 + this.parseDB(data, true); 1.733 + return this.addonDB; 1.734 + }) 1.735 + .then(null, 1.736 + error => { 1.737 + if (this._addonDB) { 1.738 + logger.debug("Synchronous load completed while waiting for async load"); 1.739 + return this.addonDB; 1.740 + } 1.741 + if (error.becauseNoSuchFile) { 1.742 + this.upgradeDB(true); 1.743 + } 1.744 + else { 1.745 + // it's there but unreadable 1.746 + this.rebuildUnreadableDB(error, true); 1.747 + } 1.748 + return this.addonDB; 1.749 + }); 1.750 + }, 1.751 + 1.752 + /** 1.753 + * Rebuild the database from addon install directories. If this.migrateData 1.754 + * is available, uses migrated information for settings on the addons found 1.755 + * during rebuild 1.756 + * @param aRebuildOnError 1.757 + * A boolean indicating whether add-on information should be loaded 1.758 + * from the install locations if the database needs to be rebuilt. 1.759 + * (if false, caller is XPIProvider.checkForChanges() which will rebuild) 1.760 + */ 1.761 + rebuildDatabase: function XIPDB_rebuildDatabase(aRebuildOnError) { 1.762 + this.addonDB = new Map(); 1.763 + this.initialized = true; 1.764 + 1.765 + if (XPIProvider.installStates && XPIProvider.installStates.length == 0) { 1.766 + // No extensions installed, so we're done 1.767 + logger.debug("Rebuilding XPI database with no extensions"); 1.768 + return; 1.769 + } 1.770 + 1.771 + // If there is no migration data then load the list of add-on directories 1.772 + // that were active during the last run 1.773 + if (!this.migrateData) 1.774 + this.activeBundles = this.getActiveBundles(); 1.775 + 1.776 + if (aRebuildOnError) { 1.777 + logger.warn("Rebuilding add-ons database from installed extensions."); 1.778 + try { 1.779 + XPIProvider.processFileChanges(XPIProvider.installStates, {}, false); 1.780 + } 1.781 + catch (e) { 1.782 + logger.error("Failed to rebuild XPI database from installed extensions", e); 1.783 + } 1.784 + // Make sure to update the active add-ons and add-ons list on shutdown 1.785 + Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true); 1.786 + } 1.787 + }, 1.788 + 1.789 + /** 1.790 + * Gets the list of file descriptors of active extension directories or XPI 1.791 + * files from the add-ons list. This must be loaded from disk since the 1.792 + * directory service gives no easy way to get both directly. This list doesn't 1.793 + * include themes as preferences already say which theme is currently active 1.794 + * 1.795 + * @return an array of persistent descriptors for the directories 1.796 + */ 1.797 + getActiveBundles: function XPIDB_getActiveBundles() { 1.798 + let bundles = []; 1.799 + 1.800 + // non-bootstrapped extensions 1.801 + let addonsList = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST], 1.802 + true); 1.803 + 1.804 + if (!addonsList.exists()) 1.805 + // XXX Irving believes this is broken in the case where there is no 1.806 + // extensions.ini but there are bootstrap extensions (e.g. Android) 1.807 + return null; 1.808 + 1.809 + try { 1.810 + let iniFactory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"] 1.811 + .getService(Ci.nsIINIParserFactory); 1.812 + let parser = iniFactory.createINIParser(addonsList); 1.813 + let keys = parser.getKeys("ExtensionDirs"); 1.814 + 1.815 + while (keys.hasMore()) 1.816 + bundles.push(parser.getString("ExtensionDirs", keys.getNext())); 1.817 + } 1.818 + catch (e) { 1.819 + logger.warn("Failed to parse extensions.ini", e); 1.820 + return null; 1.821 + } 1.822 + 1.823 + // Also include the list of active bootstrapped extensions 1.824 + for (let id in XPIProvider.bootstrappedAddons) 1.825 + bundles.push(XPIProvider.bootstrappedAddons[id].descriptor); 1.826 + 1.827 + return bundles; 1.828 + }, 1.829 + 1.830 + /** 1.831 + * Retrieves migration data from the old extensions.rdf database. 1.832 + * 1.833 + * @return an object holding information about what add-ons were previously 1.834 + * userDisabled and any updated compatibility information 1.835 + */ 1.836 + getMigrateDataFromRDF: function XPIDB_getMigrateDataFromRDF(aDbWasMissing) { 1.837 + 1.838 + // Migrate data from extensions.rdf 1.839 + let rdffile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_OLD_DATABASE], true); 1.840 + if (!rdffile.exists()) 1.841 + return null; 1.842 + 1.843 + logger.debug("Migrating data from " + FILE_OLD_DATABASE); 1.844 + let migrateData = {}; 1.845 + 1.846 + try { 1.847 + let ds = gRDF.GetDataSourceBlocking(Services.io.newFileURI(rdffile).spec); 1.848 + let root = Cc["@mozilla.org/rdf/container;1"]. 1.849 + createInstance(Ci.nsIRDFContainer); 1.850 + root.Init(ds, gRDF.GetResource(RDFURI_ITEM_ROOT)); 1.851 + let elements = root.GetElements(); 1.852 + 1.853 + while (elements.hasMoreElements()) { 1.854 + let source = elements.getNext().QueryInterface(Ci.nsIRDFResource); 1.855 + 1.856 + let location = getRDFProperty(ds, source, "installLocation"); 1.857 + if (location) { 1.858 + if (!(location in migrateData)) 1.859 + migrateData[location] = {}; 1.860 + let id = source.ValueUTF8.substring(PREFIX_ITEM_URI.length); 1.861 + migrateData[location][id] = { 1.862 + version: getRDFProperty(ds, source, "version"), 1.863 + userDisabled: false, 1.864 + targetApplications: [] 1.865 + } 1.866 + 1.867 + let disabled = getRDFProperty(ds, source, "userDisabled"); 1.868 + if (disabled == "true" || disabled == "needs-disable") 1.869 + migrateData[location][id].userDisabled = true; 1.870 + 1.871 + let targetApps = ds.GetTargets(source, EM_R("targetApplication"), 1.872 + true); 1.873 + while (targetApps.hasMoreElements()) { 1.874 + let targetApp = targetApps.getNext() 1.875 + .QueryInterface(Ci.nsIRDFResource); 1.876 + let appInfo = { 1.877 + id: getRDFProperty(ds, targetApp, "id") 1.878 + }; 1.879 + 1.880 + let minVersion = getRDFProperty(ds, targetApp, "updatedMinVersion"); 1.881 + if (minVersion) { 1.882 + appInfo.minVersion = minVersion; 1.883 + appInfo.maxVersion = getRDFProperty(ds, targetApp, "updatedMaxVersion"); 1.884 + } 1.885 + else { 1.886 + appInfo.minVersion = getRDFProperty(ds, targetApp, "minVersion"); 1.887 + appInfo.maxVersion = getRDFProperty(ds, targetApp, "maxVersion"); 1.888 + } 1.889 + migrateData[location][id].targetApplications.push(appInfo); 1.890 + } 1.891 + } 1.892 + } 1.893 + } 1.894 + catch (e) { 1.895 + logger.warn("Error reading " + FILE_OLD_DATABASE, e); 1.896 + migrateData = null; 1.897 + } 1.898 + 1.899 + return migrateData; 1.900 + }, 1.901 + 1.902 + /** 1.903 + * Retrieves migration data from a database that has an older or newer schema. 1.904 + * 1.905 + * @return an object holding information about what add-ons were previously 1.906 + * userDisabled and any updated compatibility information 1.907 + */ 1.908 + getMigrateDataFromDatabase: function XPIDB_getMigrateDataFromDatabase(aConnection) { 1.909 + let migrateData = {}; 1.910 + 1.911 + // Attempt to migrate data from a different (even future!) version of the 1.912 + // database 1.913 + try { 1.914 + var stmt = aConnection.createStatement("PRAGMA table_info(addon)"); 1.915 + 1.916 + const REQUIRED = ["internal_id", "id", "location", "userDisabled", 1.917 + "installDate", "version"]; 1.918 + 1.919 + let reqCount = 0; 1.920 + let props = []; 1.921 + for (let row in resultRows(stmt)) { 1.922 + if (REQUIRED.indexOf(row.name) != -1) { 1.923 + reqCount++; 1.924 + props.push(row.name); 1.925 + } 1.926 + else if (DB_METADATA.indexOf(row.name) != -1) { 1.927 + props.push(row.name); 1.928 + } 1.929 + else if (DB_BOOL_METADATA.indexOf(row.name) != -1) { 1.930 + props.push(row.name); 1.931 + } 1.932 + } 1.933 + 1.934 + if (reqCount < REQUIRED.length) { 1.935 + logger.error("Unable to read anything useful from the database"); 1.936 + return null; 1.937 + } 1.938 + stmt.finalize(); 1.939 + 1.940 + stmt = aConnection.createStatement("SELECT " + props.join(",") + " FROM addon"); 1.941 + for (let row in resultRows(stmt)) { 1.942 + if (!(row.location in migrateData)) 1.943 + migrateData[row.location] = {}; 1.944 + let addonData = { 1.945 + targetApplications: [] 1.946 + } 1.947 + migrateData[row.location][row.id] = addonData; 1.948 + 1.949 + props.forEach(function(aProp) { 1.950 + if (aProp == "isForeignInstall") 1.951 + addonData.foreignInstall = (row[aProp] == 1); 1.952 + if (DB_BOOL_METADATA.indexOf(aProp) != -1) 1.953 + addonData[aProp] = row[aProp] == 1; 1.954 + else 1.955 + addonData[aProp] = row[aProp]; 1.956 + }) 1.957 + } 1.958 + 1.959 + var taStmt = aConnection.createStatement("SELECT id, minVersion, " + 1.960 + "maxVersion FROM " + 1.961 + "targetApplication WHERE " + 1.962 + "addon_internal_id=:internal_id"); 1.963 + 1.964 + for (let location in migrateData) { 1.965 + for (let id in migrateData[location]) { 1.966 + taStmt.params.internal_id = migrateData[location][id].internal_id; 1.967 + delete migrateData[location][id].internal_id; 1.968 + for (let row in resultRows(taStmt)) { 1.969 + migrateData[location][id].targetApplications.push({ 1.970 + id: row.id, 1.971 + minVersion: row.minVersion, 1.972 + maxVersion: row.maxVersion 1.973 + }); 1.974 + } 1.975 + } 1.976 + } 1.977 + } 1.978 + catch (e) { 1.979 + // An error here means the schema is too different to read 1.980 + logger.error("Error migrating data", e); 1.981 + return null; 1.982 + } 1.983 + finally { 1.984 + if (taStmt) 1.985 + taStmt.finalize(); 1.986 + if (stmt) 1.987 + stmt.finalize(); 1.988 + } 1.989 + 1.990 + return migrateData; 1.991 + }, 1.992 + 1.993 + /** 1.994 + * Shuts down the database connection and releases all cached objects. 1.995 + * Return: Promise{integer} resolves / rejects with the result of the DB 1.996 + * flush after the database is flushed and 1.997 + * all cleanup is done 1.998 + */ 1.999 + shutdown: function XPIDB_shutdown() { 1.1000 + logger.debug("shutdown"); 1.1001 + if (this.initialized) { 1.1002 + // If our last database I/O had an error, try one last time to save. 1.1003 + if (this.lastError) 1.1004 + this.saveChanges(); 1.1005 + 1.1006 + this.initialized = false; 1.1007 + 1.1008 + if (this._deferredSave) { 1.1009 + AddonManagerPrivate.recordSimpleMeasure( 1.1010 + "XPIDB_saves_total", this._deferredSave.totalSaves); 1.1011 + AddonManagerPrivate.recordSimpleMeasure( 1.1012 + "XPIDB_saves_overlapped", this._deferredSave.overlappedSaves); 1.1013 + AddonManagerPrivate.recordSimpleMeasure( 1.1014 + "XPIDB_saves_late", this._deferredSave.dirty ? 1 : 0); 1.1015 + } 1.1016 + 1.1017 + // Return a promise that any pending writes of the DB are complete and we 1.1018 + // are finished cleaning up 1.1019 + let flushPromise = this.flush(); 1.1020 + flushPromise.then(null, error => { 1.1021 + logger.error("Flush of XPI database failed", error); 1.1022 + AddonManagerPrivate.recordSimpleMeasure("XPIDB_shutdownFlush_failed", 1); 1.1023 + // If our last attempt to read or write the DB failed, force a new 1.1024 + // extensions.ini to be written to disk on the next startup 1.1025 + Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true); 1.1026 + }) 1.1027 + .then(count => { 1.1028 + // Clear out the cached addons data loaded from JSON 1.1029 + delete this.addonDB; 1.1030 + delete this._dbPromise; 1.1031 + // same for the deferred save 1.1032 + delete this._deferredSave; 1.1033 + // re-enable the schema version setter 1.1034 + delete this._schemaVersionSet; 1.1035 + }); 1.1036 + return flushPromise; 1.1037 + } 1.1038 + return Promise.resolve(0); 1.1039 + }, 1.1040 + 1.1041 + /** 1.1042 + * Return a list of all install locations known about by the database. This 1.1043 + * is often a a subset of the total install locations when not all have 1.1044 + * installed add-ons, occasionally a superset when an install location no 1.1045 + * longer exists. Only called from XPIProvider.processFileChanges, when 1.1046 + * the database should already be loaded. 1.1047 + * 1.1048 + * @return a Set of names of install locations 1.1049 + */ 1.1050 + getInstallLocations: function XPIDB_getInstallLocations() { 1.1051 + let locations = new Set(); 1.1052 + if (!this.addonDB) 1.1053 + return locations; 1.1054 + 1.1055 + for (let [, addon] of this.addonDB) { 1.1056 + locations.add(addon.location); 1.1057 + } 1.1058 + return locations; 1.1059 + }, 1.1060 + 1.1061 + /** 1.1062 + * Asynchronously list all addons that match the filter function 1.1063 + * @param aFilter 1.1064 + * Function that takes an addon instance and returns 1.1065 + * true if that addon should be included in the selected array 1.1066 + * @param aCallback 1.1067 + * Called back with an array of addons matching aFilter 1.1068 + * or an empty array if none match 1.1069 + */ 1.1070 + getAddonList: function(aFilter, aCallback) { 1.1071 + this.asyncLoadDB().then( 1.1072 + addonDB => { 1.1073 + let addonList = _filterDB(addonDB, aFilter); 1.1074 + asyncMap(addonList, getRepositoryAddon, makeSafe(aCallback)); 1.1075 + }) 1.1076 + .then(null, 1.1077 + error => { 1.1078 + logger.error("getAddonList failed", error); 1.1079 + makeSafe(aCallback)([]); 1.1080 + }); 1.1081 + }, 1.1082 + 1.1083 + /** 1.1084 + * (Possibly asynchronously) get the first addon that matches the filter function 1.1085 + * @param aFilter 1.1086 + * Function that takes an addon instance and returns 1.1087 + * true if that addon should be selected 1.1088 + * @param aCallback 1.1089 + * Called back with the addon, or null if no matching addon is found 1.1090 + */ 1.1091 + getAddon: function(aFilter, aCallback) { 1.1092 + return this.asyncLoadDB().then( 1.1093 + addonDB => { 1.1094 + getRepositoryAddon(_findAddon(addonDB, aFilter), makeSafe(aCallback)); 1.1095 + }) 1.1096 + .then(null, 1.1097 + error => { 1.1098 + logger.error("getAddon failed", e); 1.1099 + makeSafe(aCallback)(null); 1.1100 + }); 1.1101 + }, 1.1102 + 1.1103 + /** 1.1104 + * Synchronously reads all the add-ons in a particular install location. 1.1105 + * Always called with the addon database already loaded. 1.1106 + * 1.1107 + * @param aLocation 1.1108 + * The name of the install location 1.1109 + * @return an array of DBAddonInternals 1.1110 + */ 1.1111 + getAddonsInLocation: function XPIDB_getAddonsInLocation(aLocation) { 1.1112 + return _filterDB(this.addonDB, aAddon => (aAddon.location == aLocation)); 1.1113 + }, 1.1114 + 1.1115 + /** 1.1116 + * Asynchronously gets an add-on with a particular ID in a particular 1.1117 + * install location. 1.1118 + * 1.1119 + * @param aId 1.1120 + * The ID of the add-on to retrieve 1.1121 + * @param aLocation 1.1122 + * The name of the install location 1.1123 + * @param aCallback 1.1124 + * A callback to pass the DBAddonInternal to 1.1125 + */ 1.1126 + getAddonInLocation: function XPIDB_getAddonInLocation(aId, aLocation, aCallback) { 1.1127 + this.asyncLoadDB().then( 1.1128 + addonDB => getRepositoryAddon(addonDB.get(aLocation + ":" + aId), 1.1129 + makeSafe(aCallback))); 1.1130 + }, 1.1131 + 1.1132 + /** 1.1133 + * Asynchronously gets the add-on with the specified ID that is visible. 1.1134 + * 1.1135 + * @param aId 1.1136 + * The ID of the add-on to retrieve 1.1137 + * @param aCallback 1.1138 + * A callback to pass the DBAddonInternal to 1.1139 + */ 1.1140 + getVisibleAddonForID: function XPIDB_getVisibleAddonForID(aId, aCallback) { 1.1141 + this.getAddon(aAddon => ((aAddon.id == aId) && aAddon.visible), 1.1142 + aCallback); 1.1143 + }, 1.1144 + 1.1145 + /** 1.1146 + * Asynchronously gets the visible add-ons, optionally restricting by type. 1.1147 + * 1.1148 + * @param aTypes 1.1149 + * An array of types to include or null to include all types 1.1150 + * @param aCallback 1.1151 + * A callback to pass the array of DBAddonInternals to 1.1152 + */ 1.1153 + getVisibleAddons: function XPIDB_getVisibleAddons(aTypes, aCallback) { 1.1154 + this.getAddonList(aAddon => (aAddon.visible && 1.1155 + (!aTypes || (aTypes.length == 0) || 1.1156 + (aTypes.indexOf(aAddon.type) > -1))), 1.1157 + aCallback); 1.1158 + }, 1.1159 + 1.1160 + /** 1.1161 + * Synchronously gets all add-ons of a particular type. 1.1162 + * 1.1163 + * @param aType 1.1164 + * The type of add-on to retrieve 1.1165 + * @return an array of DBAddonInternals 1.1166 + */ 1.1167 + getAddonsByType: function XPIDB_getAddonsByType(aType) { 1.1168 + if (!this.addonDB) { 1.1169 + // jank-tastic! Must synchronously load DB if the theme switches from 1.1170 + // an XPI theme to a lightweight theme before the DB has loaded, 1.1171 + // because we're called from sync XPIProvider.addonChanged 1.1172 + logger.warn("Synchronous load of XPI database due to getAddonsByType(" + aType + ")"); 1.1173 + AddonManagerPrivate.recordSimpleMeasure("XPIDB_lateOpen_byType", XPIProvider.runPhase); 1.1174 + this.syncLoadDB(true); 1.1175 + } 1.1176 + return _filterDB(this.addonDB, aAddon => (aAddon.type == aType)); 1.1177 + }, 1.1178 + 1.1179 + /** 1.1180 + * Synchronously gets an add-on with a particular internalName. 1.1181 + * 1.1182 + * @param aInternalName 1.1183 + * The internalName of the add-on to retrieve 1.1184 + * @return a DBAddonInternal 1.1185 + */ 1.1186 + getVisibleAddonForInternalName: function XPIDB_getVisibleAddonForInternalName(aInternalName) { 1.1187 + if (!this.addonDB) { 1.1188 + // This may be called when the DB hasn't otherwise been loaded 1.1189 + logger.warn("Synchronous load of XPI database due to getVisibleAddonForInternalName"); 1.1190 + AddonManagerPrivate.recordSimpleMeasure("XPIDB_lateOpen_forInternalName", 1.1191 + XPIProvider.runPhase); 1.1192 + this.syncLoadDB(true); 1.1193 + } 1.1194 + 1.1195 + return _findAddon(this.addonDB, 1.1196 + aAddon => aAddon.visible && 1.1197 + (aAddon.internalName == aInternalName)); 1.1198 + }, 1.1199 + 1.1200 + /** 1.1201 + * Asynchronously gets all add-ons with pending operations. 1.1202 + * 1.1203 + * @param aTypes 1.1204 + * The types of add-ons to retrieve or null to get all types 1.1205 + * @param aCallback 1.1206 + * A callback to pass the array of DBAddonInternal to 1.1207 + */ 1.1208 + getVisibleAddonsWithPendingOperations: 1.1209 + function XPIDB_getVisibleAddonsWithPendingOperations(aTypes, aCallback) { 1.1210 + 1.1211 + this.getAddonList( 1.1212 + aAddon => (aAddon.visible && 1.1213 + (aAddon.pendingUninstall || 1.1214 + // Logic here is tricky. If we're active but either 1.1215 + // disabled flag is set, we're pending disable; if we're not 1.1216 + // active and neither disabled flag is set, we're pending enable 1.1217 + (aAddon.active == (aAddon.userDisabled || aAddon.appDisabled))) && 1.1218 + (!aTypes || (aTypes.length == 0) || (aTypes.indexOf(aAddon.type) > -1))), 1.1219 + aCallback); 1.1220 + }, 1.1221 + 1.1222 + /** 1.1223 + * Asynchronously get an add-on by its Sync GUID. 1.1224 + * 1.1225 + * @param aGUID 1.1226 + * Sync GUID of add-on to fetch 1.1227 + * @param aCallback 1.1228 + * A callback to pass the DBAddonInternal record to. Receives null 1.1229 + * if no add-on with that GUID is found. 1.1230 + * 1.1231 + */ 1.1232 + getAddonBySyncGUID: function XPIDB_getAddonBySyncGUID(aGUID, aCallback) { 1.1233 + this.getAddon(aAddon => aAddon.syncGUID == aGUID, 1.1234 + aCallback); 1.1235 + }, 1.1236 + 1.1237 + /** 1.1238 + * Synchronously gets all add-ons in the database. 1.1239 + * This is only called from the preference observer for the default 1.1240 + * compatibility version preference, so we can return an empty list if 1.1241 + * we haven't loaded the database yet. 1.1242 + * 1.1243 + * @return an array of DBAddonInternals 1.1244 + */ 1.1245 + getAddons: function XPIDB_getAddons() { 1.1246 + if (!this.addonDB) { 1.1247 + return []; 1.1248 + } 1.1249 + return _filterDB(this.addonDB, aAddon => true); 1.1250 + }, 1.1251 + 1.1252 + /** 1.1253 + * Synchronously adds an AddonInternal's metadata to the database. 1.1254 + * 1.1255 + * @param aAddon 1.1256 + * AddonInternal to add 1.1257 + * @param aDescriptor 1.1258 + * The file descriptor of the add-on 1.1259 + * @return The DBAddonInternal that was added to the database 1.1260 + */ 1.1261 + addAddonMetadata: function XPIDB_addAddonMetadata(aAddon, aDescriptor) { 1.1262 + if (!this.addonDB) { 1.1263 + AddonManagerPrivate.recordSimpleMeasure("XPIDB_lateOpen_addMetadata", 1.1264 + XPIProvider.runPhase); 1.1265 + this.syncLoadDB(false); 1.1266 + } 1.1267 + 1.1268 + let newAddon = new DBAddonInternal(aAddon); 1.1269 + newAddon.descriptor = aDescriptor; 1.1270 + this.addonDB.set(newAddon._key, newAddon); 1.1271 + if (newAddon.visible) { 1.1272 + this.makeAddonVisible(newAddon); 1.1273 + } 1.1274 + 1.1275 + this.saveChanges(); 1.1276 + return newAddon; 1.1277 + }, 1.1278 + 1.1279 + /** 1.1280 + * Synchronously updates an add-on's metadata in the database. Currently just 1.1281 + * removes and recreates. 1.1282 + * 1.1283 + * @param aOldAddon 1.1284 + * The DBAddonInternal to be replaced 1.1285 + * @param aNewAddon 1.1286 + * The new AddonInternal to add 1.1287 + * @param aDescriptor 1.1288 + * The file descriptor of the add-on 1.1289 + * @return The DBAddonInternal that was added to the database 1.1290 + */ 1.1291 + updateAddonMetadata: function XPIDB_updateAddonMetadata(aOldAddon, aNewAddon, 1.1292 + aDescriptor) { 1.1293 + this.removeAddonMetadata(aOldAddon); 1.1294 + aNewAddon.syncGUID = aOldAddon.syncGUID; 1.1295 + aNewAddon.installDate = aOldAddon.installDate; 1.1296 + aNewAddon.applyBackgroundUpdates = aOldAddon.applyBackgroundUpdates; 1.1297 + aNewAddon.foreignInstall = aOldAddon.foreignInstall; 1.1298 + aNewAddon.active = (aNewAddon.visible && !aNewAddon.userDisabled && 1.1299 + !aNewAddon.appDisabled && !aNewAddon.pendingUninstall); 1.1300 + 1.1301 + // addAddonMetadata does a saveChanges() 1.1302 + return this.addAddonMetadata(aNewAddon, aDescriptor); 1.1303 + }, 1.1304 + 1.1305 + /** 1.1306 + * Synchronously removes an add-on from the database. 1.1307 + * 1.1308 + * @param aAddon 1.1309 + * The DBAddonInternal being removed 1.1310 + */ 1.1311 + removeAddonMetadata: function XPIDB_removeAddonMetadata(aAddon) { 1.1312 + this.addonDB.delete(aAddon._key); 1.1313 + this.saveChanges(); 1.1314 + }, 1.1315 + 1.1316 + /** 1.1317 + * Synchronously marks a DBAddonInternal as visible marking all other 1.1318 + * instances with the same ID as not visible. 1.1319 + * 1.1320 + * @param aAddon 1.1321 + * The DBAddonInternal to make visible 1.1322 + */ 1.1323 + makeAddonVisible: function XPIDB_makeAddonVisible(aAddon) { 1.1324 + logger.debug("Make addon " + aAddon._key + " visible"); 1.1325 + for (let [, otherAddon] of this.addonDB) { 1.1326 + if ((otherAddon.id == aAddon.id) && (otherAddon._key != aAddon._key)) { 1.1327 + logger.debug("Hide addon " + otherAddon._key); 1.1328 + otherAddon.visible = false; 1.1329 + } 1.1330 + } 1.1331 + aAddon.visible = true; 1.1332 + this.saveChanges(); 1.1333 + }, 1.1334 + 1.1335 + /** 1.1336 + * Synchronously sets properties for an add-on. 1.1337 + * 1.1338 + * @param aAddon 1.1339 + * The DBAddonInternal being updated 1.1340 + * @param aProperties 1.1341 + * A dictionary of properties to set 1.1342 + */ 1.1343 + setAddonProperties: function XPIDB_setAddonProperties(aAddon, aProperties) { 1.1344 + for (let key in aProperties) { 1.1345 + aAddon[key] = aProperties[key]; 1.1346 + } 1.1347 + this.saveChanges(); 1.1348 + }, 1.1349 + 1.1350 + /** 1.1351 + * Synchronously sets the Sync GUID for an add-on. 1.1352 + * Only called when the database is already loaded. 1.1353 + * 1.1354 + * @param aAddon 1.1355 + * The DBAddonInternal being updated 1.1356 + * @param aGUID 1.1357 + * GUID string to set the value to 1.1358 + * @throws if another addon already has the specified GUID 1.1359 + */ 1.1360 + setAddonSyncGUID: function XPIDB_setAddonSyncGUID(aAddon, aGUID) { 1.1361 + // Need to make sure no other addon has this GUID 1.1362 + function excludeSyncGUID(otherAddon) { 1.1363 + return (otherAddon._key != aAddon._key) && (otherAddon.syncGUID == aGUID); 1.1364 + } 1.1365 + let otherAddon = _findAddon(this.addonDB, excludeSyncGUID); 1.1366 + if (otherAddon) { 1.1367 + throw new Error("Addon sync GUID conflict for addon " + aAddon._key + 1.1368 + ": " + otherAddon._key + " already has GUID " + aGUID); 1.1369 + } 1.1370 + aAddon.syncGUID = aGUID; 1.1371 + this.saveChanges(); 1.1372 + }, 1.1373 + 1.1374 + /** 1.1375 + * Synchronously updates an add-on's active flag in the database. 1.1376 + * 1.1377 + * @param aAddon 1.1378 + * The DBAddonInternal to update 1.1379 + */ 1.1380 + updateAddonActive: function XPIDB_updateAddonActive(aAddon, aActive) { 1.1381 + logger.debug("Updating active state for add-on " + aAddon.id + " to " + aActive); 1.1382 + 1.1383 + aAddon.active = aActive; 1.1384 + this.saveChanges(); 1.1385 + }, 1.1386 + 1.1387 + /** 1.1388 + * Synchronously calculates and updates all the active flags in the database. 1.1389 + */ 1.1390 + updateActiveAddons: function XPIDB_updateActiveAddons() { 1.1391 + if (!this.addonDB) { 1.1392 + logger.warn("updateActiveAddons called when DB isn't loaded"); 1.1393 + // force the DB to load 1.1394 + AddonManagerPrivate.recordSimpleMeasure("XPIDB_lateOpen_updateActive", 1.1395 + XPIProvider.runPhase); 1.1396 + this.syncLoadDB(true); 1.1397 + } 1.1398 + logger.debug("Updating add-on states"); 1.1399 + for (let [, addon] of this.addonDB) { 1.1400 + let newActive = (addon.visible && !addon.userDisabled && 1.1401 + !addon.softDisabled && !addon.appDisabled && 1.1402 + !addon.pendingUninstall); 1.1403 + if (newActive != addon.active) { 1.1404 + addon.active = newActive; 1.1405 + this.saveChanges(); 1.1406 + } 1.1407 + } 1.1408 + }, 1.1409 + 1.1410 + /** 1.1411 + * Writes out the XPI add-ons list for the platform to read. 1.1412 + * @return true if the file was successfully updated, false otherwise 1.1413 + */ 1.1414 + writeAddonsList: function XPIDB_writeAddonsList() { 1.1415 + if (!this.addonDB) { 1.1416 + // force the DB to load 1.1417 + AddonManagerPrivate.recordSimpleMeasure("XPIDB_lateOpen_writeList", 1.1418 + XPIProvider.runPhase); 1.1419 + this.syncLoadDB(true); 1.1420 + } 1.1421 + Services.appinfo.invalidateCachesOnRestart(); 1.1422 + 1.1423 + let addonsList = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST], 1.1424 + true); 1.1425 + let enabledAddons = []; 1.1426 + let text = "[ExtensionDirs]\r\n"; 1.1427 + let count = 0; 1.1428 + let fullCount = 0; 1.1429 + 1.1430 + let activeAddons = _filterDB( 1.1431 + this.addonDB, 1.1432 + aAddon => aAddon.active && !aAddon.bootstrap && (aAddon.type != "theme")); 1.1433 + 1.1434 + for (let row of activeAddons) { 1.1435 + text += "Extension" + (count++) + "=" + row.descriptor + "\r\n"; 1.1436 + enabledAddons.push(encodeURIComponent(row.id) + ":" + 1.1437 + encodeURIComponent(row.version)); 1.1438 + } 1.1439 + fullCount += count; 1.1440 + 1.1441 + // The selected skin may come from an inactive theme (the default theme 1.1442 + // when a lightweight theme is applied for example) 1.1443 + text += "\r\n[ThemeDirs]\r\n"; 1.1444 + 1.1445 + let dssEnabled = false; 1.1446 + try { 1.1447 + dssEnabled = Services.prefs.getBoolPref(PREF_EM_DSS_ENABLED); 1.1448 + } catch (e) {} 1.1449 + 1.1450 + let themes = []; 1.1451 + if (dssEnabled) { 1.1452 + themes = _filterDB(this.addonDB, aAddon => aAddon.type == "theme"); 1.1453 + } 1.1454 + else { 1.1455 + let activeTheme = _findAddon( 1.1456 + this.addonDB, 1.1457 + aAddon => (aAddon.type == "theme") && 1.1458 + (aAddon.internalName == XPIProvider.selectedSkin)); 1.1459 + if (activeTheme) { 1.1460 + themes.push(activeTheme); 1.1461 + } 1.1462 + } 1.1463 + 1.1464 + if (themes.length > 0) { 1.1465 + count = 0; 1.1466 + for (let row of themes) { 1.1467 + text += "Extension" + (count++) + "=" + row.descriptor + "\r\n"; 1.1468 + enabledAddons.push(encodeURIComponent(row.id) + ":" + 1.1469 + encodeURIComponent(row.version)); 1.1470 + } 1.1471 + fullCount += count; 1.1472 + } 1.1473 + 1.1474 + if (fullCount > 0) { 1.1475 + logger.debug("Writing add-ons list"); 1.1476 + 1.1477 + try { 1.1478 + let addonsListTmp = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST + ".tmp"], 1.1479 + true); 1.1480 + var fos = FileUtils.openFileOutputStream(addonsListTmp); 1.1481 + fos.write(text, text.length); 1.1482 + fos.close(); 1.1483 + addonsListTmp.moveTo(addonsListTmp.parent, FILE_XPI_ADDONS_LIST); 1.1484 + 1.1485 + Services.prefs.setCharPref(PREF_EM_ENABLED_ADDONS, enabledAddons.join(",")); 1.1486 + } 1.1487 + catch (e) { 1.1488 + logger.error("Failed to write add-ons list to profile directory", e); 1.1489 + return false; 1.1490 + } 1.1491 + } 1.1492 + else { 1.1493 + if (addonsList.exists()) { 1.1494 + logger.debug("Deleting add-ons list"); 1.1495 + try { 1.1496 + addonsList.remove(false); 1.1497 + } 1.1498 + catch (e) { 1.1499 + logger.error("Failed to remove " + addonsList.path, e); 1.1500 + return false; 1.1501 + } 1.1502 + } 1.1503 + 1.1504 + Services.prefs.clearUserPref(PREF_EM_ENABLED_ADDONS); 1.1505 + } 1.1506 + return true; 1.1507 + } 1.1508 +};