Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
michael@0 | 1 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
michael@0 | 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 4 | |
michael@0 | 5 | "use strict"; |
michael@0 | 6 | |
michael@0 | 7 | const Cc = Components.classes; |
michael@0 | 8 | const Ci = Components.interfaces; |
michael@0 | 9 | const Cr = Components.results; |
michael@0 | 10 | const Cu = Components.utils; |
michael@0 | 11 | |
michael@0 | 12 | Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
michael@0 | 13 | Cu.import("resource://gre/modules/Services.jsm"); |
michael@0 | 14 | Cu.import("resource://gre/modules/AddonManager.jsm"); |
michael@0 | 15 | |
michael@0 | 16 | XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository", |
michael@0 | 17 | "resource://gre/modules/addons/AddonRepository.jsm"); |
michael@0 | 18 | XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", |
michael@0 | 19 | "resource://gre/modules/FileUtils.jsm"); |
michael@0 | 20 | XPCOMUtils.defineLazyModuleGetter(this, "DeferredSave", |
michael@0 | 21 | "resource://gre/modules/DeferredSave.jsm"); |
michael@0 | 22 | XPCOMUtils.defineLazyModuleGetter(this, "Promise", |
michael@0 | 23 | "resource://gre/modules/Promise.jsm"); |
michael@0 | 24 | XPCOMUtils.defineLazyModuleGetter(this, "OS", |
michael@0 | 25 | "resource://gre/modules/osfile.jsm"); |
michael@0 | 26 | |
michael@0 | 27 | Cu.import("resource://gre/modules/Log.jsm"); |
michael@0 | 28 | const LOGGER_ID = "addons.xpi-utils"; |
michael@0 | 29 | |
michael@0 | 30 | // Create a new logger for use by the Addons XPI Provider Utils |
michael@0 | 31 | // (Requires AddonManager.jsm) |
michael@0 | 32 | let logger = Log.repository.getLogger(LOGGER_ID); |
michael@0 | 33 | |
michael@0 | 34 | const KEY_PROFILEDIR = "ProfD"; |
michael@0 | 35 | const FILE_DATABASE = "extensions.sqlite"; |
michael@0 | 36 | const FILE_JSON_DB = "extensions.json"; |
michael@0 | 37 | const FILE_OLD_DATABASE = "extensions.rdf"; |
michael@0 | 38 | const FILE_XPI_ADDONS_LIST = "extensions.ini"; |
michael@0 | 39 | |
michael@0 | 40 | // The value for this is in Makefile.in |
michael@0 | 41 | #expand const DB_SCHEMA = __MOZ_EXTENSIONS_DB_SCHEMA__; |
michael@0 | 42 | |
michael@0 | 43 | // The last version of DB_SCHEMA implemented in SQLITE |
michael@0 | 44 | const LAST_SQLITE_DB_SCHEMA = 14; |
michael@0 | 45 | const PREF_DB_SCHEMA = "extensions.databaseSchema"; |
michael@0 | 46 | const PREF_PENDING_OPERATIONS = "extensions.pendingOperations"; |
michael@0 | 47 | const PREF_EM_ENABLED_ADDONS = "extensions.enabledAddons"; |
michael@0 | 48 | const PREF_EM_DSS_ENABLED = "extensions.dss.enabled"; |
michael@0 | 49 | |
michael@0 | 50 | |
michael@0 | 51 | // Properties that only exist in the database |
michael@0 | 52 | const DB_METADATA = ["syncGUID", |
michael@0 | 53 | "installDate", |
michael@0 | 54 | "updateDate", |
michael@0 | 55 | "size", |
michael@0 | 56 | "sourceURI", |
michael@0 | 57 | "releaseNotesURI", |
michael@0 | 58 | "applyBackgroundUpdates"]; |
michael@0 | 59 | const DB_BOOL_METADATA = ["visible", "active", "userDisabled", "appDisabled", |
michael@0 | 60 | "pendingUninstall", "bootstrap", "skinnable", |
michael@0 | 61 | "softDisabled", "isForeignInstall", |
michael@0 | 62 | "hasBinaryComponents", "strictCompatibility"]; |
michael@0 | 63 | |
michael@0 | 64 | // Properties to save in JSON file |
michael@0 | 65 | const PROP_JSON_FIELDS = ["id", "syncGUID", "location", "version", "type", |
michael@0 | 66 | "internalName", "updateURL", "updateKey", "optionsURL", |
michael@0 | 67 | "optionsType", "aboutURL", "iconURL", "icon64URL", |
michael@0 | 68 | "defaultLocale", "visible", "active", "userDisabled", |
michael@0 | 69 | "appDisabled", "pendingUninstall", "descriptor", "installDate", |
michael@0 | 70 | "updateDate", "applyBackgroundUpdates", "bootstrap", |
michael@0 | 71 | "skinnable", "size", "sourceURI", "releaseNotesURI", |
michael@0 | 72 | "softDisabled", "foreignInstall", "hasBinaryComponents", |
michael@0 | 73 | "strictCompatibility", "locales", "targetApplications", |
michael@0 | 74 | "targetPlatforms"]; |
michael@0 | 75 | |
michael@0 | 76 | // Time to wait before async save of XPI JSON database, in milliseconds |
michael@0 | 77 | const ASYNC_SAVE_DELAY_MS = 20; |
michael@0 | 78 | |
michael@0 | 79 | const PREFIX_ITEM_URI = "urn:mozilla:item:"; |
michael@0 | 80 | const RDFURI_ITEM_ROOT = "urn:mozilla:item:root" |
michael@0 | 81 | const PREFIX_NS_EM = "http://www.mozilla.org/2004/em-rdf#"; |
michael@0 | 82 | |
michael@0 | 83 | XPCOMUtils.defineLazyServiceGetter(this, "gRDF", "@mozilla.org/rdf/rdf-service;1", |
michael@0 | 84 | Ci.nsIRDFService); |
michael@0 | 85 | |
michael@0 | 86 | function EM_R(aProperty) { |
michael@0 | 87 | return gRDF.GetResource(PREFIX_NS_EM + aProperty); |
michael@0 | 88 | } |
michael@0 | 89 | |
michael@0 | 90 | /** |
michael@0 | 91 | * Converts an RDF literal, resource or integer into a string. |
michael@0 | 92 | * |
michael@0 | 93 | * @param aLiteral |
michael@0 | 94 | * The RDF object to convert |
michael@0 | 95 | * @return a string if the object could be converted or null |
michael@0 | 96 | */ |
michael@0 | 97 | function getRDFValue(aLiteral) { |
michael@0 | 98 | if (aLiteral instanceof Ci.nsIRDFLiteral) |
michael@0 | 99 | return aLiteral.Value; |
michael@0 | 100 | if (aLiteral instanceof Ci.nsIRDFResource) |
michael@0 | 101 | return aLiteral.Value; |
michael@0 | 102 | if (aLiteral instanceof Ci.nsIRDFInt) |
michael@0 | 103 | return aLiteral.Value; |
michael@0 | 104 | return null; |
michael@0 | 105 | } |
michael@0 | 106 | |
michael@0 | 107 | /** |
michael@0 | 108 | * Gets an RDF property as a string |
michael@0 | 109 | * |
michael@0 | 110 | * @param aDs |
michael@0 | 111 | * The RDF datasource to read the property from |
michael@0 | 112 | * @param aResource |
michael@0 | 113 | * The RDF resource to read the property from |
michael@0 | 114 | * @param aProperty |
michael@0 | 115 | * The property to read |
michael@0 | 116 | * @return a string if the property existed or null |
michael@0 | 117 | */ |
michael@0 | 118 | function getRDFProperty(aDs, aResource, aProperty) { |
michael@0 | 119 | return getRDFValue(aDs.GetTarget(aResource, EM_R(aProperty), true)); |
michael@0 | 120 | } |
michael@0 | 121 | |
michael@0 | 122 | /** |
michael@0 | 123 | * Asynchronously fill in the _repositoryAddon field for one addon |
michael@0 | 124 | */ |
michael@0 | 125 | function getRepositoryAddon(aAddon, aCallback) { |
michael@0 | 126 | if (!aAddon) { |
michael@0 | 127 | aCallback(aAddon); |
michael@0 | 128 | return; |
michael@0 | 129 | } |
michael@0 | 130 | function completeAddon(aRepositoryAddon) { |
michael@0 | 131 | aAddon._repositoryAddon = aRepositoryAddon; |
michael@0 | 132 | aAddon.compatibilityOverrides = aRepositoryAddon ? |
michael@0 | 133 | aRepositoryAddon.compatibilityOverrides : |
michael@0 | 134 | null; |
michael@0 | 135 | aCallback(aAddon); |
michael@0 | 136 | } |
michael@0 | 137 | AddonRepository.getCachedAddonByID(aAddon.id, completeAddon); |
michael@0 | 138 | } |
michael@0 | 139 | |
michael@0 | 140 | /** |
michael@0 | 141 | * Wrap an API-supplied function in an exception handler to make it safe to call |
michael@0 | 142 | */ |
michael@0 | 143 | function makeSafe(aCallback) { |
michael@0 | 144 | return function(...aArgs) { |
michael@0 | 145 | try { |
michael@0 | 146 | aCallback(...aArgs); |
michael@0 | 147 | } |
michael@0 | 148 | catch(ex) { |
michael@0 | 149 | logger.warn("XPI Database callback failed", ex); |
michael@0 | 150 | } |
michael@0 | 151 | } |
michael@0 | 152 | } |
michael@0 | 153 | |
michael@0 | 154 | /** |
michael@0 | 155 | * A helper method to asynchronously call a function on an array |
michael@0 | 156 | * of objects, calling a callback when function(x) has been gathered |
michael@0 | 157 | * for every element of the array. |
michael@0 | 158 | * WARNING: not currently error-safe; if the async function does not call |
michael@0 | 159 | * our internal callback for any of the array elements, asyncMap will not |
michael@0 | 160 | * call the callback parameter. |
michael@0 | 161 | * |
michael@0 | 162 | * @param aObjects |
michael@0 | 163 | * The array of objects to process asynchronously |
michael@0 | 164 | * @param aMethod |
michael@0 | 165 | * Function with signature function(object, function aCallback(f_of_object)) |
michael@0 | 166 | * @param aCallback |
michael@0 | 167 | * Function with signature f([aMethod(object)]), called when all values |
michael@0 | 168 | * are available |
michael@0 | 169 | */ |
michael@0 | 170 | function asyncMap(aObjects, aMethod, aCallback) { |
michael@0 | 171 | var resultsPending = aObjects.length; |
michael@0 | 172 | var results = [] |
michael@0 | 173 | if (resultsPending == 0) { |
michael@0 | 174 | aCallback(results); |
michael@0 | 175 | return; |
michael@0 | 176 | } |
michael@0 | 177 | |
michael@0 | 178 | function asyncMap_gotValue(aIndex, aValue) { |
michael@0 | 179 | results[aIndex] = aValue; |
michael@0 | 180 | if (--resultsPending == 0) { |
michael@0 | 181 | aCallback(results); |
michael@0 | 182 | } |
michael@0 | 183 | } |
michael@0 | 184 | |
michael@0 | 185 | aObjects.map(function asyncMap_each(aObject, aIndex, aArray) { |
michael@0 | 186 | try { |
michael@0 | 187 | aMethod(aObject, function asyncMap_callback(aResult) { |
michael@0 | 188 | asyncMap_gotValue(aIndex, aResult); |
michael@0 | 189 | }); |
michael@0 | 190 | } |
michael@0 | 191 | catch (e) { |
michael@0 | 192 | logger.warn("Async map function failed", e); |
michael@0 | 193 | asyncMap_gotValue(aIndex, undefined); |
michael@0 | 194 | } |
michael@0 | 195 | }); |
michael@0 | 196 | } |
michael@0 | 197 | |
michael@0 | 198 | /** |
michael@0 | 199 | * A generator to synchronously return result rows from an mozIStorageStatement. |
michael@0 | 200 | * |
michael@0 | 201 | * @param aStatement |
michael@0 | 202 | * The statement to execute |
michael@0 | 203 | */ |
michael@0 | 204 | function resultRows(aStatement) { |
michael@0 | 205 | try { |
michael@0 | 206 | while (stepStatement(aStatement)) |
michael@0 | 207 | yield aStatement.row; |
michael@0 | 208 | } |
michael@0 | 209 | finally { |
michael@0 | 210 | aStatement.reset(); |
michael@0 | 211 | } |
michael@0 | 212 | } |
michael@0 | 213 | |
michael@0 | 214 | |
michael@0 | 215 | /** |
michael@0 | 216 | * A helper function to log an SQL error. |
michael@0 | 217 | * |
michael@0 | 218 | * @param aError |
michael@0 | 219 | * The storage error code associated with the error |
michael@0 | 220 | * @param aErrorString |
michael@0 | 221 | * An error message |
michael@0 | 222 | */ |
michael@0 | 223 | function logSQLError(aError, aErrorString) { |
michael@0 | 224 | logger.error("SQL error " + aError + ": " + aErrorString); |
michael@0 | 225 | } |
michael@0 | 226 | |
michael@0 | 227 | /** |
michael@0 | 228 | * A helper function to log any errors that occur during async statements. |
michael@0 | 229 | * |
michael@0 | 230 | * @param aError |
michael@0 | 231 | * A mozIStorageError to log |
michael@0 | 232 | */ |
michael@0 | 233 | function asyncErrorLogger(aError) { |
michael@0 | 234 | logSQLError(aError.result, aError.message); |
michael@0 | 235 | } |
michael@0 | 236 | |
michael@0 | 237 | /** |
michael@0 | 238 | * A helper function to step a statement synchronously and log any error that |
michael@0 | 239 | * occurs. |
michael@0 | 240 | * |
michael@0 | 241 | * @param aStatement |
michael@0 | 242 | * A mozIStorageStatement to execute |
michael@0 | 243 | */ |
michael@0 | 244 | function stepStatement(aStatement) { |
michael@0 | 245 | try { |
michael@0 | 246 | return aStatement.executeStep(); |
michael@0 | 247 | } |
michael@0 | 248 | catch (e) { |
michael@0 | 249 | logSQLError(XPIDatabase.connection.lastError, |
michael@0 | 250 | XPIDatabase.connection.lastErrorString); |
michael@0 | 251 | throw e; |
michael@0 | 252 | } |
michael@0 | 253 | } |
michael@0 | 254 | |
michael@0 | 255 | |
michael@0 | 256 | /** |
michael@0 | 257 | * Copies properties from one object to another. If no target object is passed |
michael@0 | 258 | * a new object will be created and returned. |
michael@0 | 259 | * |
michael@0 | 260 | * @param aObject |
michael@0 | 261 | * An object to copy from |
michael@0 | 262 | * @param aProperties |
michael@0 | 263 | * An array of properties to be copied |
michael@0 | 264 | * @param aTarget |
michael@0 | 265 | * An optional target object to copy the properties to |
michael@0 | 266 | * @return the object that the properties were copied onto |
michael@0 | 267 | */ |
michael@0 | 268 | function copyProperties(aObject, aProperties, aTarget) { |
michael@0 | 269 | if (!aTarget) |
michael@0 | 270 | aTarget = {}; |
michael@0 | 271 | aProperties.forEach(function(aProp) { |
michael@0 | 272 | aTarget[aProp] = aObject[aProp]; |
michael@0 | 273 | }); |
michael@0 | 274 | return aTarget; |
michael@0 | 275 | } |
michael@0 | 276 | |
michael@0 | 277 | /** |
michael@0 | 278 | * Copies properties from a mozIStorageRow to an object. If no target object is |
michael@0 | 279 | * passed a new object will be created and returned. |
michael@0 | 280 | * |
michael@0 | 281 | * @param aRow |
michael@0 | 282 | * A mozIStorageRow to copy from |
michael@0 | 283 | * @param aProperties |
michael@0 | 284 | * An array of properties to be copied |
michael@0 | 285 | * @param aTarget |
michael@0 | 286 | * An optional target object to copy the properties to |
michael@0 | 287 | * @return the object that the properties were copied onto |
michael@0 | 288 | */ |
michael@0 | 289 | function copyRowProperties(aRow, aProperties, aTarget) { |
michael@0 | 290 | if (!aTarget) |
michael@0 | 291 | aTarget = {}; |
michael@0 | 292 | aProperties.forEach(function(aProp) { |
michael@0 | 293 | aTarget[aProp] = aRow.getResultByName(aProp); |
michael@0 | 294 | }); |
michael@0 | 295 | return aTarget; |
michael@0 | 296 | } |
michael@0 | 297 | |
michael@0 | 298 | /** |
michael@0 | 299 | * The DBAddonInternal is a special AddonInternal that has been retrieved from |
michael@0 | 300 | * the database. The constructor will initialize the DBAddonInternal with a set |
michael@0 | 301 | * of fields, which could come from either the JSON store or as an |
michael@0 | 302 | * XPIProvider.AddonInternal created from an addon's manifest |
michael@0 | 303 | * @constructor |
michael@0 | 304 | * @param aLoaded |
michael@0 | 305 | * Addon data fields loaded from JSON or the addon manifest. |
michael@0 | 306 | */ |
michael@0 | 307 | function DBAddonInternal(aLoaded) { |
michael@0 | 308 | copyProperties(aLoaded, PROP_JSON_FIELDS, this); |
michael@0 | 309 | |
michael@0 | 310 | if (aLoaded._installLocation) { |
michael@0 | 311 | this._installLocation = aLoaded._installLocation; |
michael@0 | 312 | this.location = aLoaded._installLocation._name; |
michael@0 | 313 | } |
michael@0 | 314 | else if (aLoaded.location) { |
michael@0 | 315 | this._installLocation = XPIProvider.installLocationsByName[this.location]; |
michael@0 | 316 | } |
michael@0 | 317 | |
michael@0 | 318 | this._key = this.location + ":" + this.id; |
michael@0 | 319 | |
michael@0 | 320 | try { |
michael@0 | 321 | this._sourceBundle = this._installLocation.getLocationForID(this.id); |
michael@0 | 322 | } |
michael@0 | 323 | catch (e) { |
michael@0 | 324 | // An exception will be thrown if the add-on appears in the database but |
michael@0 | 325 | // not on disk. In general this should only happen during startup as |
michael@0 | 326 | // this change is being detected. |
michael@0 | 327 | } |
michael@0 | 328 | |
michael@0 | 329 | XPCOMUtils.defineLazyGetter(this, "pendingUpgrade", |
michael@0 | 330 | function DBA_pendingUpgradeGetter() { |
michael@0 | 331 | for (let install of XPIProvider.installs) { |
michael@0 | 332 | if (install.state == AddonManager.STATE_INSTALLED && |
michael@0 | 333 | !(install.addon.inDatabase) && |
michael@0 | 334 | install.addon.id == this.id && |
michael@0 | 335 | install.installLocation == this._installLocation) { |
michael@0 | 336 | delete this.pendingUpgrade; |
michael@0 | 337 | return this.pendingUpgrade = install.addon; |
michael@0 | 338 | } |
michael@0 | 339 | }; |
michael@0 | 340 | return null; |
michael@0 | 341 | }); |
michael@0 | 342 | } |
michael@0 | 343 | |
michael@0 | 344 | function DBAddonInternalPrototype() |
michael@0 | 345 | { |
michael@0 | 346 | this.applyCompatibilityUpdate = |
michael@0 | 347 | function(aUpdate, aSyncCompatibility) { |
michael@0 | 348 | this.targetApplications.forEach(function(aTargetApp) { |
michael@0 | 349 | aUpdate.targetApplications.forEach(function(aUpdateTarget) { |
michael@0 | 350 | if (aTargetApp.id == aUpdateTarget.id && (aSyncCompatibility || |
michael@0 | 351 | Services.vc.compare(aTargetApp.maxVersion, aUpdateTarget.maxVersion) < 0)) { |
michael@0 | 352 | aTargetApp.minVersion = aUpdateTarget.minVersion; |
michael@0 | 353 | aTargetApp.maxVersion = aUpdateTarget.maxVersion; |
michael@0 | 354 | XPIDatabase.saveChanges(); |
michael@0 | 355 | } |
michael@0 | 356 | }); |
michael@0 | 357 | }); |
michael@0 | 358 | XPIProvider.updateAddonDisabledState(this); |
michael@0 | 359 | }; |
michael@0 | 360 | |
michael@0 | 361 | this.toJSON = |
michael@0 | 362 | function() { |
michael@0 | 363 | return copyProperties(this, PROP_JSON_FIELDS); |
michael@0 | 364 | }; |
michael@0 | 365 | |
michael@0 | 366 | Object.defineProperty(this, "inDatabase", |
michael@0 | 367 | { get: function() { return true; }, |
michael@0 | 368 | enumerable: true, |
michael@0 | 369 | configurable: true }); |
michael@0 | 370 | } |
michael@0 | 371 | DBAddonInternalPrototype.prototype = AddonInternal.prototype; |
michael@0 | 372 | |
michael@0 | 373 | DBAddonInternal.prototype = new DBAddonInternalPrototype(); |
michael@0 | 374 | |
michael@0 | 375 | /** |
michael@0 | 376 | * Internal interface: find an addon from an already loaded addonDB |
michael@0 | 377 | */ |
michael@0 | 378 | function _findAddon(addonDB, aFilter) { |
michael@0 | 379 | for (let [, addon] of addonDB) { |
michael@0 | 380 | if (aFilter(addon)) { |
michael@0 | 381 | return addon; |
michael@0 | 382 | } |
michael@0 | 383 | } |
michael@0 | 384 | return null; |
michael@0 | 385 | } |
michael@0 | 386 | |
michael@0 | 387 | /** |
michael@0 | 388 | * Internal interface to get a filtered list of addons from a loaded addonDB |
michael@0 | 389 | */ |
michael@0 | 390 | function _filterDB(addonDB, aFilter) { |
michael@0 | 391 | let addonList = []; |
michael@0 | 392 | for (let [, addon] of addonDB) { |
michael@0 | 393 | if (aFilter(addon)) { |
michael@0 | 394 | addonList.push(addon); |
michael@0 | 395 | } |
michael@0 | 396 | } |
michael@0 | 397 | |
michael@0 | 398 | return addonList; |
michael@0 | 399 | } |
michael@0 | 400 | |
michael@0 | 401 | this.XPIDatabase = { |
michael@0 | 402 | // true if the database connection has been opened |
michael@0 | 403 | initialized: false, |
michael@0 | 404 | // The database file |
michael@0 | 405 | jsonFile: FileUtils.getFile(KEY_PROFILEDIR, [FILE_JSON_DB], true), |
michael@0 | 406 | // Migration data loaded from an old version of the database. |
michael@0 | 407 | migrateData: null, |
michael@0 | 408 | // Active add-on directories loaded from extensions.ini and prefs at startup. |
michael@0 | 409 | activeBundles: null, |
michael@0 | 410 | |
michael@0 | 411 | // Saved error object if we fail to read an existing database |
michael@0 | 412 | _loadError: null, |
michael@0 | 413 | |
michael@0 | 414 | // Error reported by our most recent attempt to read or write the database, if any |
michael@0 | 415 | get lastError() { |
michael@0 | 416 | if (this._loadError) |
michael@0 | 417 | return this._loadError; |
michael@0 | 418 | if (this._deferredSave) |
michael@0 | 419 | return this._deferredSave.lastError; |
michael@0 | 420 | return null; |
michael@0 | 421 | }, |
michael@0 | 422 | |
michael@0 | 423 | /** |
michael@0 | 424 | * Mark the current stored data dirty, and schedule a flush to disk |
michael@0 | 425 | */ |
michael@0 | 426 | saveChanges: function() { |
michael@0 | 427 | if (!this.initialized) { |
michael@0 | 428 | throw new Error("Attempt to use XPI database when it is not initialized"); |
michael@0 | 429 | } |
michael@0 | 430 | |
michael@0 | 431 | if (!this._deferredSave) { |
michael@0 | 432 | this._deferredSave = new DeferredSave(this.jsonFile.path, |
michael@0 | 433 | () => JSON.stringify(this), |
michael@0 | 434 | ASYNC_SAVE_DELAY_MS); |
michael@0 | 435 | } |
michael@0 | 436 | |
michael@0 | 437 | let promise = this._deferredSave.saveChanges(); |
michael@0 | 438 | if (!this._schemaVersionSet) { |
michael@0 | 439 | this._schemaVersionSet = true; |
michael@0 | 440 | promise.then( |
michael@0 | 441 | count => { |
michael@0 | 442 | // Update the XPIDB schema version preference the first time we successfully |
michael@0 | 443 | // save the database. |
michael@0 | 444 | logger.debug("XPI Database saved, setting schema version preference to " + DB_SCHEMA); |
michael@0 | 445 | Services.prefs.setIntPref(PREF_DB_SCHEMA, DB_SCHEMA); |
michael@0 | 446 | // Reading the DB worked once, so we don't need the load error |
michael@0 | 447 | this._loadError = null; |
michael@0 | 448 | }, |
michael@0 | 449 | error => { |
michael@0 | 450 | // Need to try setting the schema version again later |
michael@0 | 451 | this._schemaVersionSet = false; |
michael@0 | 452 | logger.warn("Failed to save XPI database", error); |
michael@0 | 453 | // this._deferredSave.lastError has the most recent error so we don't |
michael@0 | 454 | // need this any more |
michael@0 | 455 | this._loadError = null; |
michael@0 | 456 | }); |
michael@0 | 457 | } |
michael@0 | 458 | }, |
michael@0 | 459 | |
michael@0 | 460 | flush: function() { |
michael@0 | 461 | // handle the "in memory only" and "saveChanges never called" cases |
michael@0 | 462 | if (!this._deferredSave) { |
michael@0 | 463 | return Promise.resolve(0); |
michael@0 | 464 | } |
michael@0 | 465 | |
michael@0 | 466 | return this._deferredSave.flush(); |
michael@0 | 467 | }, |
michael@0 | 468 | |
michael@0 | 469 | /** |
michael@0 | 470 | * Converts the current internal state of the XPI addon database to |
michael@0 | 471 | * a JSON.stringify()-ready structure |
michael@0 | 472 | */ |
michael@0 | 473 | toJSON: function() { |
michael@0 | 474 | if (!this.addonDB) { |
michael@0 | 475 | // We never loaded the database? |
michael@0 | 476 | throw new Error("Attempt to save database without loading it first"); |
michael@0 | 477 | } |
michael@0 | 478 | |
michael@0 | 479 | let toSave = { |
michael@0 | 480 | schemaVersion: DB_SCHEMA, |
michael@0 | 481 | addons: [...this.addonDB.values()] |
michael@0 | 482 | }; |
michael@0 | 483 | return toSave; |
michael@0 | 484 | }, |
michael@0 | 485 | |
michael@0 | 486 | /** |
michael@0 | 487 | * Pull upgrade information from an existing SQLITE database |
michael@0 | 488 | * |
michael@0 | 489 | * @return false if there is no SQLITE database |
michael@0 | 490 | * true and sets this.migrateData to null if the SQLITE DB exists |
michael@0 | 491 | * but does not contain useful information |
michael@0 | 492 | * true and sets this.migrateData to |
michael@0 | 493 | * {location: {id1:{addon1}, id2:{addon2}}, location2:{...}, ...} |
michael@0 | 494 | * if there is useful information |
michael@0 | 495 | */ |
michael@0 | 496 | getMigrateDataFromSQLITE: function XPIDB_getMigrateDataFromSQLITE() { |
michael@0 | 497 | let connection = null; |
michael@0 | 498 | let dbfile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_DATABASE], true); |
michael@0 | 499 | // Attempt to open the database |
michael@0 | 500 | try { |
michael@0 | 501 | connection = Services.storage.openUnsharedDatabase(dbfile); |
michael@0 | 502 | } |
michael@0 | 503 | catch (e) { |
michael@0 | 504 | logger.warn("Failed to open sqlite database " + dbfile.path + " for upgrade", e); |
michael@0 | 505 | return null; |
michael@0 | 506 | } |
michael@0 | 507 | logger.debug("Migrating data from sqlite"); |
michael@0 | 508 | let migrateData = this.getMigrateDataFromDatabase(connection); |
michael@0 | 509 | connection.close(); |
michael@0 | 510 | return migrateData; |
michael@0 | 511 | }, |
michael@0 | 512 | |
michael@0 | 513 | /** |
michael@0 | 514 | * Synchronously opens and reads the database file, upgrading from old |
michael@0 | 515 | * databases or making a new DB if needed. |
michael@0 | 516 | * |
michael@0 | 517 | * The possibilities, in order of priority, are: |
michael@0 | 518 | * 1) Perfectly good, up to date database |
michael@0 | 519 | * 2) Out of date JSON database needs to be upgraded => upgrade |
michael@0 | 520 | * 3) JSON database exists but is mangled somehow => build new JSON |
michael@0 | 521 | * 4) no JSON DB, but a useable SQLITE db we can upgrade from => upgrade |
michael@0 | 522 | * 5) useless SQLITE DB => build new JSON |
michael@0 | 523 | * 6) useable RDF DB => upgrade |
michael@0 | 524 | * 7) useless RDF DB => build new JSON |
michael@0 | 525 | * 8) Nothing at all => build new JSON |
michael@0 | 526 | * @param aRebuildOnError |
michael@0 | 527 | * A boolean indicating whether add-on information should be loaded |
michael@0 | 528 | * from the install locations if the database needs to be rebuilt. |
michael@0 | 529 | * (if false, caller is XPIProvider.checkForChanges() which will rebuild) |
michael@0 | 530 | */ |
michael@0 | 531 | syncLoadDB: function XPIDB_syncLoadDB(aRebuildOnError) { |
michael@0 | 532 | this.migrateData = null; |
michael@0 | 533 | let fstream = null; |
michael@0 | 534 | let data = ""; |
michael@0 | 535 | try { |
michael@0 | 536 | let readTimer = AddonManagerPrivate.simpleTimer("XPIDB_syncRead_MS"); |
michael@0 | 537 | logger.debug("Opening XPI database " + this.jsonFile.path); |
michael@0 | 538 | fstream = Components.classes["@mozilla.org/network/file-input-stream;1"]. |
michael@0 | 539 | createInstance(Components.interfaces.nsIFileInputStream); |
michael@0 | 540 | fstream.init(this.jsonFile, -1, 0, 0); |
michael@0 | 541 | let cstream = null; |
michael@0 | 542 | try { |
michael@0 | 543 | cstream = Components.classes["@mozilla.org/intl/converter-input-stream;1"]. |
michael@0 | 544 | createInstance(Components.interfaces.nsIConverterInputStream); |
michael@0 | 545 | cstream.init(fstream, "UTF-8", 0, 0); |
michael@0 | 546 | let (str = {}) { |
michael@0 | 547 | let read = 0; |
michael@0 | 548 | do { |
michael@0 | 549 | read = cstream.readString(0xffffffff, str); // read as much as we can and put it in str.value |
michael@0 | 550 | data += str.value; |
michael@0 | 551 | } while (read != 0); |
michael@0 | 552 | } |
michael@0 | 553 | readTimer.done(); |
michael@0 | 554 | this.parseDB(data, aRebuildOnError); |
michael@0 | 555 | } |
michael@0 | 556 | catch(e) { |
michael@0 | 557 | logger.error("Failed to load XPI JSON data from profile", e); |
michael@0 | 558 | let rebuildTimer = AddonManagerPrivate.simpleTimer("XPIDB_rebuildReadFailed_MS"); |
michael@0 | 559 | this.rebuildDatabase(aRebuildOnError); |
michael@0 | 560 | rebuildTimer.done(); |
michael@0 | 561 | } |
michael@0 | 562 | finally { |
michael@0 | 563 | if (cstream) |
michael@0 | 564 | cstream.close(); |
michael@0 | 565 | } |
michael@0 | 566 | } |
michael@0 | 567 | catch (e) { |
michael@0 | 568 | if (e.result === Cr.NS_ERROR_FILE_NOT_FOUND) { |
michael@0 | 569 | this.upgradeDB(aRebuildOnError); |
michael@0 | 570 | } |
michael@0 | 571 | else { |
michael@0 | 572 | this.rebuildUnreadableDB(e, aRebuildOnError); |
michael@0 | 573 | } |
michael@0 | 574 | } |
michael@0 | 575 | finally { |
michael@0 | 576 | if (fstream) |
michael@0 | 577 | fstream.close(); |
michael@0 | 578 | } |
michael@0 | 579 | // If an async load was also in progress, resolve that promise with our DB; |
michael@0 | 580 | // otherwise create a resolved promise |
michael@0 | 581 | if (this._dbPromise) { |
michael@0 | 582 | AddonManagerPrivate.recordSimpleMeasure("XPIDB_overlapped_load", 1); |
michael@0 | 583 | this._dbPromise.resolve(this.addonDB); |
michael@0 | 584 | } |
michael@0 | 585 | else |
michael@0 | 586 | this._dbPromise = Promise.resolve(this.addonDB); |
michael@0 | 587 | }, |
michael@0 | 588 | |
michael@0 | 589 | /** |
michael@0 | 590 | * Parse loaded data, reconstructing the database if the loaded data is not valid |
michael@0 | 591 | * @param aRebuildOnError |
michael@0 | 592 | * If true, synchronously reconstruct the database from installed add-ons |
michael@0 | 593 | */ |
michael@0 | 594 | parseDB: function(aData, aRebuildOnError) { |
michael@0 | 595 | let parseTimer = AddonManagerPrivate.simpleTimer("XPIDB_parseDB_MS"); |
michael@0 | 596 | try { |
michael@0 | 597 | // dump("Loaded JSON:\n" + aData + "\n"); |
michael@0 | 598 | let inputAddons = JSON.parse(aData); |
michael@0 | 599 | // Now do some sanity checks on our JSON db |
michael@0 | 600 | if (!("schemaVersion" in inputAddons) || !("addons" in inputAddons)) { |
michael@0 | 601 | parseTimer.done(); |
michael@0 | 602 | // Content of JSON file is bad, need to rebuild from scratch |
michael@0 | 603 | logger.error("bad JSON file contents"); |
michael@0 | 604 | AddonManagerPrivate.recordSimpleMeasure("XPIDB_startupError", "badJSON"); |
michael@0 | 605 | let rebuildTimer = AddonManagerPrivate.simpleTimer("XPIDB_rebuildBadJSON_MS"); |
michael@0 | 606 | this.rebuildDatabase(aRebuildOnError); |
michael@0 | 607 | rebuildTimer.done(); |
michael@0 | 608 | return; |
michael@0 | 609 | } |
michael@0 | 610 | if (inputAddons.schemaVersion != DB_SCHEMA) { |
michael@0 | 611 | // Handle mismatched JSON schema version. For now, we assume |
michael@0 | 612 | // compatibility for JSON data, though we throw away any fields we |
michael@0 | 613 | // don't know about (bug 902956) |
michael@0 | 614 | AddonManagerPrivate.recordSimpleMeasure("XPIDB_startupError", |
michael@0 | 615 | "schemaMismatch-" + inputAddons.schemaVersion); |
michael@0 | 616 | logger.debug("JSON schema mismatch: expected " + DB_SCHEMA + |
michael@0 | 617 | ", actual " + inputAddons.schemaVersion); |
michael@0 | 618 | // When we rev the schema of the JSON database, we need to make sure we |
michael@0 | 619 | // force the DB to save so that the DB_SCHEMA value in the JSON file and |
michael@0 | 620 | // the preference are updated. |
michael@0 | 621 | } |
michael@0 | 622 | // If we got here, we probably have good data |
michael@0 | 623 | // Make AddonInternal instances from the loaded data and save them |
michael@0 | 624 | let addonDB = new Map(); |
michael@0 | 625 | for (let loadedAddon of inputAddons.addons) { |
michael@0 | 626 | let newAddon = new DBAddonInternal(loadedAddon); |
michael@0 | 627 | addonDB.set(newAddon._key, newAddon); |
michael@0 | 628 | }; |
michael@0 | 629 | parseTimer.done(); |
michael@0 | 630 | this.addonDB = addonDB; |
michael@0 | 631 | logger.debug("Successfully read XPI database"); |
michael@0 | 632 | this.initialized = true; |
michael@0 | 633 | } |
michael@0 | 634 | catch(e) { |
michael@0 | 635 | // If we catch and log a SyntaxError from the JSON |
michael@0 | 636 | // parser, the xpcshell test harness fails the test for us: bug 870828 |
michael@0 | 637 | parseTimer.done(); |
michael@0 | 638 | if (e.name == "SyntaxError") { |
michael@0 | 639 | logger.error("Syntax error parsing saved XPI JSON data"); |
michael@0 | 640 | AddonManagerPrivate.recordSimpleMeasure("XPIDB_startupError", "syntax"); |
michael@0 | 641 | } |
michael@0 | 642 | else { |
michael@0 | 643 | logger.error("Failed to load XPI JSON data from profile", e); |
michael@0 | 644 | AddonManagerPrivate.recordSimpleMeasure("XPIDB_startupError", "other"); |
michael@0 | 645 | } |
michael@0 | 646 | let rebuildTimer = AddonManagerPrivate.simpleTimer("XPIDB_rebuildReadFailed_MS"); |
michael@0 | 647 | this.rebuildDatabase(aRebuildOnError); |
michael@0 | 648 | rebuildTimer.done(); |
michael@0 | 649 | } |
michael@0 | 650 | }, |
michael@0 | 651 | |
michael@0 | 652 | /** |
michael@0 | 653 | * Upgrade database from earlier (sqlite or RDF) version if available |
michael@0 | 654 | */ |
michael@0 | 655 | upgradeDB: function(aRebuildOnError) { |
michael@0 | 656 | let upgradeTimer = AddonManagerPrivate.simpleTimer("XPIDB_upgradeDB_MS"); |
michael@0 | 657 | try { |
michael@0 | 658 | let schemaVersion = Services.prefs.getIntPref(PREF_DB_SCHEMA); |
michael@0 | 659 | if (schemaVersion <= LAST_SQLITE_DB_SCHEMA) { |
michael@0 | 660 | // we should have an older SQLITE database |
michael@0 | 661 | logger.debug("Attempting to upgrade from SQLITE database"); |
michael@0 | 662 | this.migrateData = this.getMigrateDataFromSQLITE(); |
michael@0 | 663 | } |
michael@0 | 664 | else { |
michael@0 | 665 | // we've upgraded before but the JSON file is gone, fall through |
michael@0 | 666 | // and rebuild from scratch |
michael@0 | 667 | AddonManagerPrivate.recordSimpleMeasure("XPIDB_startupError", "dbMissing"); |
michael@0 | 668 | } |
michael@0 | 669 | } |
michael@0 | 670 | catch(e) { |
michael@0 | 671 | // No schema version pref means either a really old upgrade (RDF) or |
michael@0 | 672 | // a new profile |
michael@0 | 673 | this.migrateData = this.getMigrateDataFromRDF(); |
michael@0 | 674 | } |
michael@0 | 675 | |
michael@0 | 676 | this.rebuildDatabase(aRebuildOnError); |
michael@0 | 677 | upgradeTimer.done(); |
michael@0 | 678 | }, |
michael@0 | 679 | |
michael@0 | 680 | /** |
michael@0 | 681 | * Reconstruct when the DB file exists but is unreadable |
michael@0 | 682 | * (for example because read permission is denied) |
michael@0 | 683 | */ |
michael@0 | 684 | rebuildUnreadableDB: function(aError, aRebuildOnError) { |
michael@0 | 685 | let rebuildTimer = AddonManagerPrivate.simpleTimer("XPIDB_rebuildUnreadableDB_MS"); |
michael@0 | 686 | logger.warn("Extensions database " + this.jsonFile.path + |
michael@0 | 687 | " exists but is not readable; rebuilding", aError); |
michael@0 | 688 | // Remember the error message until we try and write at least once, so |
michael@0 | 689 | // we know at shutdown time that there was a problem |
michael@0 | 690 | this._loadError = aError; |
michael@0 | 691 | AddonManagerPrivate.recordSimpleMeasure("XPIDB_startupError", "unreadable"); |
michael@0 | 692 | this.rebuildDatabase(aRebuildOnError); |
michael@0 | 693 | rebuildTimer.done(); |
michael@0 | 694 | }, |
michael@0 | 695 | |
michael@0 | 696 | /** |
michael@0 | 697 | * Open and read the XPI database asynchronously, upgrading if |
michael@0 | 698 | * necessary. If any DB load operation fails, we need to |
michael@0 | 699 | * synchronously rebuild the DB from the installed extensions. |
michael@0 | 700 | * |
michael@0 | 701 | * @return Promise<Map> resolves to the Map of loaded JSON data stored |
michael@0 | 702 | * in this.addonDB; never rejects. |
michael@0 | 703 | */ |
michael@0 | 704 | asyncLoadDB: function XPIDB_asyncLoadDB() { |
michael@0 | 705 | // Already started (and possibly finished) loading |
michael@0 | 706 | if (this._dbPromise) { |
michael@0 | 707 | return this._dbPromise; |
michael@0 | 708 | } |
michael@0 | 709 | |
michael@0 | 710 | logger.debug("Starting async load of XPI database " + this.jsonFile.path); |
michael@0 | 711 | AddonManagerPrivate.recordSimpleMeasure("XPIDB_async_load", XPIProvider.runPhase); |
michael@0 | 712 | let readOptions = { |
michael@0 | 713 | outExecutionDuration: 0 |
michael@0 | 714 | }; |
michael@0 | 715 | return this._dbPromise = OS.File.read(this.jsonFile.path, null, readOptions).then( |
michael@0 | 716 | byteArray => { |
michael@0 | 717 | logger.debug("Async JSON file read took " + readOptions.outExecutionDuration + " MS"); |
michael@0 | 718 | AddonManagerPrivate.recordSimpleMeasure("XPIDB_asyncRead_MS", |
michael@0 | 719 | readOptions.outExecutionDuration); |
michael@0 | 720 | if (this._addonDB) { |
michael@0 | 721 | logger.debug("Synchronous load completed while waiting for async load"); |
michael@0 | 722 | return this.addonDB; |
michael@0 | 723 | } |
michael@0 | 724 | logger.debug("Finished async read of XPI database, parsing..."); |
michael@0 | 725 | let decodeTimer = AddonManagerPrivate.simpleTimer("XPIDB_decode_MS"); |
michael@0 | 726 | let decoder = new TextDecoder(); |
michael@0 | 727 | let data = decoder.decode(byteArray); |
michael@0 | 728 | decodeTimer.done(); |
michael@0 | 729 | this.parseDB(data, true); |
michael@0 | 730 | return this.addonDB; |
michael@0 | 731 | }) |
michael@0 | 732 | .then(null, |
michael@0 | 733 | error => { |
michael@0 | 734 | if (this._addonDB) { |
michael@0 | 735 | logger.debug("Synchronous load completed while waiting for async load"); |
michael@0 | 736 | return this.addonDB; |
michael@0 | 737 | } |
michael@0 | 738 | if (error.becauseNoSuchFile) { |
michael@0 | 739 | this.upgradeDB(true); |
michael@0 | 740 | } |
michael@0 | 741 | else { |
michael@0 | 742 | // it's there but unreadable |
michael@0 | 743 | this.rebuildUnreadableDB(error, true); |
michael@0 | 744 | } |
michael@0 | 745 | return this.addonDB; |
michael@0 | 746 | }); |
michael@0 | 747 | }, |
michael@0 | 748 | |
michael@0 | 749 | /** |
michael@0 | 750 | * Rebuild the database from addon install directories. If this.migrateData |
michael@0 | 751 | * is available, uses migrated information for settings on the addons found |
michael@0 | 752 | * during rebuild |
michael@0 | 753 | * @param aRebuildOnError |
michael@0 | 754 | * A boolean indicating whether add-on information should be loaded |
michael@0 | 755 | * from the install locations if the database needs to be rebuilt. |
michael@0 | 756 | * (if false, caller is XPIProvider.checkForChanges() which will rebuild) |
michael@0 | 757 | */ |
michael@0 | 758 | rebuildDatabase: function XIPDB_rebuildDatabase(aRebuildOnError) { |
michael@0 | 759 | this.addonDB = new Map(); |
michael@0 | 760 | this.initialized = true; |
michael@0 | 761 | |
michael@0 | 762 | if (XPIProvider.installStates && XPIProvider.installStates.length == 0) { |
michael@0 | 763 | // No extensions installed, so we're done |
michael@0 | 764 | logger.debug("Rebuilding XPI database with no extensions"); |
michael@0 | 765 | return; |
michael@0 | 766 | } |
michael@0 | 767 | |
michael@0 | 768 | // If there is no migration data then load the list of add-on directories |
michael@0 | 769 | // that were active during the last run |
michael@0 | 770 | if (!this.migrateData) |
michael@0 | 771 | this.activeBundles = this.getActiveBundles(); |
michael@0 | 772 | |
michael@0 | 773 | if (aRebuildOnError) { |
michael@0 | 774 | logger.warn("Rebuilding add-ons database from installed extensions."); |
michael@0 | 775 | try { |
michael@0 | 776 | XPIProvider.processFileChanges(XPIProvider.installStates, {}, false); |
michael@0 | 777 | } |
michael@0 | 778 | catch (e) { |
michael@0 | 779 | logger.error("Failed to rebuild XPI database from installed extensions", e); |
michael@0 | 780 | } |
michael@0 | 781 | // Make sure to update the active add-ons and add-ons list on shutdown |
michael@0 | 782 | Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true); |
michael@0 | 783 | } |
michael@0 | 784 | }, |
michael@0 | 785 | |
michael@0 | 786 | /** |
michael@0 | 787 | * Gets the list of file descriptors of active extension directories or XPI |
michael@0 | 788 | * files from the add-ons list. This must be loaded from disk since the |
michael@0 | 789 | * directory service gives no easy way to get both directly. This list doesn't |
michael@0 | 790 | * include themes as preferences already say which theme is currently active |
michael@0 | 791 | * |
michael@0 | 792 | * @return an array of persistent descriptors for the directories |
michael@0 | 793 | */ |
michael@0 | 794 | getActiveBundles: function XPIDB_getActiveBundles() { |
michael@0 | 795 | let bundles = []; |
michael@0 | 796 | |
michael@0 | 797 | // non-bootstrapped extensions |
michael@0 | 798 | let addonsList = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST], |
michael@0 | 799 | true); |
michael@0 | 800 | |
michael@0 | 801 | if (!addonsList.exists()) |
michael@0 | 802 | // XXX Irving believes this is broken in the case where there is no |
michael@0 | 803 | // extensions.ini but there are bootstrap extensions (e.g. Android) |
michael@0 | 804 | return null; |
michael@0 | 805 | |
michael@0 | 806 | try { |
michael@0 | 807 | let iniFactory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"] |
michael@0 | 808 | .getService(Ci.nsIINIParserFactory); |
michael@0 | 809 | let parser = iniFactory.createINIParser(addonsList); |
michael@0 | 810 | let keys = parser.getKeys("ExtensionDirs"); |
michael@0 | 811 | |
michael@0 | 812 | while (keys.hasMore()) |
michael@0 | 813 | bundles.push(parser.getString("ExtensionDirs", keys.getNext())); |
michael@0 | 814 | } |
michael@0 | 815 | catch (e) { |
michael@0 | 816 | logger.warn("Failed to parse extensions.ini", e); |
michael@0 | 817 | return null; |
michael@0 | 818 | } |
michael@0 | 819 | |
michael@0 | 820 | // Also include the list of active bootstrapped extensions |
michael@0 | 821 | for (let id in XPIProvider.bootstrappedAddons) |
michael@0 | 822 | bundles.push(XPIProvider.bootstrappedAddons[id].descriptor); |
michael@0 | 823 | |
michael@0 | 824 | return bundles; |
michael@0 | 825 | }, |
michael@0 | 826 | |
michael@0 | 827 | /** |
michael@0 | 828 | * Retrieves migration data from the old extensions.rdf database. |
michael@0 | 829 | * |
michael@0 | 830 | * @return an object holding information about what add-ons were previously |
michael@0 | 831 | * userDisabled and any updated compatibility information |
michael@0 | 832 | */ |
michael@0 | 833 | getMigrateDataFromRDF: function XPIDB_getMigrateDataFromRDF(aDbWasMissing) { |
michael@0 | 834 | |
michael@0 | 835 | // Migrate data from extensions.rdf |
michael@0 | 836 | let rdffile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_OLD_DATABASE], true); |
michael@0 | 837 | if (!rdffile.exists()) |
michael@0 | 838 | return null; |
michael@0 | 839 | |
michael@0 | 840 | logger.debug("Migrating data from " + FILE_OLD_DATABASE); |
michael@0 | 841 | let migrateData = {}; |
michael@0 | 842 | |
michael@0 | 843 | try { |
michael@0 | 844 | let ds = gRDF.GetDataSourceBlocking(Services.io.newFileURI(rdffile).spec); |
michael@0 | 845 | let root = Cc["@mozilla.org/rdf/container;1"]. |
michael@0 | 846 | createInstance(Ci.nsIRDFContainer); |
michael@0 | 847 | root.Init(ds, gRDF.GetResource(RDFURI_ITEM_ROOT)); |
michael@0 | 848 | let elements = root.GetElements(); |
michael@0 | 849 | |
michael@0 | 850 | while (elements.hasMoreElements()) { |
michael@0 | 851 | let source = elements.getNext().QueryInterface(Ci.nsIRDFResource); |
michael@0 | 852 | |
michael@0 | 853 | let location = getRDFProperty(ds, source, "installLocation"); |
michael@0 | 854 | if (location) { |
michael@0 | 855 | if (!(location in migrateData)) |
michael@0 | 856 | migrateData[location] = {}; |
michael@0 | 857 | let id = source.ValueUTF8.substring(PREFIX_ITEM_URI.length); |
michael@0 | 858 | migrateData[location][id] = { |
michael@0 | 859 | version: getRDFProperty(ds, source, "version"), |
michael@0 | 860 | userDisabled: false, |
michael@0 | 861 | targetApplications: [] |
michael@0 | 862 | } |
michael@0 | 863 | |
michael@0 | 864 | let disabled = getRDFProperty(ds, source, "userDisabled"); |
michael@0 | 865 | if (disabled == "true" || disabled == "needs-disable") |
michael@0 | 866 | migrateData[location][id].userDisabled = true; |
michael@0 | 867 | |
michael@0 | 868 | let targetApps = ds.GetTargets(source, EM_R("targetApplication"), |
michael@0 | 869 | true); |
michael@0 | 870 | while (targetApps.hasMoreElements()) { |
michael@0 | 871 | let targetApp = targetApps.getNext() |
michael@0 | 872 | .QueryInterface(Ci.nsIRDFResource); |
michael@0 | 873 | let appInfo = { |
michael@0 | 874 | id: getRDFProperty(ds, targetApp, "id") |
michael@0 | 875 | }; |
michael@0 | 876 | |
michael@0 | 877 | let minVersion = getRDFProperty(ds, targetApp, "updatedMinVersion"); |
michael@0 | 878 | if (minVersion) { |
michael@0 | 879 | appInfo.minVersion = minVersion; |
michael@0 | 880 | appInfo.maxVersion = getRDFProperty(ds, targetApp, "updatedMaxVersion"); |
michael@0 | 881 | } |
michael@0 | 882 | else { |
michael@0 | 883 | appInfo.minVersion = getRDFProperty(ds, targetApp, "minVersion"); |
michael@0 | 884 | appInfo.maxVersion = getRDFProperty(ds, targetApp, "maxVersion"); |
michael@0 | 885 | } |
michael@0 | 886 | migrateData[location][id].targetApplications.push(appInfo); |
michael@0 | 887 | } |
michael@0 | 888 | } |
michael@0 | 889 | } |
michael@0 | 890 | } |
michael@0 | 891 | catch (e) { |
michael@0 | 892 | logger.warn("Error reading " + FILE_OLD_DATABASE, e); |
michael@0 | 893 | migrateData = null; |
michael@0 | 894 | } |
michael@0 | 895 | |
michael@0 | 896 | return migrateData; |
michael@0 | 897 | }, |
michael@0 | 898 | |
michael@0 | 899 | /** |
michael@0 | 900 | * Retrieves migration data from a database that has an older or newer schema. |
michael@0 | 901 | * |
michael@0 | 902 | * @return an object holding information about what add-ons were previously |
michael@0 | 903 | * userDisabled and any updated compatibility information |
michael@0 | 904 | */ |
michael@0 | 905 | getMigrateDataFromDatabase: function XPIDB_getMigrateDataFromDatabase(aConnection) { |
michael@0 | 906 | let migrateData = {}; |
michael@0 | 907 | |
michael@0 | 908 | // Attempt to migrate data from a different (even future!) version of the |
michael@0 | 909 | // database |
michael@0 | 910 | try { |
michael@0 | 911 | var stmt = aConnection.createStatement("PRAGMA table_info(addon)"); |
michael@0 | 912 | |
michael@0 | 913 | const REQUIRED = ["internal_id", "id", "location", "userDisabled", |
michael@0 | 914 | "installDate", "version"]; |
michael@0 | 915 | |
michael@0 | 916 | let reqCount = 0; |
michael@0 | 917 | let props = []; |
michael@0 | 918 | for (let row in resultRows(stmt)) { |
michael@0 | 919 | if (REQUIRED.indexOf(row.name) != -1) { |
michael@0 | 920 | reqCount++; |
michael@0 | 921 | props.push(row.name); |
michael@0 | 922 | } |
michael@0 | 923 | else if (DB_METADATA.indexOf(row.name) != -1) { |
michael@0 | 924 | props.push(row.name); |
michael@0 | 925 | } |
michael@0 | 926 | else if (DB_BOOL_METADATA.indexOf(row.name) != -1) { |
michael@0 | 927 | props.push(row.name); |
michael@0 | 928 | } |
michael@0 | 929 | } |
michael@0 | 930 | |
michael@0 | 931 | if (reqCount < REQUIRED.length) { |
michael@0 | 932 | logger.error("Unable to read anything useful from the database"); |
michael@0 | 933 | return null; |
michael@0 | 934 | } |
michael@0 | 935 | stmt.finalize(); |
michael@0 | 936 | |
michael@0 | 937 | stmt = aConnection.createStatement("SELECT " + props.join(",") + " FROM addon"); |
michael@0 | 938 | for (let row in resultRows(stmt)) { |
michael@0 | 939 | if (!(row.location in migrateData)) |
michael@0 | 940 | migrateData[row.location] = {}; |
michael@0 | 941 | let addonData = { |
michael@0 | 942 | targetApplications: [] |
michael@0 | 943 | } |
michael@0 | 944 | migrateData[row.location][row.id] = addonData; |
michael@0 | 945 | |
michael@0 | 946 | props.forEach(function(aProp) { |
michael@0 | 947 | if (aProp == "isForeignInstall") |
michael@0 | 948 | addonData.foreignInstall = (row[aProp] == 1); |
michael@0 | 949 | if (DB_BOOL_METADATA.indexOf(aProp) != -1) |
michael@0 | 950 | addonData[aProp] = row[aProp] == 1; |
michael@0 | 951 | else |
michael@0 | 952 | addonData[aProp] = row[aProp]; |
michael@0 | 953 | }) |
michael@0 | 954 | } |
michael@0 | 955 | |
michael@0 | 956 | var taStmt = aConnection.createStatement("SELECT id, minVersion, " + |
michael@0 | 957 | "maxVersion FROM " + |
michael@0 | 958 | "targetApplication WHERE " + |
michael@0 | 959 | "addon_internal_id=:internal_id"); |
michael@0 | 960 | |
michael@0 | 961 | for (let location in migrateData) { |
michael@0 | 962 | for (let id in migrateData[location]) { |
michael@0 | 963 | taStmt.params.internal_id = migrateData[location][id].internal_id; |
michael@0 | 964 | delete migrateData[location][id].internal_id; |
michael@0 | 965 | for (let row in resultRows(taStmt)) { |
michael@0 | 966 | migrateData[location][id].targetApplications.push({ |
michael@0 | 967 | id: row.id, |
michael@0 | 968 | minVersion: row.minVersion, |
michael@0 | 969 | maxVersion: row.maxVersion |
michael@0 | 970 | }); |
michael@0 | 971 | } |
michael@0 | 972 | } |
michael@0 | 973 | } |
michael@0 | 974 | } |
michael@0 | 975 | catch (e) { |
michael@0 | 976 | // An error here means the schema is too different to read |
michael@0 | 977 | logger.error("Error migrating data", e); |
michael@0 | 978 | return null; |
michael@0 | 979 | } |
michael@0 | 980 | finally { |
michael@0 | 981 | if (taStmt) |
michael@0 | 982 | taStmt.finalize(); |
michael@0 | 983 | if (stmt) |
michael@0 | 984 | stmt.finalize(); |
michael@0 | 985 | } |
michael@0 | 986 | |
michael@0 | 987 | return migrateData; |
michael@0 | 988 | }, |
michael@0 | 989 | |
michael@0 | 990 | /** |
michael@0 | 991 | * Shuts down the database connection and releases all cached objects. |
michael@0 | 992 | * Return: Promise{integer} resolves / rejects with the result of the DB |
michael@0 | 993 | * flush after the database is flushed and |
michael@0 | 994 | * all cleanup is done |
michael@0 | 995 | */ |
michael@0 | 996 | shutdown: function XPIDB_shutdown() { |
michael@0 | 997 | logger.debug("shutdown"); |
michael@0 | 998 | if (this.initialized) { |
michael@0 | 999 | // If our last database I/O had an error, try one last time to save. |
michael@0 | 1000 | if (this.lastError) |
michael@0 | 1001 | this.saveChanges(); |
michael@0 | 1002 | |
michael@0 | 1003 | this.initialized = false; |
michael@0 | 1004 | |
michael@0 | 1005 | if (this._deferredSave) { |
michael@0 | 1006 | AddonManagerPrivate.recordSimpleMeasure( |
michael@0 | 1007 | "XPIDB_saves_total", this._deferredSave.totalSaves); |
michael@0 | 1008 | AddonManagerPrivate.recordSimpleMeasure( |
michael@0 | 1009 | "XPIDB_saves_overlapped", this._deferredSave.overlappedSaves); |
michael@0 | 1010 | AddonManagerPrivate.recordSimpleMeasure( |
michael@0 | 1011 | "XPIDB_saves_late", this._deferredSave.dirty ? 1 : 0); |
michael@0 | 1012 | } |
michael@0 | 1013 | |
michael@0 | 1014 | // Return a promise that any pending writes of the DB are complete and we |
michael@0 | 1015 | // are finished cleaning up |
michael@0 | 1016 | let flushPromise = this.flush(); |
michael@0 | 1017 | flushPromise.then(null, error => { |
michael@0 | 1018 | logger.error("Flush of XPI database failed", error); |
michael@0 | 1019 | AddonManagerPrivate.recordSimpleMeasure("XPIDB_shutdownFlush_failed", 1); |
michael@0 | 1020 | // If our last attempt to read or write the DB failed, force a new |
michael@0 | 1021 | // extensions.ini to be written to disk on the next startup |
michael@0 | 1022 | Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true); |
michael@0 | 1023 | }) |
michael@0 | 1024 | .then(count => { |
michael@0 | 1025 | // Clear out the cached addons data loaded from JSON |
michael@0 | 1026 | delete this.addonDB; |
michael@0 | 1027 | delete this._dbPromise; |
michael@0 | 1028 | // same for the deferred save |
michael@0 | 1029 | delete this._deferredSave; |
michael@0 | 1030 | // re-enable the schema version setter |
michael@0 | 1031 | delete this._schemaVersionSet; |
michael@0 | 1032 | }); |
michael@0 | 1033 | return flushPromise; |
michael@0 | 1034 | } |
michael@0 | 1035 | return Promise.resolve(0); |
michael@0 | 1036 | }, |
michael@0 | 1037 | |
michael@0 | 1038 | /** |
michael@0 | 1039 | * Return a list of all install locations known about by the database. This |
michael@0 | 1040 | * is often a a subset of the total install locations when not all have |
michael@0 | 1041 | * installed add-ons, occasionally a superset when an install location no |
michael@0 | 1042 | * longer exists. Only called from XPIProvider.processFileChanges, when |
michael@0 | 1043 | * the database should already be loaded. |
michael@0 | 1044 | * |
michael@0 | 1045 | * @return a Set of names of install locations |
michael@0 | 1046 | */ |
michael@0 | 1047 | getInstallLocations: function XPIDB_getInstallLocations() { |
michael@0 | 1048 | let locations = new Set(); |
michael@0 | 1049 | if (!this.addonDB) |
michael@0 | 1050 | return locations; |
michael@0 | 1051 | |
michael@0 | 1052 | for (let [, addon] of this.addonDB) { |
michael@0 | 1053 | locations.add(addon.location); |
michael@0 | 1054 | } |
michael@0 | 1055 | return locations; |
michael@0 | 1056 | }, |
michael@0 | 1057 | |
michael@0 | 1058 | /** |
michael@0 | 1059 | * Asynchronously list all addons that match the filter function |
michael@0 | 1060 | * @param aFilter |
michael@0 | 1061 | * Function that takes an addon instance and returns |
michael@0 | 1062 | * true if that addon should be included in the selected array |
michael@0 | 1063 | * @param aCallback |
michael@0 | 1064 | * Called back with an array of addons matching aFilter |
michael@0 | 1065 | * or an empty array if none match |
michael@0 | 1066 | */ |
michael@0 | 1067 | getAddonList: function(aFilter, aCallback) { |
michael@0 | 1068 | this.asyncLoadDB().then( |
michael@0 | 1069 | addonDB => { |
michael@0 | 1070 | let addonList = _filterDB(addonDB, aFilter); |
michael@0 | 1071 | asyncMap(addonList, getRepositoryAddon, makeSafe(aCallback)); |
michael@0 | 1072 | }) |
michael@0 | 1073 | .then(null, |
michael@0 | 1074 | error => { |
michael@0 | 1075 | logger.error("getAddonList failed", error); |
michael@0 | 1076 | makeSafe(aCallback)([]); |
michael@0 | 1077 | }); |
michael@0 | 1078 | }, |
michael@0 | 1079 | |
michael@0 | 1080 | /** |
michael@0 | 1081 | * (Possibly asynchronously) get the first addon that matches the filter function |
michael@0 | 1082 | * @param aFilter |
michael@0 | 1083 | * Function that takes an addon instance and returns |
michael@0 | 1084 | * true if that addon should be selected |
michael@0 | 1085 | * @param aCallback |
michael@0 | 1086 | * Called back with the addon, or null if no matching addon is found |
michael@0 | 1087 | */ |
michael@0 | 1088 | getAddon: function(aFilter, aCallback) { |
michael@0 | 1089 | return this.asyncLoadDB().then( |
michael@0 | 1090 | addonDB => { |
michael@0 | 1091 | getRepositoryAddon(_findAddon(addonDB, aFilter), makeSafe(aCallback)); |
michael@0 | 1092 | }) |
michael@0 | 1093 | .then(null, |
michael@0 | 1094 | error => { |
michael@0 | 1095 | logger.error("getAddon failed", e); |
michael@0 | 1096 | makeSafe(aCallback)(null); |
michael@0 | 1097 | }); |
michael@0 | 1098 | }, |
michael@0 | 1099 | |
michael@0 | 1100 | /** |
michael@0 | 1101 | * Synchronously reads all the add-ons in a particular install location. |
michael@0 | 1102 | * Always called with the addon database already loaded. |
michael@0 | 1103 | * |
michael@0 | 1104 | * @param aLocation |
michael@0 | 1105 | * The name of the install location |
michael@0 | 1106 | * @return an array of DBAddonInternals |
michael@0 | 1107 | */ |
michael@0 | 1108 | getAddonsInLocation: function XPIDB_getAddonsInLocation(aLocation) { |
michael@0 | 1109 | return _filterDB(this.addonDB, aAddon => (aAddon.location == aLocation)); |
michael@0 | 1110 | }, |
michael@0 | 1111 | |
michael@0 | 1112 | /** |
michael@0 | 1113 | * Asynchronously gets an add-on with a particular ID in a particular |
michael@0 | 1114 | * install location. |
michael@0 | 1115 | * |
michael@0 | 1116 | * @param aId |
michael@0 | 1117 | * The ID of the add-on to retrieve |
michael@0 | 1118 | * @param aLocation |
michael@0 | 1119 | * The name of the install location |
michael@0 | 1120 | * @param aCallback |
michael@0 | 1121 | * A callback to pass the DBAddonInternal to |
michael@0 | 1122 | */ |
michael@0 | 1123 | getAddonInLocation: function XPIDB_getAddonInLocation(aId, aLocation, aCallback) { |
michael@0 | 1124 | this.asyncLoadDB().then( |
michael@0 | 1125 | addonDB => getRepositoryAddon(addonDB.get(aLocation + ":" + aId), |
michael@0 | 1126 | makeSafe(aCallback))); |
michael@0 | 1127 | }, |
michael@0 | 1128 | |
michael@0 | 1129 | /** |
michael@0 | 1130 | * Asynchronously gets the add-on with the specified ID that is visible. |
michael@0 | 1131 | * |
michael@0 | 1132 | * @param aId |
michael@0 | 1133 | * The ID of the add-on to retrieve |
michael@0 | 1134 | * @param aCallback |
michael@0 | 1135 | * A callback to pass the DBAddonInternal to |
michael@0 | 1136 | */ |
michael@0 | 1137 | getVisibleAddonForID: function XPIDB_getVisibleAddonForID(aId, aCallback) { |
michael@0 | 1138 | this.getAddon(aAddon => ((aAddon.id == aId) && aAddon.visible), |
michael@0 | 1139 | aCallback); |
michael@0 | 1140 | }, |
michael@0 | 1141 | |
michael@0 | 1142 | /** |
michael@0 | 1143 | * Asynchronously gets the visible add-ons, optionally restricting by type. |
michael@0 | 1144 | * |
michael@0 | 1145 | * @param aTypes |
michael@0 | 1146 | * An array of types to include or null to include all types |
michael@0 | 1147 | * @param aCallback |
michael@0 | 1148 | * A callback to pass the array of DBAddonInternals to |
michael@0 | 1149 | */ |
michael@0 | 1150 | getVisibleAddons: function XPIDB_getVisibleAddons(aTypes, aCallback) { |
michael@0 | 1151 | this.getAddonList(aAddon => (aAddon.visible && |
michael@0 | 1152 | (!aTypes || (aTypes.length == 0) || |
michael@0 | 1153 | (aTypes.indexOf(aAddon.type) > -1))), |
michael@0 | 1154 | aCallback); |
michael@0 | 1155 | }, |
michael@0 | 1156 | |
michael@0 | 1157 | /** |
michael@0 | 1158 | * Synchronously gets all add-ons of a particular type. |
michael@0 | 1159 | * |
michael@0 | 1160 | * @param aType |
michael@0 | 1161 | * The type of add-on to retrieve |
michael@0 | 1162 | * @return an array of DBAddonInternals |
michael@0 | 1163 | */ |
michael@0 | 1164 | getAddonsByType: function XPIDB_getAddonsByType(aType) { |
michael@0 | 1165 | if (!this.addonDB) { |
michael@0 | 1166 | // jank-tastic! Must synchronously load DB if the theme switches from |
michael@0 | 1167 | // an XPI theme to a lightweight theme before the DB has loaded, |
michael@0 | 1168 | // because we're called from sync XPIProvider.addonChanged |
michael@0 | 1169 | logger.warn("Synchronous load of XPI database due to getAddonsByType(" + aType + ")"); |
michael@0 | 1170 | AddonManagerPrivate.recordSimpleMeasure("XPIDB_lateOpen_byType", XPIProvider.runPhase); |
michael@0 | 1171 | this.syncLoadDB(true); |
michael@0 | 1172 | } |
michael@0 | 1173 | return _filterDB(this.addonDB, aAddon => (aAddon.type == aType)); |
michael@0 | 1174 | }, |
michael@0 | 1175 | |
michael@0 | 1176 | /** |
michael@0 | 1177 | * Synchronously gets an add-on with a particular internalName. |
michael@0 | 1178 | * |
michael@0 | 1179 | * @param aInternalName |
michael@0 | 1180 | * The internalName of the add-on to retrieve |
michael@0 | 1181 | * @return a DBAddonInternal |
michael@0 | 1182 | */ |
michael@0 | 1183 | getVisibleAddonForInternalName: function XPIDB_getVisibleAddonForInternalName(aInternalName) { |
michael@0 | 1184 | if (!this.addonDB) { |
michael@0 | 1185 | // This may be called when the DB hasn't otherwise been loaded |
michael@0 | 1186 | logger.warn("Synchronous load of XPI database due to getVisibleAddonForInternalName"); |
michael@0 | 1187 | AddonManagerPrivate.recordSimpleMeasure("XPIDB_lateOpen_forInternalName", |
michael@0 | 1188 | XPIProvider.runPhase); |
michael@0 | 1189 | this.syncLoadDB(true); |
michael@0 | 1190 | } |
michael@0 | 1191 | |
michael@0 | 1192 | return _findAddon(this.addonDB, |
michael@0 | 1193 | aAddon => aAddon.visible && |
michael@0 | 1194 | (aAddon.internalName == aInternalName)); |
michael@0 | 1195 | }, |
michael@0 | 1196 | |
michael@0 | 1197 | /** |
michael@0 | 1198 | * Asynchronously gets all add-ons with pending operations. |
michael@0 | 1199 | * |
michael@0 | 1200 | * @param aTypes |
michael@0 | 1201 | * The types of add-ons to retrieve or null to get all types |
michael@0 | 1202 | * @param aCallback |
michael@0 | 1203 | * A callback to pass the array of DBAddonInternal to |
michael@0 | 1204 | */ |
michael@0 | 1205 | getVisibleAddonsWithPendingOperations: |
michael@0 | 1206 | function XPIDB_getVisibleAddonsWithPendingOperations(aTypes, aCallback) { |
michael@0 | 1207 | |
michael@0 | 1208 | this.getAddonList( |
michael@0 | 1209 | aAddon => (aAddon.visible && |
michael@0 | 1210 | (aAddon.pendingUninstall || |
michael@0 | 1211 | // Logic here is tricky. If we're active but either |
michael@0 | 1212 | // disabled flag is set, we're pending disable; if we're not |
michael@0 | 1213 | // active and neither disabled flag is set, we're pending enable |
michael@0 | 1214 | (aAddon.active == (aAddon.userDisabled || aAddon.appDisabled))) && |
michael@0 | 1215 | (!aTypes || (aTypes.length == 0) || (aTypes.indexOf(aAddon.type) > -1))), |
michael@0 | 1216 | aCallback); |
michael@0 | 1217 | }, |
michael@0 | 1218 | |
michael@0 | 1219 | /** |
michael@0 | 1220 | * Asynchronously get an add-on by its Sync GUID. |
michael@0 | 1221 | * |
michael@0 | 1222 | * @param aGUID |
michael@0 | 1223 | * Sync GUID of add-on to fetch |
michael@0 | 1224 | * @param aCallback |
michael@0 | 1225 | * A callback to pass the DBAddonInternal record to. Receives null |
michael@0 | 1226 | * if no add-on with that GUID is found. |
michael@0 | 1227 | * |
michael@0 | 1228 | */ |
michael@0 | 1229 | getAddonBySyncGUID: function XPIDB_getAddonBySyncGUID(aGUID, aCallback) { |
michael@0 | 1230 | this.getAddon(aAddon => aAddon.syncGUID == aGUID, |
michael@0 | 1231 | aCallback); |
michael@0 | 1232 | }, |
michael@0 | 1233 | |
michael@0 | 1234 | /** |
michael@0 | 1235 | * Synchronously gets all add-ons in the database. |
michael@0 | 1236 | * This is only called from the preference observer for the default |
michael@0 | 1237 | * compatibility version preference, so we can return an empty list if |
michael@0 | 1238 | * we haven't loaded the database yet. |
michael@0 | 1239 | * |
michael@0 | 1240 | * @return an array of DBAddonInternals |
michael@0 | 1241 | */ |
michael@0 | 1242 | getAddons: function XPIDB_getAddons() { |
michael@0 | 1243 | if (!this.addonDB) { |
michael@0 | 1244 | return []; |
michael@0 | 1245 | } |
michael@0 | 1246 | return _filterDB(this.addonDB, aAddon => true); |
michael@0 | 1247 | }, |
michael@0 | 1248 | |
michael@0 | 1249 | /** |
michael@0 | 1250 | * Synchronously adds an AddonInternal's metadata to the database. |
michael@0 | 1251 | * |
michael@0 | 1252 | * @param aAddon |
michael@0 | 1253 | * AddonInternal to add |
michael@0 | 1254 | * @param aDescriptor |
michael@0 | 1255 | * The file descriptor of the add-on |
michael@0 | 1256 | * @return The DBAddonInternal that was added to the database |
michael@0 | 1257 | */ |
michael@0 | 1258 | addAddonMetadata: function XPIDB_addAddonMetadata(aAddon, aDescriptor) { |
michael@0 | 1259 | if (!this.addonDB) { |
michael@0 | 1260 | AddonManagerPrivate.recordSimpleMeasure("XPIDB_lateOpen_addMetadata", |
michael@0 | 1261 | XPIProvider.runPhase); |
michael@0 | 1262 | this.syncLoadDB(false); |
michael@0 | 1263 | } |
michael@0 | 1264 | |
michael@0 | 1265 | let newAddon = new DBAddonInternal(aAddon); |
michael@0 | 1266 | newAddon.descriptor = aDescriptor; |
michael@0 | 1267 | this.addonDB.set(newAddon._key, newAddon); |
michael@0 | 1268 | if (newAddon.visible) { |
michael@0 | 1269 | this.makeAddonVisible(newAddon); |
michael@0 | 1270 | } |
michael@0 | 1271 | |
michael@0 | 1272 | this.saveChanges(); |
michael@0 | 1273 | return newAddon; |
michael@0 | 1274 | }, |
michael@0 | 1275 | |
michael@0 | 1276 | /** |
michael@0 | 1277 | * Synchronously updates an add-on's metadata in the database. Currently just |
michael@0 | 1278 | * removes and recreates. |
michael@0 | 1279 | * |
michael@0 | 1280 | * @param aOldAddon |
michael@0 | 1281 | * The DBAddonInternal to be replaced |
michael@0 | 1282 | * @param aNewAddon |
michael@0 | 1283 | * The new AddonInternal to add |
michael@0 | 1284 | * @param aDescriptor |
michael@0 | 1285 | * The file descriptor of the add-on |
michael@0 | 1286 | * @return The DBAddonInternal that was added to the database |
michael@0 | 1287 | */ |
michael@0 | 1288 | updateAddonMetadata: function XPIDB_updateAddonMetadata(aOldAddon, aNewAddon, |
michael@0 | 1289 | aDescriptor) { |
michael@0 | 1290 | this.removeAddonMetadata(aOldAddon); |
michael@0 | 1291 | aNewAddon.syncGUID = aOldAddon.syncGUID; |
michael@0 | 1292 | aNewAddon.installDate = aOldAddon.installDate; |
michael@0 | 1293 | aNewAddon.applyBackgroundUpdates = aOldAddon.applyBackgroundUpdates; |
michael@0 | 1294 | aNewAddon.foreignInstall = aOldAddon.foreignInstall; |
michael@0 | 1295 | aNewAddon.active = (aNewAddon.visible && !aNewAddon.userDisabled && |
michael@0 | 1296 | !aNewAddon.appDisabled && !aNewAddon.pendingUninstall); |
michael@0 | 1297 | |
michael@0 | 1298 | // addAddonMetadata does a saveChanges() |
michael@0 | 1299 | return this.addAddonMetadata(aNewAddon, aDescriptor); |
michael@0 | 1300 | }, |
michael@0 | 1301 | |
michael@0 | 1302 | /** |
michael@0 | 1303 | * Synchronously removes an add-on from the database. |
michael@0 | 1304 | * |
michael@0 | 1305 | * @param aAddon |
michael@0 | 1306 | * The DBAddonInternal being removed |
michael@0 | 1307 | */ |
michael@0 | 1308 | removeAddonMetadata: function XPIDB_removeAddonMetadata(aAddon) { |
michael@0 | 1309 | this.addonDB.delete(aAddon._key); |
michael@0 | 1310 | this.saveChanges(); |
michael@0 | 1311 | }, |
michael@0 | 1312 | |
michael@0 | 1313 | /** |
michael@0 | 1314 | * Synchronously marks a DBAddonInternal as visible marking all other |
michael@0 | 1315 | * instances with the same ID as not visible. |
michael@0 | 1316 | * |
michael@0 | 1317 | * @param aAddon |
michael@0 | 1318 | * The DBAddonInternal to make visible |
michael@0 | 1319 | */ |
michael@0 | 1320 | makeAddonVisible: function XPIDB_makeAddonVisible(aAddon) { |
michael@0 | 1321 | logger.debug("Make addon " + aAddon._key + " visible"); |
michael@0 | 1322 | for (let [, otherAddon] of this.addonDB) { |
michael@0 | 1323 | if ((otherAddon.id == aAddon.id) && (otherAddon._key != aAddon._key)) { |
michael@0 | 1324 | logger.debug("Hide addon " + otherAddon._key); |
michael@0 | 1325 | otherAddon.visible = false; |
michael@0 | 1326 | } |
michael@0 | 1327 | } |
michael@0 | 1328 | aAddon.visible = true; |
michael@0 | 1329 | this.saveChanges(); |
michael@0 | 1330 | }, |
michael@0 | 1331 | |
michael@0 | 1332 | /** |
michael@0 | 1333 | * Synchronously sets properties for an add-on. |
michael@0 | 1334 | * |
michael@0 | 1335 | * @param aAddon |
michael@0 | 1336 | * The DBAddonInternal being updated |
michael@0 | 1337 | * @param aProperties |
michael@0 | 1338 | * A dictionary of properties to set |
michael@0 | 1339 | */ |
michael@0 | 1340 | setAddonProperties: function XPIDB_setAddonProperties(aAddon, aProperties) { |
michael@0 | 1341 | for (let key in aProperties) { |
michael@0 | 1342 | aAddon[key] = aProperties[key]; |
michael@0 | 1343 | } |
michael@0 | 1344 | this.saveChanges(); |
michael@0 | 1345 | }, |
michael@0 | 1346 | |
michael@0 | 1347 | /** |
michael@0 | 1348 | * Synchronously sets the Sync GUID for an add-on. |
michael@0 | 1349 | * Only called when the database is already loaded. |
michael@0 | 1350 | * |
michael@0 | 1351 | * @param aAddon |
michael@0 | 1352 | * The DBAddonInternal being updated |
michael@0 | 1353 | * @param aGUID |
michael@0 | 1354 | * GUID string to set the value to |
michael@0 | 1355 | * @throws if another addon already has the specified GUID |
michael@0 | 1356 | */ |
michael@0 | 1357 | setAddonSyncGUID: function XPIDB_setAddonSyncGUID(aAddon, aGUID) { |
michael@0 | 1358 | // Need to make sure no other addon has this GUID |
michael@0 | 1359 | function excludeSyncGUID(otherAddon) { |
michael@0 | 1360 | return (otherAddon._key != aAddon._key) && (otherAddon.syncGUID == aGUID); |
michael@0 | 1361 | } |
michael@0 | 1362 | let otherAddon = _findAddon(this.addonDB, excludeSyncGUID); |
michael@0 | 1363 | if (otherAddon) { |
michael@0 | 1364 | throw new Error("Addon sync GUID conflict for addon " + aAddon._key + |
michael@0 | 1365 | ": " + otherAddon._key + " already has GUID " + aGUID); |
michael@0 | 1366 | } |
michael@0 | 1367 | aAddon.syncGUID = aGUID; |
michael@0 | 1368 | this.saveChanges(); |
michael@0 | 1369 | }, |
michael@0 | 1370 | |
michael@0 | 1371 | /** |
michael@0 | 1372 | * Synchronously updates an add-on's active flag in the database. |
michael@0 | 1373 | * |
michael@0 | 1374 | * @param aAddon |
michael@0 | 1375 | * The DBAddonInternal to update |
michael@0 | 1376 | */ |
michael@0 | 1377 | updateAddonActive: function XPIDB_updateAddonActive(aAddon, aActive) { |
michael@0 | 1378 | logger.debug("Updating active state for add-on " + aAddon.id + " to " + aActive); |
michael@0 | 1379 | |
michael@0 | 1380 | aAddon.active = aActive; |
michael@0 | 1381 | this.saveChanges(); |
michael@0 | 1382 | }, |
michael@0 | 1383 | |
michael@0 | 1384 | /** |
michael@0 | 1385 | * Synchronously calculates and updates all the active flags in the database. |
michael@0 | 1386 | */ |
michael@0 | 1387 | updateActiveAddons: function XPIDB_updateActiveAddons() { |
michael@0 | 1388 | if (!this.addonDB) { |
michael@0 | 1389 | logger.warn("updateActiveAddons called when DB isn't loaded"); |
michael@0 | 1390 | // force the DB to load |
michael@0 | 1391 | AddonManagerPrivate.recordSimpleMeasure("XPIDB_lateOpen_updateActive", |
michael@0 | 1392 | XPIProvider.runPhase); |
michael@0 | 1393 | this.syncLoadDB(true); |
michael@0 | 1394 | } |
michael@0 | 1395 | logger.debug("Updating add-on states"); |
michael@0 | 1396 | for (let [, addon] of this.addonDB) { |
michael@0 | 1397 | let newActive = (addon.visible && !addon.userDisabled && |
michael@0 | 1398 | !addon.softDisabled && !addon.appDisabled && |
michael@0 | 1399 | !addon.pendingUninstall); |
michael@0 | 1400 | if (newActive != addon.active) { |
michael@0 | 1401 | addon.active = newActive; |
michael@0 | 1402 | this.saveChanges(); |
michael@0 | 1403 | } |
michael@0 | 1404 | } |
michael@0 | 1405 | }, |
michael@0 | 1406 | |
michael@0 | 1407 | /** |
michael@0 | 1408 | * Writes out the XPI add-ons list for the platform to read. |
michael@0 | 1409 | * @return true if the file was successfully updated, false otherwise |
michael@0 | 1410 | */ |
michael@0 | 1411 | writeAddonsList: function XPIDB_writeAddonsList() { |
michael@0 | 1412 | if (!this.addonDB) { |
michael@0 | 1413 | // force the DB to load |
michael@0 | 1414 | AddonManagerPrivate.recordSimpleMeasure("XPIDB_lateOpen_writeList", |
michael@0 | 1415 | XPIProvider.runPhase); |
michael@0 | 1416 | this.syncLoadDB(true); |
michael@0 | 1417 | } |
michael@0 | 1418 | Services.appinfo.invalidateCachesOnRestart(); |
michael@0 | 1419 | |
michael@0 | 1420 | let addonsList = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST], |
michael@0 | 1421 | true); |
michael@0 | 1422 | let enabledAddons = []; |
michael@0 | 1423 | let text = "[ExtensionDirs]\r\n"; |
michael@0 | 1424 | let count = 0; |
michael@0 | 1425 | let fullCount = 0; |
michael@0 | 1426 | |
michael@0 | 1427 | let activeAddons = _filterDB( |
michael@0 | 1428 | this.addonDB, |
michael@0 | 1429 | aAddon => aAddon.active && !aAddon.bootstrap && (aAddon.type != "theme")); |
michael@0 | 1430 | |
michael@0 | 1431 | for (let row of activeAddons) { |
michael@0 | 1432 | text += "Extension" + (count++) + "=" + row.descriptor + "\r\n"; |
michael@0 | 1433 | enabledAddons.push(encodeURIComponent(row.id) + ":" + |
michael@0 | 1434 | encodeURIComponent(row.version)); |
michael@0 | 1435 | } |
michael@0 | 1436 | fullCount += count; |
michael@0 | 1437 | |
michael@0 | 1438 | // The selected skin may come from an inactive theme (the default theme |
michael@0 | 1439 | // when a lightweight theme is applied for example) |
michael@0 | 1440 | text += "\r\n[ThemeDirs]\r\n"; |
michael@0 | 1441 | |
michael@0 | 1442 | let dssEnabled = false; |
michael@0 | 1443 | try { |
michael@0 | 1444 | dssEnabled = Services.prefs.getBoolPref(PREF_EM_DSS_ENABLED); |
michael@0 | 1445 | } catch (e) {} |
michael@0 | 1446 | |
michael@0 | 1447 | let themes = []; |
michael@0 | 1448 | if (dssEnabled) { |
michael@0 | 1449 | themes = _filterDB(this.addonDB, aAddon => aAddon.type == "theme"); |
michael@0 | 1450 | } |
michael@0 | 1451 | else { |
michael@0 | 1452 | let activeTheme = _findAddon( |
michael@0 | 1453 | this.addonDB, |
michael@0 | 1454 | aAddon => (aAddon.type == "theme") && |
michael@0 | 1455 | (aAddon.internalName == XPIProvider.selectedSkin)); |
michael@0 | 1456 | if (activeTheme) { |
michael@0 | 1457 | themes.push(activeTheme); |
michael@0 | 1458 | } |
michael@0 | 1459 | } |
michael@0 | 1460 | |
michael@0 | 1461 | if (themes.length > 0) { |
michael@0 | 1462 | count = 0; |
michael@0 | 1463 | for (let row of themes) { |
michael@0 | 1464 | text += "Extension" + (count++) + "=" + row.descriptor + "\r\n"; |
michael@0 | 1465 | enabledAddons.push(encodeURIComponent(row.id) + ":" + |
michael@0 | 1466 | encodeURIComponent(row.version)); |
michael@0 | 1467 | } |
michael@0 | 1468 | fullCount += count; |
michael@0 | 1469 | } |
michael@0 | 1470 | |
michael@0 | 1471 | if (fullCount > 0) { |
michael@0 | 1472 | logger.debug("Writing add-ons list"); |
michael@0 | 1473 | |
michael@0 | 1474 | try { |
michael@0 | 1475 | let addonsListTmp = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST + ".tmp"], |
michael@0 | 1476 | true); |
michael@0 | 1477 | var fos = FileUtils.openFileOutputStream(addonsListTmp); |
michael@0 | 1478 | fos.write(text, text.length); |
michael@0 | 1479 | fos.close(); |
michael@0 | 1480 | addonsListTmp.moveTo(addonsListTmp.parent, FILE_XPI_ADDONS_LIST); |
michael@0 | 1481 | |
michael@0 | 1482 | Services.prefs.setCharPref(PREF_EM_ENABLED_ADDONS, enabledAddons.join(",")); |
michael@0 | 1483 | } |
michael@0 | 1484 | catch (e) { |
michael@0 | 1485 | logger.error("Failed to write add-ons list to profile directory", e); |
michael@0 | 1486 | return false; |
michael@0 | 1487 | } |
michael@0 | 1488 | } |
michael@0 | 1489 | else { |
michael@0 | 1490 | if (addonsList.exists()) { |
michael@0 | 1491 | logger.debug("Deleting add-ons list"); |
michael@0 | 1492 | try { |
michael@0 | 1493 | addonsList.remove(false); |
michael@0 | 1494 | } |
michael@0 | 1495 | catch (e) { |
michael@0 | 1496 | logger.error("Failed to remove " + addonsList.path, e); |
michael@0 | 1497 | return false; |
michael@0 | 1498 | } |
michael@0 | 1499 | } |
michael@0 | 1500 | |
michael@0 | 1501 | Services.prefs.clearUserPref(PREF_EM_ENABLED_ADDONS); |
michael@0 | 1502 | } |
michael@0 | 1503 | return true; |
michael@0 | 1504 | } |
michael@0 | 1505 | }; |