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