michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cr = Components.results; michael@0: const Cu = Components.utils; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/AddonManager.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository", michael@0: "resource://gre/modules/addons/AddonRepository.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", michael@0: "resource://gre/modules/FileUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "DeferredSave", michael@0: "resource://gre/modules/DeferredSave.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Promise", michael@0: "resource://gre/modules/Promise.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "OS", michael@0: "resource://gre/modules/osfile.jsm"); michael@0: michael@0: Cu.import("resource://gre/modules/Log.jsm"); michael@0: const LOGGER_ID = "addons.xpi-utils"; michael@0: michael@0: // Create a new logger for use by the Addons XPI Provider Utils michael@0: // (Requires AddonManager.jsm) michael@0: let logger = Log.repository.getLogger(LOGGER_ID); michael@0: michael@0: const KEY_PROFILEDIR = "ProfD"; michael@0: const FILE_DATABASE = "extensions.sqlite"; michael@0: const FILE_JSON_DB = "extensions.json"; michael@0: const FILE_OLD_DATABASE = "extensions.rdf"; michael@0: const FILE_XPI_ADDONS_LIST = "extensions.ini"; michael@0: michael@0: // The value for this is in Makefile.in michael@0: #expand const DB_SCHEMA = __MOZ_EXTENSIONS_DB_SCHEMA__; michael@0: michael@0: // The last version of DB_SCHEMA implemented in SQLITE michael@0: const LAST_SQLITE_DB_SCHEMA = 14; michael@0: const PREF_DB_SCHEMA = "extensions.databaseSchema"; michael@0: const PREF_PENDING_OPERATIONS = "extensions.pendingOperations"; michael@0: const PREF_EM_ENABLED_ADDONS = "extensions.enabledAddons"; michael@0: const PREF_EM_DSS_ENABLED = "extensions.dss.enabled"; michael@0: michael@0: michael@0: // Properties that only exist in the database michael@0: const DB_METADATA = ["syncGUID", michael@0: "installDate", michael@0: "updateDate", michael@0: "size", michael@0: "sourceURI", michael@0: "releaseNotesURI", michael@0: "applyBackgroundUpdates"]; michael@0: const DB_BOOL_METADATA = ["visible", "active", "userDisabled", "appDisabled", michael@0: "pendingUninstall", "bootstrap", "skinnable", michael@0: "softDisabled", "isForeignInstall", michael@0: "hasBinaryComponents", "strictCompatibility"]; michael@0: michael@0: // Properties to save in JSON file michael@0: const PROP_JSON_FIELDS = ["id", "syncGUID", "location", "version", "type", michael@0: "internalName", "updateURL", "updateKey", "optionsURL", michael@0: "optionsType", "aboutURL", "iconURL", "icon64URL", michael@0: "defaultLocale", "visible", "active", "userDisabled", michael@0: "appDisabled", "pendingUninstall", "descriptor", "installDate", michael@0: "updateDate", "applyBackgroundUpdates", "bootstrap", michael@0: "skinnable", "size", "sourceURI", "releaseNotesURI", michael@0: "softDisabled", "foreignInstall", "hasBinaryComponents", michael@0: "strictCompatibility", "locales", "targetApplications", michael@0: "targetPlatforms"]; michael@0: michael@0: // Time to wait before async save of XPI JSON database, in milliseconds michael@0: const ASYNC_SAVE_DELAY_MS = 20; michael@0: michael@0: const PREFIX_ITEM_URI = "urn:mozilla:item:"; michael@0: const RDFURI_ITEM_ROOT = "urn:mozilla:item:root" michael@0: const PREFIX_NS_EM = "http://www.mozilla.org/2004/em-rdf#"; michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "gRDF", "@mozilla.org/rdf/rdf-service;1", michael@0: Ci.nsIRDFService); michael@0: michael@0: function EM_R(aProperty) { michael@0: return gRDF.GetResource(PREFIX_NS_EM + aProperty); michael@0: } michael@0: michael@0: /** michael@0: * Converts an RDF literal, resource or integer into a string. michael@0: * michael@0: * @param aLiteral michael@0: * The RDF object to convert michael@0: * @return a string if the object could be converted or null michael@0: */ michael@0: function getRDFValue(aLiteral) { michael@0: if (aLiteral instanceof Ci.nsIRDFLiteral) michael@0: return aLiteral.Value; michael@0: if (aLiteral instanceof Ci.nsIRDFResource) michael@0: return aLiteral.Value; michael@0: if (aLiteral instanceof Ci.nsIRDFInt) michael@0: return aLiteral.Value; michael@0: return null; michael@0: } michael@0: michael@0: /** michael@0: * Gets an RDF property as a string michael@0: * michael@0: * @param aDs michael@0: * The RDF datasource to read the property from michael@0: * @param aResource michael@0: * The RDF resource to read the property from michael@0: * @param aProperty michael@0: * The property to read michael@0: * @return a string if the property existed or null michael@0: */ michael@0: function getRDFProperty(aDs, aResource, aProperty) { michael@0: return getRDFValue(aDs.GetTarget(aResource, EM_R(aProperty), true)); michael@0: } michael@0: michael@0: /** michael@0: * Asynchronously fill in the _repositoryAddon field for one addon michael@0: */ michael@0: function getRepositoryAddon(aAddon, aCallback) { michael@0: if (!aAddon) { michael@0: aCallback(aAddon); michael@0: return; michael@0: } michael@0: function completeAddon(aRepositoryAddon) { michael@0: aAddon._repositoryAddon = aRepositoryAddon; michael@0: aAddon.compatibilityOverrides = aRepositoryAddon ? michael@0: aRepositoryAddon.compatibilityOverrides : michael@0: null; michael@0: aCallback(aAddon); michael@0: } michael@0: AddonRepository.getCachedAddonByID(aAddon.id, completeAddon); michael@0: } michael@0: michael@0: /** michael@0: * Wrap an API-supplied function in an exception handler to make it safe to call michael@0: */ michael@0: function makeSafe(aCallback) { michael@0: return function(...aArgs) { michael@0: try { michael@0: aCallback(...aArgs); michael@0: } michael@0: catch(ex) { michael@0: logger.warn("XPI Database callback failed", ex); michael@0: } michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * A helper method to asynchronously call a function on an array michael@0: * of objects, calling a callback when function(x) has been gathered michael@0: * for every element of the array. michael@0: * WARNING: not currently error-safe; if the async function does not call michael@0: * our internal callback for any of the array elements, asyncMap will not michael@0: * call the callback parameter. michael@0: * michael@0: * @param aObjects michael@0: * The array of objects to process asynchronously michael@0: * @param aMethod michael@0: * Function with signature function(object, function aCallback(f_of_object)) michael@0: * @param aCallback michael@0: * Function with signature f([aMethod(object)]), called when all values michael@0: * are available michael@0: */ michael@0: function asyncMap(aObjects, aMethod, aCallback) { michael@0: var resultsPending = aObjects.length; michael@0: var results = [] michael@0: if (resultsPending == 0) { michael@0: aCallback(results); michael@0: return; michael@0: } michael@0: michael@0: function asyncMap_gotValue(aIndex, aValue) { michael@0: results[aIndex] = aValue; michael@0: if (--resultsPending == 0) { michael@0: aCallback(results); michael@0: } michael@0: } michael@0: michael@0: aObjects.map(function asyncMap_each(aObject, aIndex, aArray) { michael@0: try { michael@0: aMethod(aObject, function asyncMap_callback(aResult) { michael@0: asyncMap_gotValue(aIndex, aResult); michael@0: }); michael@0: } michael@0: catch (e) { michael@0: logger.warn("Async map function failed", e); michael@0: asyncMap_gotValue(aIndex, undefined); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * A generator to synchronously return result rows from an mozIStorageStatement. michael@0: * michael@0: * @param aStatement michael@0: * The statement to execute michael@0: */ michael@0: function resultRows(aStatement) { michael@0: try { michael@0: while (stepStatement(aStatement)) michael@0: yield aStatement.row; michael@0: } michael@0: finally { michael@0: aStatement.reset(); michael@0: } michael@0: } michael@0: michael@0: michael@0: /** michael@0: * A helper function to log an SQL error. michael@0: * michael@0: * @param aError michael@0: * The storage error code associated with the error michael@0: * @param aErrorString michael@0: * An error message michael@0: */ michael@0: function logSQLError(aError, aErrorString) { michael@0: logger.error("SQL error " + aError + ": " + aErrorString); michael@0: } michael@0: michael@0: /** michael@0: * A helper function to log any errors that occur during async statements. michael@0: * michael@0: * @param aError michael@0: * A mozIStorageError to log michael@0: */ michael@0: function asyncErrorLogger(aError) { michael@0: logSQLError(aError.result, aError.message); michael@0: } michael@0: michael@0: /** michael@0: * A helper function to step a statement synchronously and log any error that michael@0: * occurs. michael@0: * michael@0: * @param aStatement michael@0: * A mozIStorageStatement to execute michael@0: */ michael@0: function stepStatement(aStatement) { michael@0: try { michael@0: return aStatement.executeStep(); michael@0: } michael@0: catch (e) { michael@0: logSQLError(XPIDatabase.connection.lastError, michael@0: XPIDatabase.connection.lastErrorString); michael@0: throw e; michael@0: } michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Copies properties from one object to another. If no target object is passed michael@0: * a new object will be created and returned. michael@0: * michael@0: * @param aObject michael@0: * An object to copy from michael@0: * @param aProperties michael@0: * An array of properties to be copied michael@0: * @param aTarget michael@0: * An optional target object to copy the properties to michael@0: * @return the object that the properties were copied onto michael@0: */ michael@0: function copyProperties(aObject, aProperties, aTarget) { michael@0: if (!aTarget) michael@0: aTarget = {}; michael@0: aProperties.forEach(function(aProp) { michael@0: aTarget[aProp] = aObject[aProp]; michael@0: }); michael@0: return aTarget; michael@0: } michael@0: michael@0: /** michael@0: * Copies properties from a mozIStorageRow to an object. If no target object is michael@0: * passed a new object will be created and returned. michael@0: * michael@0: * @param aRow michael@0: * A mozIStorageRow to copy from michael@0: * @param aProperties michael@0: * An array of properties to be copied michael@0: * @param aTarget michael@0: * An optional target object to copy the properties to michael@0: * @return the object that the properties were copied onto michael@0: */ michael@0: function copyRowProperties(aRow, aProperties, aTarget) { michael@0: if (!aTarget) michael@0: aTarget = {}; michael@0: aProperties.forEach(function(aProp) { michael@0: aTarget[aProp] = aRow.getResultByName(aProp); michael@0: }); michael@0: return aTarget; michael@0: } michael@0: michael@0: /** michael@0: * The DBAddonInternal is a special AddonInternal that has been retrieved from michael@0: * the database. The constructor will initialize the DBAddonInternal with a set michael@0: * of fields, which could come from either the JSON store or as an michael@0: * XPIProvider.AddonInternal created from an addon's manifest michael@0: * @constructor michael@0: * @param aLoaded michael@0: * Addon data fields loaded from JSON or the addon manifest. michael@0: */ michael@0: function DBAddonInternal(aLoaded) { michael@0: copyProperties(aLoaded, PROP_JSON_FIELDS, this); michael@0: michael@0: if (aLoaded._installLocation) { michael@0: this._installLocation = aLoaded._installLocation; michael@0: this.location = aLoaded._installLocation._name; michael@0: } michael@0: else if (aLoaded.location) { michael@0: this._installLocation = XPIProvider.installLocationsByName[this.location]; michael@0: } michael@0: michael@0: this._key = this.location + ":" + this.id; michael@0: michael@0: try { michael@0: this._sourceBundle = this._installLocation.getLocationForID(this.id); michael@0: } michael@0: catch (e) { michael@0: // An exception will be thrown if the add-on appears in the database but michael@0: // not on disk. In general this should only happen during startup as michael@0: // this change is being detected. michael@0: } michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "pendingUpgrade", michael@0: function DBA_pendingUpgradeGetter() { michael@0: for (let install of XPIProvider.installs) { michael@0: if (install.state == AddonManager.STATE_INSTALLED && michael@0: !(install.addon.inDatabase) && michael@0: install.addon.id == this.id && michael@0: install.installLocation == this._installLocation) { michael@0: delete this.pendingUpgrade; michael@0: return this.pendingUpgrade = install.addon; michael@0: } michael@0: }; michael@0: return null; michael@0: }); michael@0: } michael@0: michael@0: function DBAddonInternalPrototype() michael@0: { michael@0: this.applyCompatibilityUpdate = michael@0: function(aUpdate, aSyncCompatibility) { michael@0: this.targetApplications.forEach(function(aTargetApp) { michael@0: aUpdate.targetApplications.forEach(function(aUpdateTarget) { michael@0: if (aTargetApp.id == aUpdateTarget.id && (aSyncCompatibility || michael@0: Services.vc.compare(aTargetApp.maxVersion, aUpdateTarget.maxVersion) < 0)) { michael@0: aTargetApp.minVersion = aUpdateTarget.minVersion; michael@0: aTargetApp.maxVersion = aUpdateTarget.maxVersion; michael@0: XPIDatabase.saveChanges(); michael@0: } michael@0: }); michael@0: }); michael@0: XPIProvider.updateAddonDisabledState(this); michael@0: }; michael@0: michael@0: this.toJSON = michael@0: function() { michael@0: return copyProperties(this, PROP_JSON_FIELDS); michael@0: }; michael@0: michael@0: Object.defineProperty(this, "inDatabase", michael@0: { get: function() { return true; }, michael@0: enumerable: true, michael@0: configurable: true }); michael@0: } michael@0: DBAddonInternalPrototype.prototype = AddonInternal.prototype; michael@0: michael@0: DBAddonInternal.prototype = new DBAddonInternalPrototype(); michael@0: michael@0: /** michael@0: * Internal interface: find an addon from an already loaded addonDB michael@0: */ michael@0: function _findAddon(addonDB, aFilter) { michael@0: for (let [, addon] of addonDB) { michael@0: if (aFilter(addon)) { michael@0: return addon; michael@0: } michael@0: } michael@0: return null; michael@0: } michael@0: michael@0: /** michael@0: * Internal interface to get a filtered list of addons from a loaded addonDB michael@0: */ michael@0: function _filterDB(addonDB, aFilter) { michael@0: let addonList = []; michael@0: for (let [, addon] of addonDB) { michael@0: if (aFilter(addon)) { michael@0: addonList.push(addon); michael@0: } michael@0: } michael@0: michael@0: return addonList; michael@0: } michael@0: michael@0: this.XPIDatabase = { michael@0: // true if the database connection has been opened michael@0: initialized: false, michael@0: // The database file michael@0: jsonFile: FileUtils.getFile(KEY_PROFILEDIR, [FILE_JSON_DB], true), michael@0: // Migration data loaded from an old version of the database. michael@0: migrateData: null, michael@0: // Active add-on directories loaded from extensions.ini and prefs at startup. michael@0: activeBundles: null, michael@0: michael@0: // Saved error object if we fail to read an existing database michael@0: _loadError: null, michael@0: michael@0: // Error reported by our most recent attempt to read or write the database, if any michael@0: get lastError() { michael@0: if (this._loadError) michael@0: return this._loadError; michael@0: if (this._deferredSave) michael@0: return this._deferredSave.lastError; michael@0: return null; michael@0: }, michael@0: michael@0: /** michael@0: * Mark the current stored data dirty, and schedule a flush to disk michael@0: */ michael@0: saveChanges: function() { michael@0: if (!this.initialized) { michael@0: throw new Error("Attempt to use XPI database when it is not initialized"); michael@0: } michael@0: michael@0: if (!this._deferredSave) { michael@0: this._deferredSave = new DeferredSave(this.jsonFile.path, michael@0: () => JSON.stringify(this), michael@0: ASYNC_SAVE_DELAY_MS); michael@0: } michael@0: michael@0: let promise = this._deferredSave.saveChanges(); michael@0: if (!this._schemaVersionSet) { michael@0: this._schemaVersionSet = true; michael@0: promise.then( michael@0: count => { michael@0: // Update the XPIDB schema version preference the first time we successfully michael@0: // save the database. michael@0: logger.debug("XPI Database saved, setting schema version preference to " + DB_SCHEMA); michael@0: Services.prefs.setIntPref(PREF_DB_SCHEMA, DB_SCHEMA); michael@0: // Reading the DB worked once, so we don't need the load error michael@0: this._loadError = null; michael@0: }, michael@0: error => { michael@0: // Need to try setting the schema version again later michael@0: this._schemaVersionSet = false; michael@0: logger.warn("Failed to save XPI database", error); michael@0: // this._deferredSave.lastError has the most recent error so we don't michael@0: // need this any more michael@0: this._loadError = null; michael@0: }); michael@0: } michael@0: }, michael@0: michael@0: flush: function() { michael@0: // handle the "in memory only" and "saveChanges never called" cases michael@0: if (!this._deferredSave) { michael@0: return Promise.resolve(0); michael@0: } michael@0: michael@0: return this._deferredSave.flush(); michael@0: }, michael@0: michael@0: /** michael@0: * Converts the current internal state of the XPI addon database to michael@0: * a JSON.stringify()-ready structure michael@0: */ michael@0: toJSON: function() { michael@0: if (!this.addonDB) { michael@0: // We never loaded the database? michael@0: throw new Error("Attempt to save database without loading it first"); michael@0: } michael@0: michael@0: let toSave = { michael@0: schemaVersion: DB_SCHEMA, michael@0: addons: [...this.addonDB.values()] michael@0: }; michael@0: return toSave; michael@0: }, michael@0: michael@0: /** michael@0: * Pull upgrade information from an existing SQLITE database michael@0: * michael@0: * @return false if there is no SQLITE database michael@0: * true and sets this.migrateData to null if the SQLITE DB exists michael@0: * but does not contain useful information michael@0: * true and sets this.migrateData to michael@0: * {location: {id1:{addon1}, id2:{addon2}}, location2:{...}, ...} michael@0: * if there is useful information michael@0: */ michael@0: getMigrateDataFromSQLITE: function XPIDB_getMigrateDataFromSQLITE() { michael@0: let connection = null; michael@0: let dbfile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_DATABASE], true); michael@0: // Attempt to open the database michael@0: try { michael@0: connection = Services.storage.openUnsharedDatabase(dbfile); michael@0: } michael@0: catch (e) { michael@0: logger.warn("Failed to open sqlite database " + dbfile.path + " for upgrade", e); michael@0: return null; michael@0: } michael@0: logger.debug("Migrating data from sqlite"); michael@0: let migrateData = this.getMigrateDataFromDatabase(connection); michael@0: connection.close(); michael@0: return migrateData; michael@0: }, michael@0: michael@0: /** michael@0: * Synchronously opens and reads the database file, upgrading from old michael@0: * databases or making a new DB if needed. michael@0: * michael@0: * The possibilities, in order of priority, are: michael@0: * 1) Perfectly good, up to date database michael@0: * 2) Out of date JSON database needs to be upgraded => upgrade michael@0: * 3) JSON database exists but is mangled somehow => build new JSON michael@0: * 4) no JSON DB, but a useable SQLITE db we can upgrade from => upgrade michael@0: * 5) useless SQLITE DB => build new JSON michael@0: * 6) useable RDF DB => upgrade michael@0: * 7) useless RDF DB => build new JSON michael@0: * 8) Nothing at all => build new JSON michael@0: * @param aRebuildOnError michael@0: * A boolean indicating whether add-on information should be loaded michael@0: * from the install locations if the database needs to be rebuilt. michael@0: * (if false, caller is XPIProvider.checkForChanges() which will rebuild) michael@0: */ michael@0: syncLoadDB: function XPIDB_syncLoadDB(aRebuildOnError) { michael@0: this.migrateData = null; michael@0: let fstream = null; michael@0: let data = ""; michael@0: try { michael@0: let readTimer = AddonManagerPrivate.simpleTimer("XPIDB_syncRead_MS"); michael@0: logger.debug("Opening XPI database " + this.jsonFile.path); michael@0: fstream = Components.classes["@mozilla.org/network/file-input-stream;1"]. michael@0: createInstance(Components.interfaces.nsIFileInputStream); michael@0: fstream.init(this.jsonFile, -1, 0, 0); michael@0: let cstream = null; michael@0: try { michael@0: cstream = Components.classes["@mozilla.org/intl/converter-input-stream;1"]. michael@0: createInstance(Components.interfaces.nsIConverterInputStream); michael@0: cstream.init(fstream, "UTF-8", 0, 0); michael@0: let (str = {}) { michael@0: let read = 0; michael@0: do { michael@0: read = cstream.readString(0xffffffff, str); // read as much as we can and put it in str.value michael@0: data += str.value; michael@0: } while (read != 0); michael@0: } michael@0: readTimer.done(); michael@0: this.parseDB(data, aRebuildOnError); michael@0: } michael@0: catch(e) { michael@0: logger.error("Failed to load XPI JSON data from profile", e); michael@0: let rebuildTimer = AddonManagerPrivate.simpleTimer("XPIDB_rebuildReadFailed_MS"); michael@0: this.rebuildDatabase(aRebuildOnError); michael@0: rebuildTimer.done(); michael@0: } michael@0: finally { michael@0: if (cstream) michael@0: cstream.close(); michael@0: } michael@0: } michael@0: catch (e) { michael@0: if (e.result === Cr.NS_ERROR_FILE_NOT_FOUND) { michael@0: this.upgradeDB(aRebuildOnError); michael@0: } michael@0: else { michael@0: this.rebuildUnreadableDB(e, aRebuildOnError); michael@0: } michael@0: } michael@0: finally { michael@0: if (fstream) michael@0: fstream.close(); michael@0: } michael@0: // If an async load was also in progress, resolve that promise with our DB; michael@0: // otherwise create a resolved promise michael@0: if (this._dbPromise) { michael@0: AddonManagerPrivate.recordSimpleMeasure("XPIDB_overlapped_load", 1); michael@0: this._dbPromise.resolve(this.addonDB); michael@0: } michael@0: else michael@0: this._dbPromise = Promise.resolve(this.addonDB); michael@0: }, michael@0: michael@0: /** michael@0: * Parse loaded data, reconstructing the database if the loaded data is not valid michael@0: * @param aRebuildOnError michael@0: * If true, synchronously reconstruct the database from installed add-ons michael@0: */ michael@0: parseDB: function(aData, aRebuildOnError) { michael@0: let parseTimer = AddonManagerPrivate.simpleTimer("XPIDB_parseDB_MS"); michael@0: try { michael@0: // dump("Loaded JSON:\n" + aData + "\n"); michael@0: let inputAddons = JSON.parse(aData); michael@0: // Now do some sanity checks on our JSON db michael@0: if (!("schemaVersion" in inputAddons) || !("addons" in inputAddons)) { michael@0: parseTimer.done(); michael@0: // Content of JSON file is bad, need to rebuild from scratch michael@0: logger.error("bad JSON file contents"); michael@0: AddonManagerPrivate.recordSimpleMeasure("XPIDB_startupError", "badJSON"); michael@0: let rebuildTimer = AddonManagerPrivate.simpleTimer("XPIDB_rebuildBadJSON_MS"); michael@0: this.rebuildDatabase(aRebuildOnError); michael@0: rebuildTimer.done(); michael@0: return; michael@0: } michael@0: if (inputAddons.schemaVersion != DB_SCHEMA) { michael@0: // Handle mismatched JSON schema version. For now, we assume michael@0: // compatibility for JSON data, though we throw away any fields we michael@0: // don't know about (bug 902956) michael@0: AddonManagerPrivate.recordSimpleMeasure("XPIDB_startupError", michael@0: "schemaMismatch-" + inputAddons.schemaVersion); michael@0: logger.debug("JSON schema mismatch: expected " + DB_SCHEMA + michael@0: ", actual " + inputAddons.schemaVersion); michael@0: // When we rev the schema of the JSON database, we need to make sure we michael@0: // force the DB to save so that the DB_SCHEMA value in the JSON file and michael@0: // the preference are updated. michael@0: } michael@0: // If we got here, we probably have good data michael@0: // Make AddonInternal instances from the loaded data and save them michael@0: let addonDB = new Map(); michael@0: for (let loadedAddon of inputAddons.addons) { michael@0: let newAddon = new DBAddonInternal(loadedAddon); michael@0: addonDB.set(newAddon._key, newAddon); michael@0: }; michael@0: parseTimer.done(); michael@0: this.addonDB = addonDB; michael@0: logger.debug("Successfully read XPI database"); michael@0: this.initialized = true; michael@0: } michael@0: catch(e) { michael@0: // If we catch and log a SyntaxError from the JSON michael@0: // parser, the xpcshell test harness fails the test for us: bug 870828 michael@0: parseTimer.done(); michael@0: if (e.name == "SyntaxError") { michael@0: logger.error("Syntax error parsing saved XPI JSON data"); michael@0: AddonManagerPrivate.recordSimpleMeasure("XPIDB_startupError", "syntax"); michael@0: } michael@0: else { michael@0: logger.error("Failed to load XPI JSON data from profile", e); michael@0: AddonManagerPrivate.recordSimpleMeasure("XPIDB_startupError", "other"); michael@0: } michael@0: let rebuildTimer = AddonManagerPrivate.simpleTimer("XPIDB_rebuildReadFailed_MS"); michael@0: this.rebuildDatabase(aRebuildOnError); michael@0: rebuildTimer.done(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Upgrade database from earlier (sqlite or RDF) version if available michael@0: */ michael@0: upgradeDB: function(aRebuildOnError) { michael@0: let upgradeTimer = AddonManagerPrivate.simpleTimer("XPIDB_upgradeDB_MS"); michael@0: try { michael@0: let schemaVersion = Services.prefs.getIntPref(PREF_DB_SCHEMA); michael@0: if (schemaVersion <= LAST_SQLITE_DB_SCHEMA) { michael@0: // we should have an older SQLITE database michael@0: logger.debug("Attempting to upgrade from SQLITE database"); michael@0: this.migrateData = this.getMigrateDataFromSQLITE(); michael@0: } michael@0: else { michael@0: // we've upgraded before but the JSON file is gone, fall through michael@0: // and rebuild from scratch michael@0: AddonManagerPrivate.recordSimpleMeasure("XPIDB_startupError", "dbMissing"); michael@0: } michael@0: } michael@0: catch(e) { michael@0: // No schema version pref means either a really old upgrade (RDF) or michael@0: // a new profile michael@0: this.migrateData = this.getMigrateDataFromRDF(); michael@0: } michael@0: michael@0: this.rebuildDatabase(aRebuildOnError); michael@0: upgradeTimer.done(); michael@0: }, michael@0: michael@0: /** michael@0: * Reconstruct when the DB file exists but is unreadable michael@0: * (for example because read permission is denied) michael@0: */ michael@0: rebuildUnreadableDB: function(aError, aRebuildOnError) { michael@0: let rebuildTimer = AddonManagerPrivate.simpleTimer("XPIDB_rebuildUnreadableDB_MS"); michael@0: logger.warn("Extensions database " + this.jsonFile.path + michael@0: " exists but is not readable; rebuilding", aError); michael@0: // Remember the error message until we try and write at least once, so michael@0: // we know at shutdown time that there was a problem michael@0: this._loadError = aError; michael@0: AddonManagerPrivate.recordSimpleMeasure("XPIDB_startupError", "unreadable"); michael@0: this.rebuildDatabase(aRebuildOnError); michael@0: rebuildTimer.done(); michael@0: }, michael@0: michael@0: /** michael@0: * Open and read the XPI database asynchronously, upgrading if michael@0: * necessary. If any DB load operation fails, we need to michael@0: * synchronously rebuild the DB from the installed extensions. michael@0: * michael@0: * @return Promise resolves to the Map of loaded JSON data stored michael@0: * in this.addonDB; never rejects. michael@0: */ michael@0: asyncLoadDB: function XPIDB_asyncLoadDB() { michael@0: // Already started (and possibly finished) loading michael@0: if (this._dbPromise) { michael@0: return this._dbPromise; michael@0: } michael@0: michael@0: logger.debug("Starting async load of XPI database " + this.jsonFile.path); michael@0: AddonManagerPrivate.recordSimpleMeasure("XPIDB_async_load", XPIProvider.runPhase); michael@0: let readOptions = { michael@0: outExecutionDuration: 0 michael@0: }; michael@0: return this._dbPromise = OS.File.read(this.jsonFile.path, null, readOptions).then( michael@0: byteArray => { michael@0: logger.debug("Async JSON file read took " + readOptions.outExecutionDuration + " MS"); michael@0: AddonManagerPrivate.recordSimpleMeasure("XPIDB_asyncRead_MS", michael@0: readOptions.outExecutionDuration); michael@0: if (this._addonDB) { michael@0: logger.debug("Synchronous load completed while waiting for async load"); michael@0: return this.addonDB; michael@0: } michael@0: logger.debug("Finished async read of XPI database, parsing..."); michael@0: let decodeTimer = AddonManagerPrivate.simpleTimer("XPIDB_decode_MS"); michael@0: let decoder = new TextDecoder(); michael@0: let data = decoder.decode(byteArray); michael@0: decodeTimer.done(); michael@0: this.parseDB(data, true); michael@0: return this.addonDB; michael@0: }) michael@0: .then(null, michael@0: error => { michael@0: if (this._addonDB) { michael@0: logger.debug("Synchronous load completed while waiting for async load"); michael@0: return this.addonDB; michael@0: } michael@0: if (error.becauseNoSuchFile) { michael@0: this.upgradeDB(true); michael@0: } michael@0: else { michael@0: // it's there but unreadable michael@0: this.rebuildUnreadableDB(error, true); michael@0: } michael@0: return this.addonDB; michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Rebuild the database from addon install directories. If this.migrateData michael@0: * is available, uses migrated information for settings on the addons found michael@0: * during rebuild michael@0: * @param aRebuildOnError michael@0: * A boolean indicating whether add-on information should be loaded michael@0: * from the install locations if the database needs to be rebuilt. michael@0: * (if false, caller is XPIProvider.checkForChanges() which will rebuild) michael@0: */ michael@0: rebuildDatabase: function XIPDB_rebuildDatabase(aRebuildOnError) { michael@0: this.addonDB = new Map(); michael@0: this.initialized = true; michael@0: michael@0: if (XPIProvider.installStates && XPIProvider.installStates.length == 0) { michael@0: // No extensions installed, so we're done michael@0: logger.debug("Rebuilding XPI database with no extensions"); michael@0: return; michael@0: } michael@0: michael@0: // If there is no migration data then load the list of add-on directories michael@0: // that were active during the last run michael@0: if (!this.migrateData) michael@0: this.activeBundles = this.getActiveBundles(); michael@0: michael@0: if (aRebuildOnError) { michael@0: logger.warn("Rebuilding add-ons database from installed extensions."); michael@0: try { michael@0: XPIProvider.processFileChanges(XPIProvider.installStates, {}, false); michael@0: } michael@0: catch (e) { michael@0: logger.error("Failed to rebuild XPI database from installed extensions", e); michael@0: } michael@0: // Make sure to update the active add-ons and add-ons list on shutdown michael@0: Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Gets the list of file descriptors of active extension directories or XPI michael@0: * files from the add-ons list. This must be loaded from disk since the michael@0: * directory service gives no easy way to get both directly. This list doesn't michael@0: * include themes as preferences already say which theme is currently active michael@0: * michael@0: * @return an array of persistent descriptors for the directories michael@0: */ michael@0: getActiveBundles: function XPIDB_getActiveBundles() { michael@0: let bundles = []; michael@0: michael@0: // non-bootstrapped extensions michael@0: let addonsList = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST], michael@0: true); michael@0: michael@0: if (!addonsList.exists()) michael@0: // XXX Irving believes this is broken in the case where there is no michael@0: // extensions.ini but there are bootstrap extensions (e.g. Android) michael@0: return null; michael@0: michael@0: try { michael@0: let iniFactory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"] michael@0: .getService(Ci.nsIINIParserFactory); michael@0: let parser = iniFactory.createINIParser(addonsList); michael@0: let keys = parser.getKeys("ExtensionDirs"); michael@0: michael@0: while (keys.hasMore()) michael@0: bundles.push(parser.getString("ExtensionDirs", keys.getNext())); michael@0: } michael@0: catch (e) { michael@0: logger.warn("Failed to parse extensions.ini", e); michael@0: return null; michael@0: } michael@0: michael@0: // Also include the list of active bootstrapped extensions michael@0: for (let id in XPIProvider.bootstrappedAddons) michael@0: bundles.push(XPIProvider.bootstrappedAddons[id].descriptor); michael@0: michael@0: return bundles; michael@0: }, michael@0: michael@0: /** michael@0: * Retrieves migration data from the old extensions.rdf database. michael@0: * michael@0: * @return an object holding information about what add-ons were previously michael@0: * userDisabled and any updated compatibility information michael@0: */ michael@0: getMigrateDataFromRDF: function XPIDB_getMigrateDataFromRDF(aDbWasMissing) { michael@0: michael@0: // Migrate data from extensions.rdf michael@0: let rdffile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_OLD_DATABASE], true); michael@0: if (!rdffile.exists()) michael@0: return null; michael@0: michael@0: logger.debug("Migrating data from " + FILE_OLD_DATABASE); michael@0: let migrateData = {}; michael@0: michael@0: try { michael@0: let ds = gRDF.GetDataSourceBlocking(Services.io.newFileURI(rdffile).spec); michael@0: let root = Cc["@mozilla.org/rdf/container;1"]. michael@0: createInstance(Ci.nsIRDFContainer); michael@0: root.Init(ds, gRDF.GetResource(RDFURI_ITEM_ROOT)); michael@0: let elements = root.GetElements(); michael@0: michael@0: while (elements.hasMoreElements()) { michael@0: let source = elements.getNext().QueryInterface(Ci.nsIRDFResource); michael@0: michael@0: let location = getRDFProperty(ds, source, "installLocation"); michael@0: if (location) { michael@0: if (!(location in migrateData)) michael@0: migrateData[location] = {}; michael@0: let id = source.ValueUTF8.substring(PREFIX_ITEM_URI.length); michael@0: migrateData[location][id] = { michael@0: version: getRDFProperty(ds, source, "version"), michael@0: userDisabled: false, michael@0: targetApplications: [] michael@0: } michael@0: michael@0: let disabled = getRDFProperty(ds, source, "userDisabled"); michael@0: if (disabled == "true" || disabled == "needs-disable") michael@0: migrateData[location][id].userDisabled = true; michael@0: michael@0: let targetApps = ds.GetTargets(source, EM_R("targetApplication"), michael@0: true); michael@0: while (targetApps.hasMoreElements()) { michael@0: let targetApp = targetApps.getNext() michael@0: .QueryInterface(Ci.nsIRDFResource); michael@0: let appInfo = { michael@0: id: getRDFProperty(ds, targetApp, "id") michael@0: }; michael@0: michael@0: let minVersion = getRDFProperty(ds, targetApp, "updatedMinVersion"); michael@0: if (minVersion) { michael@0: appInfo.minVersion = minVersion; michael@0: appInfo.maxVersion = getRDFProperty(ds, targetApp, "updatedMaxVersion"); michael@0: } michael@0: else { michael@0: appInfo.minVersion = getRDFProperty(ds, targetApp, "minVersion"); michael@0: appInfo.maxVersion = getRDFProperty(ds, targetApp, "maxVersion"); michael@0: } michael@0: migrateData[location][id].targetApplications.push(appInfo); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: catch (e) { michael@0: logger.warn("Error reading " + FILE_OLD_DATABASE, e); michael@0: migrateData = null; michael@0: } michael@0: michael@0: return migrateData; michael@0: }, michael@0: michael@0: /** michael@0: * Retrieves migration data from a database that has an older or newer schema. michael@0: * michael@0: * @return an object holding information about what add-ons were previously michael@0: * userDisabled and any updated compatibility information michael@0: */ michael@0: getMigrateDataFromDatabase: function XPIDB_getMigrateDataFromDatabase(aConnection) { michael@0: let migrateData = {}; michael@0: michael@0: // Attempt to migrate data from a different (even future!) version of the michael@0: // database michael@0: try { michael@0: var stmt = aConnection.createStatement("PRAGMA table_info(addon)"); michael@0: michael@0: const REQUIRED = ["internal_id", "id", "location", "userDisabled", michael@0: "installDate", "version"]; michael@0: michael@0: let reqCount = 0; michael@0: let props = []; michael@0: for (let row in resultRows(stmt)) { michael@0: if (REQUIRED.indexOf(row.name) != -1) { michael@0: reqCount++; michael@0: props.push(row.name); michael@0: } michael@0: else if (DB_METADATA.indexOf(row.name) != -1) { michael@0: props.push(row.name); michael@0: } michael@0: else if (DB_BOOL_METADATA.indexOf(row.name) != -1) { michael@0: props.push(row.name); michael@0: } michael@0: } michael@0: michael@0: if (reqCount < REQUIRED.length) { michael@0: logger.error("Unable to read anything useful from the database"); michael@0: return null; michael@0: } michael@0: stmt.finalize(); michael@0: michael@0: stmt = aConnection.createStatement("SELECT " + props.join(",") + " FROM addon"); michael@0: for (let row in resultRows(stmt)) { michael@0: if (!(row.location in migrateData)) michael@0: migrateData[row.location] = {}; michael@0: let addonData = { michael@0: targetApplications: [] michael@0: } michael@0: migrateData[row.location][row.id] = addonData; michael@0: michael@0: props.forEach(function(aProp) { michael@0: if (aProp == "isForeignInstall") michael@0: addonData.foreignInstall = (row[aProp] == 1); michael@0: if (DB_BOOL_METADATA.indexOf(aProp) != -1) michael@0: addonData[aProp] = row[aProp] == 1; michael@0: else michael@0: addonData[aProp] = row[aProp]; michael@0: }) michael@0: } michael@0: michael@0: var taStmt = aConnection.createStatement("SELECT id, minVersion, " + michael@0: "maxVersion FROM " + michael@0: "targetApplication WHERE " + michael@0: "addon_internal_id=:internal_id"); michael@0: michael@0: for (let location in migrateData) { michael@0: for (let id in migrateData[location]) { michael@0: taStmt.params.internal_id = migrateData[location][id].internal_id; michael@0: delete migrateData[location][id].internal_id; michael@0: for (let row in resultRows(taStmt)) { michael@0: migrateData[location][id].targetApplications.push({ michael@0: id: row.id, michael@0: minVersion: row.minVersion, michael@0: maxVersion: row.maxVersion michael@0: }); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: catch (e) { michael@0: // An error here means the schema is too different to read michael@0: logger.error("Error migrating data", e); michael@0: return null; michael@0: } michael@0: finally { michael@0: if (taStmt) michael@0: taStmt.finalize(); michael@0: if (stmt) michael@0: stmt.finalize(); michael@0: } michael@0: michael@0: return migrateData; michael@0: }, michael@0: michael@0: /** michael@0: * Shuts down the database connection and releases all cached objects. michael@0: * Return: Promise{integer} resolves / rejects with the result of the DB michael@0: * flush after the database is flushed and michael@0: * all cleanup is done michael@0: */ michael@0: shutdown: function XPIDB_shutdown() { michael@0: logger.debug("shutdown"); michael@0: if (this.initialized) { michael@0: // If our last database I/O had an error, try one last time to save. michael@0: if (this.lastError) michael@0: this.saveChanges(); michael@0: michael@0: this.initialized = false; michael@0: michael@0: if (this._deferredSave) { michael@0: AddonManagerPrivate.recordSimpleMeasure( michael@0: "XPIDB_saves_total", this._deferredSave.totalSaves); michael@0: AddonManagerPrivate.recordSimpleMeasure( michael@0: "XPIDB_saves_overlapped", this._deferredSave.overlappedSaves); michael@0: AddonManagerPrivate.recordSimpleMeasure( michael@0: "XPIDB_saves_late", this._deferredSave.dirty ? 1 : 0); michael@0: } michael@0: michael@0: // Return a promise that any pending writes of the DB are complete and we michael@0: // are finished cleaning up michael@0: let flushPromise = this.flush(); michael@0: flushPromise.then(null, error => { michael@0: logger.error("Flush of XPI database failed", error); michael@0: AddonManagerPrivate.recordSimpleMeasure("XPIDB_shutdownFlush_failed", 1); michael@0: // If our last attempt to read or write the DB failed, force a new michael@0: // extensions.ini to be written to disk on the next startup michael@0: Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true); michael@0: }) michael@0: .then(count => { michael@0: // Clear out the cached addons data loaded from JSON michael@0: delete this.addonDB; michael@0: delete this._dbPromise; michael@0: // same for the deferred save michael@0: delete this._deferredSave; michael@0: // re-enable the schema version setter michael@0: delete this._schemaVersionSet; michael@0: }); michael@0: return flushPromise; michael@0: } michael@0: return Promise.resolve(0); michael@0: }, michael@0: michael@0: /** michael@0: * Return a list of all install locations known about by the database. This michael@0: * is often a a subset of the total install locations when not all have michael@0: * installed add-ons, occasionally a superset when an install location no michael@0: * longer exists. Only called from XPIProvider.processFileChanges, when michael@0: * the database should already be loaded. michael@0: * michael@0: * @return a Set of names of install locations michael@0: */ michael@0: getInstallLocations: function XPIDB_getInstallLocations() { michael@0: let locations = new Set(); michael@0: if (!this.addonDB) michael@0: return locations; michael@0: michael@0: for (let [, addon] of this.addonDB) { michael@0: locations.add(addon.location); michael@0: } michael@0: return locations; michael@0: }, michael@0: michael@0: /** michael@0: * Asynchronously list all addons that match the filter function michael@0: * @param aFilter michael@0: * Function that takes an addon instance and returns michael@0: * true if that addon should be included in the selected array michael@0: * @param aCallback michael@0: * Called back with an array of addons matching aFilter michael@0: * or an empty array if none match michael@0: */ michael@0: getAddonList: function(aFilter, aCallback) { michael@0: this.asyncLoadDB().then( michael@0: addonDB => { michael@0: let addonList = _filterDB(addonDB, aFilter); michael@0: asyncMap(addonList, getRepositoryAddon, makeSafe(aCallback)); michael@0: }) michael@0: .then(null, michael@0: error => { michael@0: logger.error("getAddonList failed", error); michael@0: makeSafe(aCallback)([]); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * (Possibly asynchronously) get the first addon that matches the filter function michael@0: * @param aFilter michael@0: * Function that takes an addon instance and returns michael@0: * true if that addon should be selected michael@0: * @param aCallback michael@0: * Called back with the addon, or null if no matching addon is found michael@0: */ michael@0: getAddon: function(aFilter, aCallback) { michael@0: return this.asyncLoadDB().then( michael@0: addonDB => { michael@0: getRepositoryAddon(_findAddon(addonDB, aFilter), makeSafe(aCallback)); michael@0: }) michael@0: .then(null, michael@0: error => { michael@0: logger.error("getAddon failed", e); michael@0: makeSafe(aCallback)(null); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Synchronously reads all the add-ons in a particular install location. michael@0: * Always called with the addon database already loaded. michael@0: * michael@0: * @param aLocation michael@0: * The name of the install location michael@0: * @return an array of DBAddonInternals michael@0: */ michael@0: getAddonsInLocation: function XPIDB_getAddonsInLocation(aLocation) { michael@0: return _filterDB(this.addonDB, aAddon => (aAddon.location == aLocation)); michael@0: }, michael@0: michael@0: /** michael@0: * Asynchronously gets an add-on with a particular ID in a particular michael@0: * install location. michael@0: * michael@0: * @param aId michael@0: * The ID of the add-on to retrieve michael@0: * @param aLocation michael@0: * The name of the install location michael@0: * @param aCallback michael@0: * A callback to pass the DBAddonInternal to michael@0: */ michael@0: getAddonInLocation: function XPIDB_getAddonInLocation(aId, aLocation, aCallback) { michael@0: this.asyncLoadDB().then( michael@0: addonDB => getRepositoryAddon(addonDB.get(aLocation + ":" + aId), michael@0: makeSafe(aCallback))); michael@0: }, michael@0: michael@0: /** michael@0: * Asynchronously gets the add-on with the specified ID that is visible. michael@0: * michael@0: * @param aId michael@0: * The ID of the add-on to retrieve michael@0: * @param aCallback michael@0: * A callback to pass the DBAddonInternal to michael@0: */ michael@0: getVisibleAddonForID: function XPIDB_getVisibleAddonForID(aId, aCallback) { michael@0: this.getAddon(aAddon => ((aAddon.id == aId) && aAddon.visible), michael@0: aCallback); michael@0: }, michael@0: michael@0: /** michael@0: * Asynchronously gets the visible add-ons, optionally restricting by type. michael@0: * michael@0: * @param aTypes michael@0: * An array of types to include or null to include all types michael@0: * @param aCallback michael@0: * A callback to pass the array of DBAddonInternals to michael@0: */ michael@0: getVisibleAddons: function XPIDB_getVisibleAddons(aTypes, aCallback) { michael@0: this.getAddonList(aAddon => (aAddon.visible && michael@0: (!aTypes || (aTypes.length == 0) || michael@0: (aTypes.indexOf(aAddon.type) > -1))), michael@0: aCallback); michael@0: }, michael@0: michael@0: /** michael@0: * Synchronously gets all add-ons of a particular type. michael@0: * michael@0: * @param aType michael@0: * The type of add-on to retrieve michael@0: * @return an array of DBAddonInternals michael@0: */ michael@0: getAddonsByType: function XPIDB_getAddonsByType(aType) { michael@0: if (!this.addonDB) { michael@0: // jank-tastic! Must synchronously load DB if the theme switches from michael@0: // an XPI theme to a lightweight theme before the DB has loaded, michael@0: // because we're called from sync XPIProvider.addonChanged michael@0: logger.warn("Synchronous load of XPI database due to getAddonsByType(" + aType + ")"); michael@0: AddonManagerPrivate.recordSimpleMeasure("XPIDB_lateOpen_byType", XPIProvider.runPhase); michael@0: this.syncLoadDB(true); michael@0: } michael@0: return _filterDB(this.addonDB, aAddon => (aAddon.type == aType)); michael@0: }, michael@0: michael@0: /** michael@0: * Synchronously gets an add-on with a particular internalName. michael@0: * michael@0: * @param aInternalName michael@0: * The internalName of the add-on to retrieve michael@0: * @return a DBAddonInternal michael@0: */ michael@0: getVisibleAddonForInternalName: function XPIDB_getVisibleAddonForInternalName(aInternalName) { michael@0: if (!this.addonDB) { michael@0: // This may be called when the DB hasn't otherwise been loaded michael@0: logger.warn("Synchronous load of XPI database due to getVisibleAddonForInternalName"); michael@0: AddonManagerPrivate.recordSimpleMeasure("XPIDB_lateOpen_forInternalName", michael@0: XPIProvider.runPhase); michael@0: this.syncLoadDB(true); michael@0: } michael@0: michael@0: return _findAddon(this.addonDB, michael@0: aAddon => aAddon.visible && michael@0: (aAddon.internalName == aInternalName)); michael@0: }, michael@0: michael@0: /** michael@0: * Asynchronously gets all add-ons with pending operations. michael@0: * michael@0: * @param aTypes michael@0: * The types of add-ons to retrieve or null to get all types michael@0: * @param aCallback michael@0: * A callback to pass the array of DBAddonInternal to michael@0: */ michael@0: getVisibleAddonsWithPendingOperations: michael@0: function XPIDB_getVisibleAddonsWithPendingOperations(aTypes, aCallback) { michael@0: michael@0: this.getAddonList( michael@0: aAddon => (aAddon.visible && michael@0: (aAddon.pendingUninstall || michael@0: // Logic here is tricky. If we're active but either michael@0: // disabled flag is set, we're pending disable; if we're not michael@0: // active and neither disabled flag is set, we're pending enable michael@0: (aAddon.active == (aAddon.userDisabled || aAddon.appDisabled))) && michael@0: (!aTypes || (aTypes.length == 0) || (aTypes.indexOf(aAddon.type) > -1))), michael@0: aCallback); michael@0: }, michael@0: michael@0: /** michael@0: * Asynchronously get an add-on by its Sync GUID. michael@0: * michael@0: * @param aGUID michael@0: * Sync GUID of add-on to fetch michael@0: * @param aCallback michael@0: * A callback to pass the DBAddonInternal record to. Receives null michael@0: * if no add-on with that GUID is found. michael@0: * michael@0: */ michael@0: getAddonBySyncGUID: function XPIDB_getAddonBySyncGUID(aGUID, aCallback) { michael@0: this.getAddon(aAddon => aAddon.syncGUID == aGUID, michael@0: aCallback); michael@0: }, michael@0: michael@0: /** michael@0: * Synchronously gets all add-ons in the database. michael@0: * This is only called from the preference observer for the default michael@0: * compatibility version preference, so we can return an empty list if michael@0: * we haven't loaded the database yet. michael@0: * michael@0: * @return an array of DBAddonInternals michael@0: */ michael@0: getAddons: function XPIDB_getAddons() { michael@0: if (!this.addonDB) { michael@0: return []; michael@0: } michael@0: return _filterDB(this.addonDB, aAddon => true); michael@0: }, michael@0: michael@0: /** michael@0: * Synchronously adds an AddonInternal's metadata to the database. michael@0: * michael@0: * @param aAddon michael@0: * AddonInternal to add michael@0: * @param aDescriptor michael@0: * The file descriptor of the add-on michael@0: * @return The DBAddonInternal that was added to the database michael@0: */ michael@0: addAddonMetadata: function XPIDB_addAddonMetadata(aAddon, aDescriptor) { michael@0: if (!this.addonDB) { michael@0: AddonManagerPrivate.recordSimpleMeasure("XPIDB_lateOpen_addMetadata", michael@0: XPIProvider.runPhase); michael@0: this.syncLoadDB(false); michael@0: } michael@0: michael@0: let newAddon = new DBAddonInternal(aAddon); michael@0: newAddon.descriptor = aDescriptor; michael@0: this.addonDB.set(newAddon._key, newAddon); michael@0: if (newAddon.visible) { michael@0: this.makeAddonVisible(newAddon); michael@0: } michael@0: michael@0: this.saveChanges(); michael@0: return newAddon; michael@0: }, michael@0: michael@0: /** michael@0: * Synchronously updates an add-on's metadata in the database. Currently just michael@0: * removes and recreates. michael@0: * michael@0: * @param aOldAddon michael@0: * The DBAddonInternal to be replaced michael@0: * @param aNewAddon michael@0: * The new AddonInternal to add michael@0: * @param aDescriptor michael@0: * The file descriptor of the add-on michael@0: * @return The DBAddonInternal that was added to the database michael@0: */ michael@0: updateAddonMetadata: function XPIDB_updateAddonMetadata(aOldAddon, aNewAddon, michael@0: aDescriptor) { michael@0: this.removeAddonMetadata(aOldAddon); michael@0: aNewAddon.syncGUID = aOldAddon.syncGUID; michael@0: aNewAddon.installDate = aOldAddon.installDate; michael@0: aNewAddon.applyBackgroundUpdates = aOldAddon.applyBackgroundUpdates; michael@0: aNewAddon.foreignInstall = aOldAddon.foreignInstall; michael@0: aNewAddon.active = (aNewAddon.visible && !aNewAddon.userDisabled && michael@0: !aNewAddon.appDisabled && !aNewAddon.pendingUninstall); michael@0: michael@0: // addAddonMetadata does a saveChanges() michael@0: return this.addAddonMetadata(aNewAddon, aDescriptor); michael@0: }, michael@0: michael@0: /** michael@0: * Synchronously removes an add-on from the database. michael@0: * michael@0: * @param aAddon michael@0: * The DBAddonInternal being removed michael@0: */ michael@0: removeAddonMetadata: function XPIDB_removeAddonMetadata(aAddon) { michael@0: this.addonDB.delete(aAddon._key); michael@0: this.saveChanges(); michael@0: }, michael@0: michael@0: /** michael@0: * Synchronously marks a DBAddonInternal as visible marking all other michael@0: * instances with the same ID as not visible. michael@0: * michael@0: * @param aAddon michael@0: * The DBAddonInternal to make visible michael@0: */ michael@0: makeAddonVisible: function XPIDB_makeAddonVisible(aAddon) { michael@0: logger.debug("Make addon " + aAddon._key + " visible"); michael@0: for (let [, otherAddon] of this.addonDB) { michael@0: if ((otherAddon.id == aAddon.id) && (otherAddon._key != aAddon._key)) { michael@0: logger.debug("Hide addon " + otherAddon._key); michael@0: otherAddon.visible = false; michael@0: } michael@0: } michael@0: aAddon.visible = true; michael@0: this.saveChanges(); michael@0: }, michael@0: michael@0: /** michael@0: * Synchronously sets properties for an add-on. michael@0: * michael@0: * @param aAddon michael@0: * The DBAddonInternal being updated michael@0: * @param aProperties michael@0: * A dictionary of properties to set michael@0: */ michael@0: setAddonProperties: function XPIDB_setAddonProperties(aAddon, aProperties) { michael@0: for (let key in aProperties) { michael@0: aAddon[key] = aProperties[key]; michael@0: } michael@0: this.saveChanges(); michael@0: }, michael@0: michael@0: /** michael@0: * Synchronously sets the Sync GUID for an add-on. michael@0: * Only called when the database is already loaded. michael@0: * michael@0: * @param aAddon michael@0: * The DBAddonInternal being updated michael@0: * @param aGUID michael@0: * GUID string to set the value to michael@0: * @throws if another addon already has the specified GUID michael@0: */ michael@0: setAddonSyncGUID: function XPIDB_setAddonSyncGUID(aAddon, aGUID) { michael@0: // Need to make sure no other addon has this GUID michael@0: function excludeSyncGUID(otherAddon) { michael@0: return (otherAddon._key != aAddon._key) && (otherAddon.syncGUID == aGUID); michael@0: } michael@0: let otherAddon = _findAddon(this.addonDB, excludeSyncGUID); michael@0: if (otherAddon) { michael@0: throw new Error("Addon sync GUID conflict for addon " + aAddon._key + michael@0: ": " + otherAddon._key + " already has GUID " + aGUID); michael@0: } michael@0: aAddon.syncGUID = aGUID; michael@0: this.saveChanges(); michael@0: }, michael@0: michael@0: /** michael@0: * Synchronously updates an add-on's active flag in the database. michael@0: * michael@0: * @param aAddon michael@0: * The DBAddonInternal to update michael@0: */ michael@0: updateAddonActive: function XPIDB_updateAddonActive(aAddon, aActive) { michael@0: logger.debug("Updating active state for add-on " + aAddon.id + " to " + aActive); michael@0: michael@0: aAddon.active = aActive; michael@0: this.saveChanges(); michael@0: }, michael@0: michael@0: /** michael@0: * Synchronously calculates and updates all the active flags in the database. michael@0: */ michael@0: updateActiveAddons: function XPIDB_updateActiveAddons() { michael@0: if (!this.addonDB) { michael@0: logger.warn("updateActiveAddons called when DB isn't loaded"); michael@0: // force the DB to load michael@0: AddonManagerPrivate.recordSimpleMeasure("XPIDB_lateOpen_updateActive", michael@0: XPIProvider.runPhase); michael@0: this.syncLoadDB(true); michael@0: } michael@0: logger.debug("Updating add-on states"); michael@0: for (let [, addon] of this.addonDB) { michael@0: let newActive = (addon.visible && !addon.userDisabled && michael@0: !addon.softDisabled && !addon.appDisabled && michael@0: !addon.pendingUninstall); michael@0: if (newActive != addon.active) { michael@0: addon.active = newActive; michael@0: this.saveChanges(); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Writes out the XPI add-ons list for the platform to read. michael@0: * @return true if the file was successfully updated, false otherwise michael@0: */ michael@0: writeAddonsList: function XPIDB_writeAddonsList() { michael@0: if (!this.addonDB) { michael@0: // force the DB to load michael@0: AddonManagerPrivate.recordSimpleMeasure("XPIDB_lateOpen_writeList", michael@0: XPIProvider.runPhase); michael@0: this.syncLoadDB(true); michael@0: } michael@0: Services.appinfo.invalidateCachesOnRestart(); michael@0: michael@0: let addonsList = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST], michael@0: true); michael@0: let enabledAddons = []; michael@0: let text = "[ExtensionDirs]\r\n"; michael@0: let count = 0; michael@0: let fullCount = 0; michael@0: michael@0: let activeAddons = _filterDB( michael@0: this.addonDB, michael@0: aAddon => aAddon.active && !aAddon.bootstrap && (aAddon.type != "theme")); michael@0: michael@0: for (let row of activeAddons) { michael@0: text += "Extension" + (count++) + "=" + row.descriptor + "\r\n"; michael@0: enabledAddons.push(encodeURIComponent(row.id) + ":" + michael@0: encodeURIComponent(row.version)); michael@0: } michael@0: fullCount += count; michael@0: michael@0: // The selected skin may come from an inactive theme (the default theme michael@0: // when a lightweight theme is applied for example) michael@0: text += "\r\n[ThemeDirs]\r\n"; michael@0: michael@0: let dssEnabled = false; michael@0: try { michael@0: dssEnabled = Services.prefs.getBoolPref(PREF_EM_DSS_ENABLED); michael@0: } catch (e) {} michael@0: michael@0: let themes = []; michael@0: if (dssEnabled) { michael@0: themes = _filterDB(this.addonDB, aAddon => aAddon.type == "theme"); michael@0: } michael@0: else { michael@0: let activeTheme = _findAddon( michael@0: this.addonDB, michael@0: aAddon => (aAddon.type == "theme") && michael@0: (aAddon.internalName == XPIProvider.selectedSkin)); michael@0: if (activeTheme) { michael@0: themes.push(activeTheme); michael@0: } michael@0: } michael@0: michael@0: if (themes.length > 0) { michael@0: count = 0; michael@0: for (let row of themes) { michael@0: text += "Extension" + (count++) + "=" + row.descriptor + "\r\n"; michael@0: enabledAddons.push(encodeURIComponent(row.id) + ":" + michael@0: encodeURIComponent(row.version)); michael@0: } michael@0: fullCount += count; michael@0: } michael@0: michael@0: if (fullCount > 0) { michael@0: logger.debug("Writing add-ons list"); michael@0: michael@0: try { michael@0: let addonsListTmp = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST + ".tmp"], michael@0: true); michael@0: var fos = FileUtils.openFileOutputStream(addonsListTmp); michael@0: fos.write(text, text.length); michael@0: fos.close(); michael@0: addonsListTmp.moveTo(addonsListTmp.parent, FILE_XPI_ADDONS_LIST); michael@0: michael@0: Services.prefs.setCharPref(PREF_EM_ENABLED_ADDONS, enabledAddons.join(",")); michael@0: } michael@0: catch (e) { michael@0: logger.error("Failed to write add-ons list to profile directory", e); michael@0: return false; michael@0: } michael@0: } michael@0: else { michael@0: if (addonsList.exists()) { michael@0: logger.debug("Deleting add-ons list"); michael@0: try { michael@0: addonsList.remove(false); michael@0: } michael@0: catch (e) { michael@0: logger.error("Failed to remove " + addonsList.path, e); michael@0: return false; michael@0: } michael@0: } michael@0: michael@0: Services.prefs.clearUserPref(PREF_EM_ENABLED_ADDONS); michael@0: } michael@0: return true; michael@0: } michael@0: };