toolkit/mozapps/extensions/internal/AddonRepository.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

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
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 "use strict";
michael@0 6
michael@0 7 const Cc = Components.classes;
michael@0 8 const Ci = Components.interfaces;
michael@0 9 const Cu = Components.utils;
michael@0 10 const Cr = Components.results;
michael@0 11
michael@0 12 Components.utils.import("resource://gre/modules/Services.jsm");
michael@0 13 Components.utils.import("resource://gre/modules/AddonManager.jsm");
michael@0 14 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 15
michael@0 16 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
michael@0 17 "resource://gre/modules/FileUtils.jsm");
michael@0 18 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
michael@0 19 "resource://gre/modules/NetUtil.jsm");
michael@0 20 XPCOMUtils.defineLazyModuleGetter(this, "OS",
michael@0 21 "resource://gre/modules/osfile.jsm");
michael@0 22 XPCOMUtils.defineLazyModuleGetter(this, "DeferredSave",
michael@0 23 "resource://gre/modules/DeferredSave.jsm");
michael@0 24 XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository_SQLiteMigrator",
michael@0 25 "resource://gre/modules/addons/AddonRepository_SQLiteMigrator.jsm");
michael@0 26 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
michael@0 27 "resource://gre/modules/Promise.jsm");
michael@0 28
michael@0 29 this.EXPORTED_SYMBOLS = [ "AddonRepository" ];
michael@0 30
michael@0 31 const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled";
michael@0 32 const PREF_GETADDONS_CACHE_TYPES = "extensions.getAddons.cache.types";
michael@0 33 const PREF_GETADDONS_CACHE_ID_ENABLED = "extensions.%ID%.getAddons.cache.enabled"
michael@0 34 const PREF_GETADDONS_BROWSEADDONS = "extensions.getAddons.browseAddons";
michael@0 35 const PREF_GETADDONS_BYIDS = "extensions.getAddons.get.url";
michael@0 36 const PREF_GETADDONS_BYIDS_PERFORMANCE = "extensions.getAddons.getWithPerformance.url";
michael@0 37 const PREF_GETADDONS_BROWSERECOMMENDED = "extensions.getAddons.recommended.browseURL";
michael@0 38 const PREF_GETADDONS_GETRECOMMENDED = "extensions.getAddons.recommended.url";
michael@0 39 const PREF_GETADDONS_BROWSESEARCHRESULTS = "extensions.getAddons.search.browseURL";
michael@0 40 const PREF_GETADDONS_GETSEARCHRESULTS = "extensions.getAddons.search.url";
michael@0 41 const PREF_GETADDONS_DB_SCHEMA = "extensions.getAddons.databaseSchema"
michael@0 42
michael@0 43 const XMLURI_PARSE_ERROR = "http://www.mozilla.org/newlayout/xml/parsererror.xml";
michael@0 44
michael@0 45 const API_VERSION = "1.5";
michael@0 46 const DEFAULT_CACHE_TYPES = "extension,theme,locale,dictionary";
michael@0 47
michael@0 48 const KEY_PROFILEDIR = "ProfD";
michael@0 49 const FILE_DATABASE = "addons.json";
michael@0 50 const DB_SCHEMA = 5;
michael@0 51 const DB_MIN_JSON_SCHEMA = 5;
michael@0 52 const DB_BATCH_TIMEOUT_MS = 50;
michael@0 53
michael@0 54 const BLANK_DB = function() {
michael@0 55 return {
michael@0 56 addons: new Map(),
michael@0 57 schema: DB_SCHEMA
michael@0 58 };
michael@0 59 }
michael@0 60
michael@0 61 const TOOLKIT_ID = "toolkit@mozilla.org";
michael@0 62
michael@0 63 Cu.import("resource://gre/modules/Log.jsm");
michael@0 64 const LOGGER_ID = "addons.repository";
michael@0 65
michael@0 66 // Create a new logger for use by the Addons Repository
michael@0 67 // (Requires AddonManager.jsm)
michael@0 68 let logger = Log.repository.getLogger(LOGGER_ID);
michael@0 69
michael@0 70 // A map between XML keys to AddonSearchResult keys for string values
michael@0 71 // that require no extra parsing from XML
michael@0 72 const STRING_KEY_MAP = {
michael@0 73 name: "name",
michael@0 74 version: "version",
michael@0 75 homepage: "homepageURL",
michael@0 76 support: "supportURL"
michael@0 77 };
michael@0 78
michael@0 79 // A map between XML keys to AddonSearchResult keys for string values
michael@0 80 // that require parsing from HTML
michael@0 81 const HTML_KEY_MAP = {
michael@0 82 summary: "description",
michael@0 83 description: "fullDescription",
michael@0 84 developer_comments: "developerComments",
michael@0 85 eula: "eula"
michael@0 86 };
michael@0 87
michael@0 88 // A map between XML keys to AddonSearchResult keys for integer values
michael@0 89 // that require no extra parsing from XML
michael@0 90 const INTEGER_KEY_MAP = {
michael@0 91 total_downloads: "totalDownloads",
michael@0 92 weekly_downloads: "weeklyDownloads",
michael@0 93 daily_users: "dailyUsers"
michael@0 94 };
michael@0 95
michael@0 96 // Wrap the XHR factory so that tests can override with a mock
michael@0 97 let XHRequest = Components.Constructor("@mozilla.org/xmlextras/xmlhttprequest;1",
michael@0 98 "nsIXMLHttpRequest");
michael@0 99
michael@0 100 function convertHTMLToPlainText(html) {
michael@0 101 if (!html)
michael@0 102 return html;
michael@0 103 var converter = Cc["@mozilla.org/widget/htmlformatconverter;1"].
michael@0 104 createInstance(Ci.nsIFormatConverter);
michael@0 105
michael@0 106 var input = Cc["@mozilla.org/supports-string;1"].
michael@0 107 createInstance(Ci.nsISupportsString);
michael@0 108 input.data = html.replace(/\n/g, "<br>");
michael@0 109
michael@0 110 var output = {};
michael@0 111 converter.convert("text/html", input, input.data.length, "text/unicode",
michael@0 112 output, {});
michael@0 113
michael@0 114 if (output.value instanceof Ci.nsISupportsString)
michael@0 115 return output.value.data.replace(/\r\n/g, "\n");
michael@0 116 return html;
michael@0 117 }
michael@0 118
michael@0 119 function getAddonsToCache(aIds, aCallback) {
michael@0 120 try {
michael@0 121 var types = Services.prefs.getCharPref(PREF_GETADDONS_CACHE_TYPES);
michael@0 122 }
michael@0 123 catch (e) { }
michael@0 124 if (!types)
michael@0 125 types = DEFAULT_CACHE_TYPES;
michael@0 126
michael@0 127 types = types.split(",");
michael@0 128
michael@0 129 AddonManager.getAddonsByIDs(aIds, function getAddonsToCache_getAddonsByIDs(aAddons) {
michael@0 130 let enabledIds = [];
michael@0 131 for (var i = 0; i < aIds.length; i++) {
michael@0 132 var preference = PREF_GETADDONS_CACHE_ID_ENABLED.replace("%ID%", aIds[i]);
michael@0 133 try {
michael@0 134 if (!Services.prefs.getBoolPref(preference))
michael@0 135 continue;
michael@0 136 } catch(e) {
michael@0 137 // If the preference doesn't exist caching is enabled by default
michael@0 138 }
michael@0 139
michael@0 140 // The add-ons manager may not know about this ID yet if it is a pending
michael@0 141 // install. In that case we'll just cache it regardless
michael@0 142 if (aAddons[i] && (types.indexOf(aAddons[i].type) == -1))
michael@0 143 continue;
michael@0 144
michael@0 145 enabledIds.push(aIds[i]);
michael@0 146 }
michael@0 147
michael@0 148 aCallback(enabledIds);
michael@0 149 });
michael@0 150 }
michael@0 151
michael@0 152 function AddonSearchResult(aId) {
michael@0 153 this.id = aId;
michael@0 154 this.icons = {};
michael@0 155 this._unsupportedProperties = {};
michael@0 156 }
michael@0 157
michael@0 158 AddonSearchResult.prototype = {
michael@0 159 /**
michael@0 160 * The ID of the add-on
michael@0 161 */
michael@0 162 id: null,
michael@0 163
michael@0 164 /**
michael@0 165 * The add-on type (e.g. "extension" or "theme")
michael@0 166 */
michael@0 167 type: null,
michael@0 168
michael@0 169 /**
michael@0 170 * The name of the add-on
michael@0 171 */
michael@0 172 name: null,
michael@0 173
michael@0 174 /**
michael@0 175 * The version of the add-on
michael@0 176 */
michael@0 177 version: null,
michael@0 178
michael@0 179 /**
michael@0 180 * The creator of the add-on
michael@0 181 */
michael@0 182 creator: null,
michael@0 183
michael@0 184 /**
michael@0 185 * The developers of the add-on
michael@0 186 */
michael@0 187 developers: null,
michael@0 188
michael@0 189 /**
michael@0 190 * A short description of the add-on
michael@0 191 */
michael@0 192 description: null,
michael@0 193
michael@0 194 /**
michael@0 195 * The full description of the add-on
michael@0 196 */
michael@0 197 fullDescription: null,
michael@0 198
michael@0 199 /**
michael@0 200 * The developer comments for the add-on. This includes any information
michael@0 201 * that may be helpful to end users that isn't necessarily applicable to
michael@0 202 * the add-on description (e.g. known major bugs)
michael@0 203 */
michael@0 204 developerComments: null,
michael@0 205
michael@0 206 /**
michael@0 207 * The end-user licensing agreement (EULA) of the add-on
michael@0 208 */
michael@0 209 eula: null,
michael@0 210
michael@0 211 /**
michael@0 212 * The url of the add-on's icon
michael@0 213 */
michael@0 214 get iconURL() {
michael@0 215 return this.icons && this.icons[32];
michael@0 216 },
michael@0 217
michael@0 218 /**
michael@0 219 * The URLs of the add-on's icons, as an object with icon size as key
michael@0 220 */
michael@0 221 icons: null,
michael@0 222
michael@0 223 /**
michael@0 224 * An array of screenshot urls for the add-on
michael@0 225 */
michael@0 226 screenshots: null,
michael@0 227
michael@0 228 /**
michael@0 229 * The homepage for the add-on
michael@0 230 */
michael@0 231 homepageURL: null,
michael@0 232
michael@0 233 /**
michael@0 234 * The homepage for the add-on
michael@0 235 */
michael@0 236 learnmoreURL: null,
michael@0 237
michael@0 238 /**
michael@0 239 * The support URL for the add-on
michael@0 240 */
michael@0 241 supportURL: null,
michael@0 242
michael@0 243 /**
michael@0 244 * The contribution url of the add-on
michael@0 245 */
michael@0 246 contributionURL: null,
michael@0 247
michael@0 248 /**
michael@0 249 * The suggested contribution amount
michael@0 250 */
michael@0 251 contributionAmount: null,
michael@0 252
michael@0 253 /**
michael@0 254 * The URL to visit in order to purchase the add-on
michael@0 255 */
michael@0 256 purchaseURL: null,
michael@0 257
michael@0 258 /**
michael@0 259 * The numerical cost of the add-on in some currency, for sorting purposes
michael@0 260 * only
michael@0 261 */
michael@0 262 purchaseAmount: null,
michael@0 263
michael@0 264 /**
michael@0 265 * The display cost of the add-on, for display purposes only
michael@0 266 */
michael@0 267 purchaseDisplayAmount: null,
michael@0 268
michael@0 269 /**
michael@0 270 * The rating of the add-on, 0-5
michael@0 271 */
michael@0 272 averageRating: null,
michael@0 273
michael@0 274 /**
michael@0 275 * The number of reviews for this add-on
michael@0 276 */
michael@0 277 reviewCount: null,
michael@0 278
michael@0 279 /**
michael@0 280 * The URL to the list of reviews for this add-on
michael@0 281 */
michael@0 282 reviewURL: null,
michael@0 283
michael@0 284 /**
michael@0 285 * The total number of times the add-on was downloaded
michael@0 286 */
michael@0 287 totalDownloads: null,
michael@0 288
michael@0 289 /**
michael@0 290 * The number of times the add-on was downloaded the current week
michael@0 291 */
michael@0 292 weeklyDownloads: null,
michael@0 293
michael@0 294 /**
michael@0 295 * The number of daily users for the add-on
michael@0 296 */
michael@0 297 dailyUsers: null,
michael@0 298
michael@0 299 /**
michael@0 300 * AddonInstall object generated from the add-on XPI url
michael@0 301 */
michael@0 302 install: null,
michael@0 303
michael@0 304 /**
michael@0 305 * nsIURI storing where this add-on was installed from
michael@0 306 */
michael@0 307 sourceURI: null,
michael@0 308
michael@0 309 /**
michael@0 310 * The status of the add-on in the repository (e.g. 4 = "Public")
michael@0 311 */
michael@0 312 repositoryStatus: null,
michael@0 313
michael@0 314 /**
michael@0 315 * The size of the add-on's files in bytes. For an add-on that have not yet
michael@0 316 * been downloaded this may be an estimated value.
michael@0 317 */
michael@0 318 size: null,
michael@0 319
michael@0 320 /**
michael@0 321 * The Date that the add-on was most recently updated
michael@0 322 */
michael@0 323 updateDate: null,
michael@0 324
michael@0 325 /**
michael@0 326 * True or false depending on whether the add-on is compatible with the
michael@0 327 * current version of the application
michael@0 328 */
michael@0 329 isCompatible: true,
michael@0 330
michael@0 331 /**
michael@0 332 * True or false depending on whether the add-on is compatible with the
michael@0 333 * current platform
michael@0 334 */
michael@0 335 isPlatformCompatible: true,
michael@0 336
michael@0 337 /**
michael@0 338 * Array of AddonCompatibilityOverride objects, that describe overrides for
michael@0 339 * compatibility with an application versions.
michael@0 340 **/
michael@0 341 compatibilityOverrides: null,
michael@0 342
michael@0 343 /**
michael@0 344 * True if the add-on has a secure means of updating
michael@0 345 */
michael@0 346 providesUpdatesSecurely: true,
michael@0 347
michael@0 348 /**
michael@0 349 * The current blocklist state of the add-on
michael@0 350 */
michael@0 351 blocklistState: Ci.nsIBlocklistService.STATE_NOT_BLOCKED,
michael@0 352
michael@0 353 /**
michael@0 354 * True if this add-on cannot be used in the application based on version
michael@0 355 * compatibility, dependencies and blocklisting
michael@0 356 */
michael@0 357 appDisabled: false,
michael@0 358
michael@0 359 /**
michael@0 360 * True if the user wants this add-on to be disabled
michael@0 361 */
michael@0 362 userDisabled: false,
michael@0 363
michael@0 364 /**
michael@0 365 * Indicates what scope the add-on is installed in, per profile, user,
michael@0 366 * system or application
michael@0 367 */
michael@0 368 scope: AddonManager.SCOPE_PROFILE,
michael@0 369
michael@0 370 /**
michael@0 371 * True if the add-on is currently functional
michael@0 372 */
michael@0 373 isActive: true,
michael@0 374
michael@0 375 /**
michael@0 376 * A bitfield holding all of the current operations that are waiting to be
michael@0 377 * performed for this add-on
michael@0 378 */
michael@0 379 pendingOperations: AddonManager.PENDING_NONE,
michael@0 380
michael@0 381 /**
michael@0 382 * A bitfield holding all the the operations that can be performed on
michael@0 383 * this add-on
michael@0 384 */
michael@0 385 permissions: 0,
michael@0 386
michael@0 387 /**
michael@0 388 * Tests whether this add-on is known to be compatible with a
michael@0 389 * particular application and platform version.
michael@0 390 *
michael@0 391 * @param appVersion
michael@0 392 * An application version to test against
michael@0 393 * @param platformVersion
michael@0 394 * A platform version to test against
michael@0 395 * @return Boolean representing if the add-on is compatible
michael@0 396 */
michael@0 397 isCompatibleWith: function ASR_isCompatibleWith(aAppVerison, aPlatformVersion) {
michael@0 398 return true;
michael@0 399 },
michael@0 400
michael@0 401 /**
michael@0 402 * Starts an update check for this add-on. This will perform
michael@0 403 * asynchronously and deliver results to the given listener.
michael@0 404 *
michael@0 405 * @param aListener
michael@0 406 * An UpdateListener for the update process
michael@0 407 * @param aReason
michael@0 408 * A reason code for performing the update
michael@0 409 * @param aAppVersion
michael@0 410 * An application version to check for updates for
michael@0 411 * @param aPlatformVersion
michael@0 412 * A platform version to check for updates for
michael@0 413 */
michael@0 414 findUpdates: function ASR_findUpdates(aListener, aReason, aAppVersion, aPlatformVersion) {
michael@0 415 if ("onNoCompatibilityUpdateAvailable" in aListener)
michael@0 416 aListener.onNoCompatibilityUpdateAvailable(this);
michael@0 417 if ("onNoUpdateAvailable" in aListener)
michael@0 418 aListener.onNoUpdateAvailable(this);
michael@0 419 if ("onUpdateFinished" in aListener)
michael@0 420 aListener.onUpdateFinished(this);
michael@0 421 },
michael@0 422
michael@0 423 toJSON: function() {
michael@0 424 let json = {};
michael@0 425
michael@0 426 for (let [property, value] of Iterator(this)) {
michael@0 427 if (property.startsWith("_") ||
michael@0 428 typeof(value) === "function")
michael@0 429 continue;
michael@0 430
michael@0 431 try {
michael@0 432 switch (property) {
michael@0 433 case "sourceURI":
michael@0 434 json.sourceURI = value ? value.spec : "";
michael@0 435 break;
michael@0 436
michael@0 437 case "updateDate":
michael@0 438 json.updateDate = value ? value.getTime() : "";
michael@0 439 break;
michael@0 440
michael@0 441 default:
michael@0 442 json[property] = value;
michael@0 443 }
michael@0 444 } catch (ex) {
michael@0 445 logger.warn("Error writing property value for " + property);
michael@0 446 }
michael@0 447 }
michael@0 448
michael@0 449 for (let [property, value] of Iterator(this._unsupportedProperties)) {
michael@0 450 if (!property.startsWith("_"))
michael@0 451 json[property] = value;
michael@0 452 }
michael@0 453
michael@0 454 return json;
michael@0 455 }
michael@0 456 }
michael@0 457
michael@0 458 /**
michael@0 459 * The add-on repository is a source of add-ons that can be installed. It can
michael@0 460 * be searched in three ways. The first takes a list of IDs and returns a
michael@0 461 * list of the corresponding add-ons. The second returns a list of add-ons that
michael@0 462 * come highly recommended. This list should change frequently. The third is to
michael@0 463 * search for specific search terms entered by the user. Searches are
michael@0 464 * asynchronous and results should be passed to the provided callback object
michael@0 465 * when complete. The results passed to the callback should only include add-ons
michael@0 466 * that are compatible with the current application and are not already
michael@0 467 * installed.
michael@0 468 */
michael@0 469 this.AddonRepository = {
michael@0 470 /**
michael@0 471 * Whether caching is currently enabled
michael@0 472 */
michael@0 473 get cacheEnabled() {
michael@0 474 // Act as though caching is disabled if there was an unrecoverable error
michael@0 475 // openning the database.
michael@0 476 if (!AddonDatabase.databaseOk) {
michael@0 477 logger.warn("Cache is disabled because database is not OK");
michael@0 478 return false;
michael@0 479 }
michael@0 480
michael@0 481 let preference = PREF_GETADDONS_CACHE_ENABLED;
michael@0 482 let enabled = false;
michael@0 483 try {
michael@0 484 enabled = Services.prefs.getBoolPref(preference);
michael@0 485 } catch(e) {
michael@0 486 logger.warn("cacheEnabled: Couldn't get pref: " + preference);
michael@0 487 }
michael@0 488
michael@0 489 return enabled;
michael@0 490 },
michael@0 491
michael@0 492 // A cache of the add-ons stored in the database
michael@0 493 _addons: null,
michael@0 494
michael@0 495 // An array of callbacks pending the retrieval of add-ons from AddonDatabase
michael@0 496 _pendingCallbacks: null,
michael@0 497
michael@0 498 // Whether a migration in currently in progress
michael@0 499 _migrationInProgress: false,
michael@0 500
michael@0 501 // A callback to be called when migration finishes
michael@0 502 _postMigrationCallback: null,
michael@0 503
michael@0 504 // Whether a search is currently in progress
michael@0 505 _searching: false,
michael@0 506
michael@0 507 // XHR associated with the current request
michael@0 508 _request: null,
michael@0 509
michael@0 510 /*
michael@0 511 * Addon search results callback object that contains two functions
michael@0 512 *
michael@0 513 * searchSucceeded - Called when a search has suceeded.
michael@0 514 *
michael@0 515 * @param aAddons
michael@0 516 * An array of the add-on results. In the case of searching for
michael@0 517 * specific terms the ordering of results may be determined by
michael@0 518 * the search provider.
michael@0 519 * @param aAddonCount
michael@0 520 * The length of aAddons
michael@0 521 * @param aTotalResults
michael@0 522 * The total results actually available in the repository
michael@0 523 *
michael@0 524 *
michael@0 525 * searchFailed - Called when an error occurred when performing a search.
michael@0 526 */
michael@0 527 _callback: null,
michael@0 528
michael@0 529 // Maximum number of results to return
michael@0 530 _maxResults: null,
michael@0 531
michael@0 532 /**
michael@0 533 * Shut down AddonRepository
michael@0 534 * return: promise{integer} resolves with the result of flushing
michael@0 535 * the AddonRepository database
michael@0 536 */
michael@0 537 shutdown: function AddonRepo_shutdown() {
michael@0 538 this.cancelSearch();
michael@0 539
michael@0 540 this._addons = null;
michael@0 541 this._pendingCallbacks = null;
michael@0 542 return AddonDatabase.shutdown(false);
michael@0 543 },
michael@0 544
michael@0 545 /**
michael@0 546 * Asynchronously get a cached add-on by id. The add-on (or null if the
michael@0 547 * add-on is not found) is passed to the specified callback. If caching is
michael@0 548 * disabled, null is passed to the specified callback.
michael@0 549 *
michael@0 550 * @param aId
michael@0 551 * The id of the add-on to get
michael@0 552 * @param aCallback
michael@0 553 * The callback to pass the result back to
michael@0 554 */
michael@0 555 getCachedAddonByID: function AddonRepo_getCachedAddonByID(aId, aCallback) {
michael@0 556 if (!aId || !this.cacheEnabled) {
michael@0 557 aCallback(null);
michael@0 558 return;
michael@0 559 }
michael@0 560
michael@0 561 let self = this;
michael@0 562 function getAddon(aAddons) {
michael@0 563 aCallback((aId in aAddons) ? aAddons[aId] : null);
michael@0 564 }
michael@0 565
michael@0 566 if (this._addons == null) {
michael@0 567 if (this._pendingCallbacks == null) {
michael@0 568 // Data has not been retrieved from the database, so retrieve it
michael@0 569 this._pendingCallbacks = [];
michael@0 570 this._pendingCallbacks.push(getAddon);
michael@0 571 AddonDatabase.retrieveStoredData(function getCachedAddonByID_retrieveData(aAddons) {
michael@0 572 let pendingCallbacks = self._pendingCallbacks;
michael@0 573
michael@0 574 // Check if cache was shutdown or deleted before callback was called
michael@0 575 if (pendingCallbacks == null)
michael@0 576 return;
michael@0 577
michael@0 578 // Callbacks may want to trigger a other caching operations that may
michael@0 579 // affect _addons and _pendingCallbacks, so set to final values early
michael@0 580 self._pendingCallbacks = null;
michael@0 581 self._addons = aAddons;
michael@0 582
michael@0 583 pendingCallbacks.forEach(function(aCallback) aCallback(aAddons));
michael@0 584 });
michael@0 585
michael@0 586 return;
michael@0 587 }
michael@0 588
michael@0 589 // Data is being retrieved from the database, so wait
michael@0 590 this._pendingCallbacks.push(getAddon);
michael@0 591 return;
michael@0 592 }
michael@0 593
michael@0 594 // Data has been retrieved, so immediately return result
michael@0 595 getAddon(this._addons);
michael@0 596 },
michael@0 597
michael@0 598 /**
michael@0 599 * Asynchronously repopulate cache so it only contains the add-ons
michael@0 600 * corresponding to the specified ids. If caching is disabled,
michael@0 601 * the cache is completely removed.
michael@0 602 *
michael@0 603 * @param aIds
michael@0 604 * The array of add-on ids to repopulate the cache with
michael@0 605 * @param aCallback
michael@0 606 * The optional callback to call once complete
michael@0 607 * @param aTimeout
michael@0 608 * (Optional) timeout in milliseconds to abandon the XHR request
michael@0 609 * if we have not received a response from the server.
michael@0 610 */
michael@0 611 repopulateCache: function(aIds, aCallback, aTimeout) {
michael@0 612 this._repopulateCacheInternal(aIds, aCallback, false, aTimeout);
michael@0 613 },
michael@0 614
michael@0 615 _repopulateCacheInternal: function (aIds, aCallback, aSendPerformance, aTimeout) {
michael@0 616 // Always call AddonManager updateAddonRepositoryData after we refill the cache
michael@0 617 function repopulateAddonManager() {
michael@0 618 AddonManagerPrivate.updateAddonRepositoryData(aCallback);
michael@0 619 }
michael@0 620
michael@0 621 logger.debug("Repopulate add-on cache with " + aIds.toSource());
michael@0 622 // Completely remove cache if caching is not enabled
michael@0 623 if (!this.cacheEnabled) {
michael@0 624 logger.debug("Clearing cache because it is disabled");
michael@0 625 this._addons = null;
michael@0 626 this._pendingCallbacks = null;
michael@0 627 AddonDatabase.delete(repopulateAddonManager);
michael@0 628 return;
michael@0 629 }
michael@0 630
michael@0 631 let self = this;
michael@0 632 getAddonsToCache(aIds, function repopulateCache_getAddonsToCache(aAddons) {
michael@0 633 // Completely remove cache if there are no add-ons to cache
michael@0 634 if (aAddons.length == 0) {
michael@0 635 logger.debug("Clearing cache because 0 add-ons were requested");
michael@0 636 self._addons = null;
michael@0 637 self._pendingCallbacks = null;
michael@0 638 AddonDatabase.delete(repopulateAddonManager);
michael@0 639 return;
michael@0 640 }
michael@0 641
michael@0 642 self._beginGetAddons(aAddons, {
michael@0 643 searchSucceeded: function repopulateCacheInternal_searchSucceeded(aAddons) {
michael@0 644 self._addons = {};
michael@0 645 aAddons.forEach(function(aAddon) { self._addons[aAddon.id] = aAddon; });
michael@0 646 AddonDatabase.repopulate(aAddons, repopulateAddonManager);
michael@0 647 },
michael@0 648 searchFailed: function repopulateCacheInternal_searchFailed() {
michael@0 649 logger.warn("Search failed when repopulating cache");
michael@0 650 repopulateAddonManager();
michael@0 651 }
michael@0 652 }, aSendPerformance, aTimeout);
michael@0 653 });
michael@0 654 },
michael@0 655
michael@0 656 /**
michael@0 657 * Asynchronously add add-ons to the cache corresponding to the specified
michael@0 658 * ids. If caching is disabled, the cache is unchanged and the callback is
michael@0 659 * immediately called if it is defined.
michael@0 660 *
michael@0 661 * @param aIds
michael@0 662 * The array of add-on ids to add to the cache
michael@0 663 * @param aCallback
michael@0 664 * The optional callback to call once complete
michael@0 665 */
michael@0 666 cacheAddons: function AddonRepo_cacheAddons(aIds, aCallback) {
michael@0 667 logger.debug("cacheAddons: enabled " + this.cacheEnabled + " IDs " + aIds.toSource());
michael@0 668 if (!this.cacheEnabled) {
michael@0 669 if (aCallback)
michael@0 670 aCallback();
michael@0 671 return;
michael@0 672 }
michael@0 673
michael@0 674 let self = this;
michael@0 675 getAddonsToCache(aIds, function cacheAddons_getAddonsToCache(aAddons) {
michael@0 676 // If there are no add-ons to cache, act as if caching is disabled
michael@0 677 if (aAddons.length == 0) {
michael@0 678 if (aCallback)
michael@0 679 aCallback();
michael@0 680 return;
michael@0 681 }
michael@0 682
michael@0 683 self.getAddonsByIDs(aAddons, {
michael@0 684 searchSucceeded: function cacheAddons_searchSucceeded(aAddons) {
michael@0 685 aAddons.forEach(function(aAddon) { self._addons[aAddon.id] = aAddon; });
michael@0 686 AddonDatabase.insertAddons(aAddons, aCallback);
michael@0 687 },
michael@0 688 searchFailed: function cacheAddons_searchFailed() {
michael@0 689 logger.warn("Search failed when adding add-ons to cache");
michael@0 690 if (aCallback)
michael@0 691 aCallback();
michael@0 692 }
michael@0 693 });
michael@0 694 });
michael@0 695 },
michael@0 696
michael@0 697 /**
michael@0 698 * The homepage for visiting this repository. If the corresponding preference
michael@0 699 * is not defined, defaults to about:blank.
michael@0 700 */
michael@0 701 get homepageURL() {
michael@0 702 let url = this._formatURLPref(PREF_GETADDONS_BROWSEADDONS, {});
michael@0 703 return (url != null) ? url : "about:blank";
michael@0 704 },
michael@0 705
michael@0 706 /**
michael@0 707 * Returns whether this instance is currently performing a search. New
michael@0 708 * searches will not be performed while this is the case.
michael@0 709 */
michael@0 710 get isSearching() {
michael@0 711 return this._searching;
michael@0 712 },
michael@0 713
michael@0 714 /**
michael@0 715 * The url that can be visited to see recommended add-ons in this repository.
michael@0 716 * If the corresponding preference is not defined, defaults to about:blank.
michael@0 717 */
michael@0 718 getRecommendedURL: function AddonRepo_getRecommendedURL() {
michael@0 719 let url = this._formatURLPref(PREF_GETADDONS_BROWSERECOMMENDED, {});
michael@0 720 return (url != null) ? url : "about:blank";
michael@0 721 },
michael@0 722
michael@0 723 /**
michael@0 724 * Retrieves the url that can be visited to see search results for the given
michael@0 725 * terms. If the corresponding preference is not defined, defaults to
michael@0 726 * about:blank.
michael@0 727 *
michael@0 728 * @param aSearchTerms
michael@0 729 * Search terms used to search the repository
michael@0 730 */
michael@0 731 getSearchURL: function AddonRepo_getSearchURL(aSearchTerms) {
michael@0 732 let url = this._formatURLPref(PREF_GETADDONS_BROWSESEARCHRESULTS, {
michael@0 733 TERMS : encodeURIComponent(aSearchTerms)
michael@0 734 });
michael@0 735 return (url != null) ? url : "about:blank";
michael@0 736 },
michael@0 737
michael@0 738 /**
michael@0 739 * Cancels the search in progress. If there is no search in progress this
michael@0 740 * does nothing.
michael@0 741 */
michael@0 742 cancelSearch: function AddonRepo_cancelSearch() {
michael@0 743 this._searching = false;
michael@0 744 if (this._request) {
michael@0 745 this._request.abort();
michael@0 746 this._request = null;
michael@0 747 }
michael@0 748 this._callback = null;
michael@0 749 },
michael@0 750
michael@0 751 /**
michael@0 752 * Begins a search for add-ons in this repository by ID. Results will be
michael@0 753 * passed to the given callback.
michael@0 754 *
michael@0 755 * @param aIDs
michael@0 756 * The array of ids to search for
michael@0 757 * @param aCallback
michael@0 758 * The callback to pass results to
michael@0 759 */
michael@0 760 getAddonsByIDs: function AddonRepo_getAddonsByIDs(aIDs, aCallback) {
michael@0 761 return this._beginGetAddons(aIDs, aCallback, false);
michael@0 762 },
michael@0 763
michael@0 764 /**
michael@0 765 * Begins a search of add-ons, potentially sending performance data.
michael@0 766 *
michael@0 767 * @param aIDs
michael@0 768 * Array of ids to search for.
michael@0 769 * @param aCallback
michael@0 770 * Function to pass results to.
michael@0 771 * @param aSendPerformance
michael@0 772 * Boolean indicating whether to send performance data with the
michael@0 773 * request.
michael@0 774 * @param aTimeout
michael@0 775 * (Optional) timeout in milliseconds to abandon the XHR request
michael@0 776 * if we have not received a response from the server.
michael@0 777 */
michael@0 778 _beginGetAddons: function(aIDs, aCallback, aSendPerformance, aTimeout) {
michael@0 779 let ids = aIDs.slice(0);
michael@0 780
michael@0 781 let params = {
michael@0 782 API_VERSION : API_VERSION,
michael@0 783 IDS : ids.map(encodeURIComponent).join(',')
michael@0 784 };
michael@0 785
michael@0 786 let pref = PREF_GETADDONS_BYIDS;
michael@0 787
michael@0 788 if (aSendPerformance) {
michael@0 789 let type = Services.prefs.getPrefType(PREF_GETADDONS_BYIDS_PERFORMANCE);
michael@0 790 if (type == Services.prefs.PREF_STRING) {
michael@0 791 pref = PREF_GETADDONS_BYIDS_PERFORMANCE;
michael@0 792
michael@0 793 let startupInfo = Cc["@mozilla.org/toolkit/app-startup;1"].
michael@0 794 getService(Ci.nsIAppStartup).
michael@0 795 getStartupInfo();
michael@0 796
michael@0 797 params.TIME_MAIN = "";
michael@0 798 params.TIME_FIRST_PAINT = "";
michael@0 799 params.TIME_SESSION_RESTORED = "";
michael@0 800 if (startupInfo.process) {
michael@0 801 if (startupInfo.main) {
michael@0 802 params.TIME_MAIN = startupInfo.main - startupInfo.process;
michael@0 803 }
michael@0 804 if (startupInfo.firstPaint) {
michael@0 805 params.TIME_FIRST_PAINT = startupInfo.firstPaint -
michael@0 806 startupInfo.process;
michael@0 807 }
michael@0 808 if (startupInfo.sessionRestored) {
michael@0 809 params.TIME_SESSION_RESTORED = startupInfo.sessionRestored -
michael@0 810 startupInfo.process;
michael@0 811 }
michael@0 812 }
michael@0 813 }
michael@0 814 }
michael@0 815
michael@0 816 let url = this._formatURLPref(pref, params);
michael@0 817
michael@0 818 let self = this;
michael@0 819 function handleResults(aElements, aTotalResults, aCompatData) {
michael@0 820 // Don't use this._parseAddons() so that, for example,
michael@0 821 // incompatible add-ons are not filtered out
michael@0 822 let results = [];
michael@0 823 for (let i = 0; i < aElements.length && results.length < self._maxResults; i++) {
michael@0 824 let result = self._parseAddon(aElements[i], null, aCompatData);
michael@0 825 if (result == null)
michael@0 826 continue;
michael@0 827
michael@0 828 // Ignore add-on if it wasn't actually requested
michael@0 829 let idIndex = ids.indexOf(result.addon.id);
michael@0 830 if (idIndex == -1)
michael@0 831 continue;
michael@0 832
michael@0 833 results.push(result);
michael@0 834 // Ignore this add-on from now on
michael@0 835 ids.splice(idIndex, 1);
michael@0 836 }
michael@0 837
michael@0 838 // Include any compatibility overrides for addons not hosted by the
michael@0 839 // remote repository.
michael@0 840 for each (let addonCompat in aCompatData) {
michael@0 841 if (addonCompat.hosted)
michael@0 842 continue;
michael@0 843
michael@0 844 let addon = new AddonSearchResult(addonCompat.id);
michael@0 845 // Compatibility overrides can only be for extensions.
michael@0 846 addon.type = "extension";
michael@0 847 addon.compatibilityOverrides = addonCompat.compatRanges;
michael@0 848 let result = {
michael@0 849 addon: addon,
michael@0 850 xpiURL: null,
michael@0 851 xpiHash: null
michael@0 852 };
michael@0 853 results.push(result);
michael@0 854 }
michael@0 855
michael@0 856 // aTotalResults irrelevant
michael@0 857 self._reportSuccess(results, -1);
michael@0 858 }
michael@0 859
michael@0 860 this._beginSearch(url, ids.length, aCallback, handleResults, aTimeout);
michael@0 861 },
michael@0 862
michael@0 863 /**
michael@0 864 * Performs the daily background update check.
michael@0 865 *
michael@0 866 * This API both searches for the add-on IDs specified and sends performance
michael@0 867 * data. It is meant to be called as part of the daily update ping. It should
michael@0 868 * not be used for any other purpose. Use repopulateCache instead.
michael@0 869 *
michael@0 870 * @param aIDs
michael@0 871 * Array of add-on IDs to repopulate the cache with.
michael@0 872 * @param aCallback
michael@0 873 * Function to call when data is received. Function must be an object
michael@0 874 * with the keys searchSucceeded and searchFailed.
michael@0 875 */
michael@0 876 backgroundUpdateCheck: function AddonRepo_backgroundUpdateCheck(aIDs, aCallback) {
michael@0 877 this._repopulateCacheInternal(aIDs, aCallback, true);
michael@0 878 },
michael@0 879
michael@0 880 /**
michael@0 881 * Begins a search for recommended add-ons in this repository. Results will
michael@0 882 * be passed to the given callback.
michael@0 883 *
michael@0 884 * @param aMaxResults
michael@0 885 * The maximum number of results to return
michael@0 886 * @param aCallback
michael@0 887 * The callback to pass results to
michael@0 888 */
michael@0 889 retrieveRecommendedAddons: function AddonRepo_retrieveRecommendedAddons(aMaxResults, aCallback) {
michael@0 890 let url = this._formatURLPref(PREF_GETADDONS_GETRECOMMENDED, {
michael@0 891 API_VERSION : API_VERSION,
michael@0 892
michael@0 893 // Get twice as many results to account for potential filtering
michael@0 894 MAX_RESULTS : 2 * aMaxResults
michael@0 895 });
michael@0 896
michael@0 897 let self = this;
michael@0 898 function handleResults(aElements, aTotalResults) {
michael@0 899 self._getLocalAddonIds(function retrieveRecommendedAddons_getLocalAddonIds(aLocalAddonIds) {
michael@0 900 // aTotalResults irrelevant
michael@0 901 self._parseAddons(aElements, -1, aLocalAddonIds);
michael@0 902 });
michael@0 903 }
michael@0 904
michael@0 905 this._beginSearch(url, aMaxResults, aCallback, handleResults);
michael@0 906 },
michael@0 907
michael@0 908 /**
michael@0 909 * Begins a search for add-ons in this repository. Results will be passed to
michael@0 910 * the given callback.
michael@0 911 *
michael@0 912 * @param aSearchTerms
michael@0 913 * The terms to search for
michael@0 914 * @param aMaxResults
michael@0 915 * The maximum number of results to return
michael@0 916 * @param aCallback
michael@0 917 * The callback to pass results to
michael@0 918 */
michael@0 919 searchAddons: function AddonRepo_searchAddons(aSearchTerms, aMaxResults, aCallback) {
michael@0 920 let compatMode = "normal";
michael@0 921 if (!AddonManager.checkCompatibility)
michael@0 922 compatMode = "ignore";
michael@0 923 else if (AddonManager.strictCompatibility)
michael@0 924 compatMode = "strict";
michael@0 925
michael@0 926 let substitutions = {
michael@0 927 API_VERSION : API_VERSION,
michael@0 928 TERMS : encodeURIComponent(aSearchTerms),
michael@0 929 // Get twice as many results to account for potential filtering
michael@0 930 MAX_RESULTS : 2 * aMaxResults,
michael@0 931 COMPATIBILITY_MODE : compatMode,
michael@0 932 };
michael@0 933
michael@0 934 let url = this._formatURLPref(PREF_GETADDONS_GETSEARCHRESULTS, substitutions);
michael@0 935
michael@0 936 let self = this;
michael@0 937 function handleResults(aElements, aTotalResults) {
michael@0 938 self._getLocalAddonIds(function searchAddons_getLocalAddonIds(aLocalAddonIds) {
michael@0 939 self._parseAddons(aElements, aTotalResults, aLocalAddonIds);
michael@0 940 });
michael@0 941 }
michael@0 942
michael@0 943 this._beginSearch(url, aMaxResults, aCallback, handleResults);
michael@0 944 },
michael@0 945
michael@0 946 // Posts results to the callback
michael@0 947 _reportSuccess: function AddonRepo_reportSuccess(aResults, aTotalResults) {
michael@0 948 this._searching = false;
michael@0 949 this._request = null;
michael@0 950 // The callback may want to trigger a new search so clear references early
michael@0 951 let addons = [result.addon for each(result in aResults)];
michael@0 952 let callback = this._callback;
michael@0 953 this._callback = null;
michael@0 954 callback.searchSucceeded(addons, addons.length, aTotalResults);
michael@0 955 },
michael@0 956
michael@0 957 // Notifies the callback of a failure
michael@0 958 _reportFailure: function AddonRepo_reportFailure() {
michael@0 959 this._searching = false;
michael@0 960 this._request = null;
michael@0 961 // The callback may want to trigger a new search so clear references early
michael@0 962 let callback = this._callback;
michael@0 963 this._callback = null;
michael@0 964 callback.searchFailed();
michael@0 965 },
michael@0 966
michael@0 967 // Get descendant by unique tag name. Returns null if not unique tag name.
michael@0 968 _getUniqueDescendant: function AddonRepo_getUniqueDescendant(aElement, aTagName) {
michael@0 969 let elementsList = aElement.getElementsByTagName(aTagName);
michael@0 970 return (elementsList.length == 1) ? elementsList[0] : null;
michael@0 971 },
michael@0 972
michael@0 973 // Get direct descendant by unique tag name.
michael@0 974 // Returns null if not unique tag name.
michael@0 975 _getUniqueDirectDescendant: function AddonRepo_getUniqueDirectDescendant(aElement, aTagName) {
michael@0 976 let elementsList = Array.filter(aElement.children,
michael@0 977 function arrayFiltering(aChild) aChild.tagName == aTagName);
michael@0 978 return (elementsList.length == 1) ? elementsList[0] : null;
michael@0 979 },
michael@0 980
michael@0 981 // Parse out trimmed text content. Returns null if text content empty.
michael@0 982 _getTextContent: function AddonRepo_getTextContent(aElement) {
michael@0 983 let textContent = aElement.textContent.trim();
michael@0 984 return (textContent.length > 0) ? textContent : null;
michael@0 985 },
michael@0 986
michael@0 987 // Parse out trimmed text content of a descendant with the specified tag name
michael@0 988 // Returns null if the parsing unsuccessful.
michael@0 989 _getDescendantTextContent: function AddonRepo_getDescendantTextContent(aElement, aTagName) {
michael@0 990 let descendant = this._getUniqueDescendant(aElement, aTagName);
michael@0 991 return (descendant != null) ? this._getTextContent(descendant) : null;
michael@0 992 },
michael@0 993
michael@0 994 // Parse out trimmed text content of a direct descendant with the specified
michael@0 995 // tag name.
michael@0 996 // Returns null if the parsing unsuccessful.
michael@0 997 _getDirectDescendantTextContent: function AddonRepo_getDirectDescendantTextContent(aElement, aTagName) {
michael@0 998 let descendant = this._getUniqueDirectDescendant(aElement, aTagName);
michael@0 999 return (descendant != null) ? this._getTextContent(descendant) : null;
michael@0 1000 },
michael@0 1001
michael@0 1002 /*
michael@0 1003 * Creates an AddonSearchResult by parsing an <addon> element
michael@0 1004 *
michael@0 1005 * @param aElement
michael@0 1006 * The <addon> element to parse
michael@0 1007 * @param aSkip
michael@0 1008 * Object containing ids and sourceURIs of add-ons to skip.
michael@0 1009 * @param aCompatData
michael@0 1010 * Array of parsed addon_compatibility elements to accosiate with the
michael@0 1011 * resulting AddonSearchResult. Optional.
michael@0 1012 * @return Result object containing the parsed AddonSearchResult, xpiURL and
michael@0 1013 * xpiHash if the parsing was successful. Otherwise returns null.
michael@0 1014 */
michael@0 1015 _parseAddon: function AddonRepo_parseAddon(aElement, aSkip, aCompatData) {
michael@0 1016 let skipIDs = (aSkip && aSkip.ids) ? aSkip.ids : [];
michael@0 1017 let skipSourceURIs = (aSkip && aSkip.sourceURIs) ? aSkip.sourceURIs : [];
michael@0 1018
michael@0 1019 let guid = this._getDescendantTextContent(aElement, "guid");
michael@0 1020 if (guid == null || skipIDs.indexOf(guid) != -1)
michael@0 1021 return null;
michael@0 1022
michael@0 1023 let addon = new AddonSearchResult(guid);
michael@0 1024 let result = {
michael@0 1025 addon: addon,
michael@0 1026 xpiURL: null,
michael@0 1027 xpiHash: null
michael@0 1028 };
michael@0 1029
michael@0 1030 if (aCompatData && guid in aCompatData)
michael@0 1031 addon.compatibilityOverrides = aCompatData[guid].compatRanges;
michael@0 1032
michael@0 1033 let self = this;
michael@0 1034 for (let node = aElement.firstChild; node; node = node.nextSibling) {
michael@0 1035 if (!(node instanceof Ci.nsIDOMElement))
michael@0 1036 continue;
michael@0 1037
michael@0 1038 let localName = node.localName;
michael@0 1039
michael@0 1040 // Handle case where the wanted string value is located in text content
michael@0 1041 // but only if the content is not empty
michael@0 1042 if (localName in STRING_KEY_MAP) {
michael@0 1043 addon[STRING_KEY_MAP[localName]] = this._getTextContent(node) || addon[STRING_KEY_MAP[localName]];
michael@0 1044 continue;
michael@0 1045 }
michael@0 1046
michael@0 1047 // Handle case where the wanted string value is html located in text content
michael@0 1048 if (localName in HTML_KEY_MAP) {
michael@0 1049 addon[HTML_KEY_MAP[localName]] = convertHTMLToPlainText(this._getTextContent(node));
michael@0 1050 continue;
michael@0 1051 }
michael@0 1052
michael@0 1053 // Handle case where the wanted integer value is located in text content
michael@0 1054 if (localName in INTEGER_KEY_MAP) {
michael@0 1055 let value = parseInt(this._getTextContent(node));
michael@0 1056 if (value >= 0)
michael@0 1057 addon[INTEGER_KEY_MAP[localName]] = value;
michael@0 1058 continue;
michael@0 1059 }
michael@0 1060
michael@0 1061 // Handle cases that aren't as simple as grabbing the text content
michael@0 1062 switch (localName) {
michael@0 1063 case "type":
michael@0 1064 // Map AMO's type id to corresponding string
michael@0 1065 let id = parseInt(node.getAttribute("id"));
michael@0 1066 switch (id) {
michael@0 1067 case 1:
michael@0 1068 addon.type = "extension";
michael@0 1069 break;
michael@0 1070 case 2:
michael@0 1071 addon.type = "theme";
michael@0 1072 break;
michael@0 1073 case 3:
michael@0 1074 addon.type = "dictionary";
michael@0 1075 break;
michael@0 1076 default:
michael@0 1077 logger.warn("Unknown type id when parsing addon: " + id);
michael@0 1078 }
michael@0 1079 break;
michael@0 1080 case "authors":
michael@0 1081 let authorNodes = node.getElementsByTagName("author");
michael@0 1082 for (let authorNode of authorNodes) {
michael@0 1083 let name = self._getDescendantTextContent(authorNode, "name");
michael@0 1084 let link = self._getDescendantTextContent(authorNode, "link");
michael@0 1085 if (name == null || link == null)
michael@0 1086 continue;
michael@0 1087
michael@0 1088 let author = new AddonManagerPrivate.AddonAuthor(name, link);
michael@0 1089 if (addon.creator == null)
michael@0 1090 addon.creator = author;
michael@0 1091 else {
michael@0 1092 if (addon.developers == null)
michael@0 1093 addon.developers = [];
michael@0 1094
michael@0 1095 addon.developers.push(author);
michael@0 1096 }
michael@0 1097 }
michael@0 1098 break;
michael@0 1099 case "previews":
michael@0 1100 let previewNodes = node.getElementsByTagName("preview");
michael@0 1101 for (let previewNode of previewNodes) {
michael@0 1102 let full = self._getUniqueDescendant(previewNode, "full");
michael@0 1103 if (full == null)
michael@0 1104 continue;
michael@0 1105
michael@0 1106 let fullURL = self._getTextContent(full);
michael@0 1107 let fullWidth = full.getAttribute("width");
michael@0 1108 let fullHeight = full.getAttribute("height");
michael@0 1109
michael@0 1110 let thumbnailURL, thumbnailWidth, thumbnailHeight;
michael@0 1111 let thumbnail = self._getUniqueDescendant(previewNode, "thumbnail");
michael@0 1112 if (thumbnail) {
michael@0 1113 thumbnailURL = self._getTextContent(thumbnail);
michael@0 1114 thumbnailWidth = thumbnail.getAttribute("width");
michael@0 1115 thumbnailHeight = thumbnail.getAttribute("height");
michael@0 1116 }
michael@0 1117 let caption = self._getDescendantTextContent(previewNode, "caption");
michael@0 1118 let screenshot = new AddonManagerPrivate.AddonScreenshot(fullURL, fullWidth, fullHeight,
michael@0 1119 thumbnailURL, thumbnailWidth,
michael@0 1120 thumbnailHeight, caption);
michael@0 1121
michael@0 1122 if (addon.screenshots == null)
michael@0 1123 addon.screenshots = [];
michael@0 1124
michael@0 1125 if (previewNode.getAttribute("primary") == 1)
michael@0 1126 addon.screenshots.unshift(screenshot);
michael@0 1127 else
michael@0 1128 addon.screenshots.push(screenshot);
michael@0 1129 }
michael@0 1130 break;
michael@0 1131 case "learnmore":
michael@0 1132 addon.learnmoreURL = this._getTextContent(node);
michael@0 1133 addon.homepageURL = addon.homepageURL || addon.learnmoreURL;
michael@0 1134 break;
michael@0 1135 case "contribution_data":
michael@0 1136 let meetDevelopers = this._getDescendantTextContent(node, "meet_developers");
michael@0 1137 let suggestedAmount = this._getDescendantTextContent(node, "suggested_amount");
michael@0 1138 if (meetDevelopers != null) {
michael@0 1139 addon.contributionURL = meetDevelopers;
michael@0 1140 addon.contributionAmount = suggestedAmount;
michael@0 1141 }
michael@0 1142 break
michael@0 1143 case "payment_data":
michael@0 1144 let link = this._getDescendantTextContent(node, "link");
michael@0 1145 let amountTag = this._getUniqueDescendant(node, "amount");
michael@0 1146 let amount = parseFloat(amountTag.getAttribute("amount"));
michael@0 1147 let displayAmount = this._getTextContent(amountTag);
michael@0 1148 if (link != null && amount != null && displayAmount != null) {
michael@0 1149 addon.purchaseURL = link;
michael@0 1150 addon.purchaseAmount = amount;
michael@0 1151 addon.purchaseDisplayAmount = displayAmount;
michael@0 1152 }
michael@0 1153 break
michael@0 1154 case "rating":
michael@0 1155 let averageRating = parseInt(this._getTextContent(node));
michael@0 1156 if (averageRating >= 0)
michael@0 1157 addon.averageRating = Math.min(5, averageRating);
michael@0 1158 break;
michael@0 1159 case "reviews":
michael@0 1160 let url = this._getTextContent(node);
michael@0 1161 let num = parseInt(node.getAttribute("num"));
michael@0 1162 if (url != null && num >= 0) {
michael@0 1163 addon.reviewURL = url;
michael@0 1164 addon.reviewCount = num;
michael@0 1165 }
michael@0 1166 break;
michael@0 1167 case "status":
michael@0 1168 let repositoryStatus = parseInt(node.getAttribute("id"));
michael@0 1169 if (!isNaN(repositoryStatus))
michael@0 1170 addon.repositoryStatus = repositoryStatus;
michael@0 1171 break;
michael@0 1172 case "all_compatible_os":
michael@0 1173 let nodes = node.getElementsByTagName("os");
michael@0 1174 addon.isPlatformCompatible = Array.some(nodes, function parseAddon_platformCompatFilter(aNode) {
michael@0 1175 let text = aNode.textContent.toLowerCase().trim();
michael@0 1176 return text == "all" || text == Services.appinfo.OS.toLowerCase();
michael@0 1177 });
michael@0 1178 break;
michael@0 1179 case "install":
michael@0 1180 // No os attribute means the xpi is compatible with any os
michael@0 1181 if (node.hasAttribute("os")) {
michael@0 1182 let os = node.getAttribute("os").trim().toLowerCase();
michael@0 1183 // If the os is not ALL and not the current OS then ignore this xpi
michael@0 1184 if (os != "all" && os != Services.appinfo.OS.toLowerCase())
michael@0 1185 break;
michael@0 1186 }
michael@0 1187
michael@0 1188 let xpiURL = this._getTextContent(node);
michael@0 1189 if (xpiURL == null)
michael@0 1190 break;
michael@0 1191
michael@0 1192 if (skipSourceURIs.indexOf(xpiURL) != -1)
michael@0 1193 return null;
michael@0 1194
michael@0 1195 result.xpiURL = xpiURL;
michael@0 1196 addon.sourceURI = NetUtil.newURI(xpiURL);
michael@0 1197
michael@0 1198 let size = parseInt(node.getAttribute("size"));
michael@0 1199 addon.size = (size >= 0) ? size : null;
michael@0 1200
michael@0 1201 let xpiHash = node.getAttribute("hash");
michael@0 1202 if (xpiHash != null)
michael@0 1203 xpiHash = xpiHash.trim();
michael@0 1204 result.xpiHash = xpiHash ? xpiHash : null;
michael@0 1205 break;
michael@0 1206 case "last_updated":
michael@0 1207 let epoch = parseInt(node.getAttribute("epoch"));
michael@0 1208 if (!isNaN(epoch))
michael@0 1209 addon.updateDate = new Date(1000 * epoch);
michael@0 1210 break;
michael@0 1211 case "icon":
michael@0 1212 addon.icons[node.getAttribute("size")] = this._getTextContent(node);
michael@0 1213 break;
michael@0 1214 }
michael@0 1215 }
michael@0 1216
michael@0 1217 return result;
michael@0 1218 },
michael@0 1219
michael@0 1220 _parseAddons: function AddonRepo_parseAddons(aElements, aTotalResults, aSkip) {
michael@0 1221 let self = this;
michael@0 1222 let results = [];
michael@0 1223
michael@0 1224 function isSameApplication(aAppNode) {
michael@0 1225 return self._getTextContent(aAppNode) == Services.appinfo.ID;
michael@0 1226 }
michael@0 1227
michael@0 1228 for (let i = 0; i < aElements.length && results.length < this._maxResults; i++) {
michael@0 1229 let element = aElements[i];
michael@0 1230
michael@0 1231 let tags = this._getUniqueDescendant(element, "compatible_applications");
michael@0 1232 if (tags == null)
michael@0 1233 continue;
michael@0 1234
michael@0 1235 let applications = tags.getElementsByTagName("appID");
michael@0 1236 let compatible = Array.some(applications, function parseAddons_applicationsCompatFilter(aAppNode) {
michael@0 1237 if (!isSameApplication(aAppNode))
michael@0 1238 return false;
michael@0 1239
michael@0 1240 let parent = aAppNode.parentNode;
michael@0 1241 let minVersion = self._getDescendantTextContent(parent, "min_version");
michael@0 1242 let maxVersion = self._getDescendantTextContent(parent, "max_version");
michael@0 1243 if (minVersion == null || maxVersion == null)
michael@0 1244 return false;
michael@0 1245
michael@0 1246 let currentVersion = Services.appinfo.version;
michael@0 1247 return (Services.vc.compare(minVersion, currentVersion) <= 0 &&
michael@0 1248 ((!AddonManager.strictCompatibility) ||
michael@0 1249 Services.vc.compare(currentVersion, maxVersion) <= 0));
michael@0 1250 });
michael@0 1251
michael@0 1252 // Ignore add-ons not compatible with this Application
michael@0 1253 if (!compatible) {
michael@0 1254 if (AddonManager.checkCompatibility)
michael@0 1255 continue;
michael@0 1256
michael@0 1257 if (!Array.some(applications, isSameApplication))
michael@0 1258 continue;
michael@0 1259 }
michael@0 1260
michael@0 1261 // Add-on meets all requirements, so parse out data.
michael@0 1262 // Don't pass in compatiblity override data, because that's only returned
michael@0 1263 // in GUID searches, which don't use _parseAddons().
michael@0 1264 let result = this._parseAddon(element, aSkip);
michael@0 1265 if (result == null)
michael@0 1266 continue;
michael@0 1267
michael@0 1268 // Ignore add-on missing a required attribute
michael@0 1269 let requiredAttributes = ["id", "name", "version", "type", "creator"];
michael@0 1270 if (requiredAttributes.some(function parseAddons_attributeFilter(aAttribute) !result.addon[aAttribute]))
michael@0 1271 continue;
michael@0 1272
michael@0 1273 // Add only if the add-on is compatible with the platform
michael@0 1274 if (!result.addon.isPlatformCompatible)
michael@0 1275 continue;
michael@0 1276
michael@0 1277 // Add only if there was an xpi compatible with this OS or there was a
michael@0 1278 // way to purchase the add-on
michael@0 1279 if (!result.xpiURL && !result.addon.purchaseURL)
michael@0 1280 continue;
michael@0 1281
michael@0 1282 result.addon.isCompatible = compatible;
michael@0 1283
michael@0 1284 results.push(result);
michael@0 1285 // Ignore this add-on from now on by adding it to the skip array
michael@0 1286 aSkip.ids.push(result.addon.id);
michael@0 1287 }
michael@0 1288
michael@0 1289 // Immediately report success if no AddonInstall instances to create
michael@0 1290 let pendingResults = results.length;
michael@0 1291 if (pendingResults == 0) {
michael@0 1292 this._reportSuccess(results, aTotalResults);
michael@0 1293 return;
michael@0 1294 }
michael@0 1295
michael@0 1296 // Create an AddonInstall for each result
michael@0 1297 let self = this;
michael@0 1298 results.forEach(function(aResult) {
michael@0 1299 let addon = aResult.addon;
michael@0 1300 let callback = function addonInstallCallback(aInstall) {
michael@0 1301 addon.install = aInstall;
michael@0 1302 pendingResults--;
michael@0 1303 if (pendingResults == 0)
michael@0 1304 self._reportSuccess(results, aTotalResults);
michael@0 1305 }
michael@0 1306
michael@0 1307 if (aResult.xpiURL) {
michael@0 1308 AddonManager.getInstallForURL(aResult.xpiURL, callback,
michael@0 1309 "application/x-xpinstall", aResult.xpiHash,
michael@0 1310 addon.name, addon.icons, addon.version);
michael@0 1311 }
michael@0 1312 else {
michael@0 1313 callback(null);
michael@0 1314 }
michael@0 1315 });
michael@0 1316 },
michael@0 1317
michael@0 1318 // Parses addon_compatibility nodes, that describe compatibility overrides.
michael@0 1319 _parseAddonCompatElement: function AddonRepo_parseAddonCompatElement(aResultObj, aElement) {
michael@0 1320 let guid = this._getDescendantTextContent(aElement, "guid");
michael@0 1321 if (!guid) {
michael@0 1322 logger.debug("Compatibility override is missing guid.");
michael@0 1323 return;
michael@0 1324 }
michael@0 1325
michael@0 1326 let compat = {id: guid};
michael@0 1327 compat.hosted = aElement.getAttribute("hosted") != "false";
michael@0 1328
michael@0 1329 function findMatchingAppRange(aNodes) {
michael@0 1330 let toolkitAppRange = null;
michael@0 1331 for (let node of aNodes) {
michael@0 1332 let appID = this._getDescendantTextContent(node, "appID");
michael@0 1333 if (appID != Services.appinfo.ID && appID != TOOLKIT_ID)
michael@0 1334 continue;
michael@0 1335
michael@0 1336 let minVersion = this._getDescendantTextContent(node, "min_version");
michael@0 1337 let maxVersion = this._getDescendantTextContent(node, "max_version");
michael@0 1338 if (minVersion == null || maxVersion == null)
michael@0 1339 continue;
michael@0 1340
michael@0 1341 let appRange = { appID: appID,
michael@0 1342 appMinVersion: minVersion,
michael@0 1343 appMaxVersion: maxVersion };
michael@0 1344
michael@0 1345 // Only use Toolkit app ranges if no ranges match the application ID.
michael@0 1346 if (appID == TOOLKIT_ID)
michael@0 1347 toolkitAppRange = appRange;
michael@0 1348 else
michael@0 1349 return appRange;
michael@0 1350 }
michael@0 1351 return toolkitAppRange;
michael@0 1352 }
michael@0 1353
michael@0 1354 function parseRangeNode(aNode) {
michael@0 1355 let type = aNode.getAttribute("type");
michael@0 1356 // Only "incompatible" (blacklisting) is supported for now.
michael@0 1357 if (type != "incompatible") {
michael@0 1358 logger.debug("Compatibility override of unsupported type found.");
michael@0 1359 return null;
michael@0 1360 }
michael@0 1361
michael@0 1362 let override = new AddonManagerPrivate.AddonCompatibilityOverride(type);
michael@0 1363
michael@0 1364 override.minVersion = this._getDirectDescendantTextContent(aNode, "min_version");
michael@0 1365 override.maxVersion = this._getDirectDescendantTextContent(aNode, "max_version");
michael@0 1366
michael@0 1367 if (!override.minVersion) {
michael@0 1368 logger.debug("Compatibility override is missing min_version.");
michael@0 1369 return null;
michael@0 1370 }
michael@0 1371 if (!override.maxVersion) {
michael@0 1372 logger.debug("Compatibility override is missing max_version.");
michael@0 1373 return null;
michael@0 1374 }
michael@0 1375
michael@0 1376 let appRanges = aNode.querySelectorAll("compatible_applications > application");
michael@0 1377 let appRange = findMatchingAppRange.bind(this)(appRanges);
michael@0 1378 if (!appRange) {
michael@0 1379 logger.debug("Compatibility override is missing a valid application range.");
michael@0 1380 return null;
michael@0 1381 }
michael@0 1382
michael@0 1383 override.appID = appRange.appID;
michael@0 1384 override.appMinVersion = appRange.appMinVersion;
michael@0 1385 override.appMaxVersion = appRange.appMaxVersion;
michael@0 1386
michael@0 1387 return override;
michael@0 1388 }
michael@0 1389
michael@0 1390 let rangeNodes = aElement.querySelectorAll("version_ranges > version_range");
michael@0 1391 compat.compatRanges = Array.map(rangeNodes, parseRangeNode.bind(this))
michael@0 1392 .filter(function compatRangesFilter(aItem) !!aItem);
michael@0 1393 if (compat.compatRanges.length == 0)
michael@0 1394 return;
michael@0 1395
michael@0 1396 aResultObj[compat.id] = compat;
michael@0 1397 },
michael@0 1398
michael@0 1399 // Parses addon_compatibility elements.
michael@0 1400 _parseAddonCompatData: function AddonRepo_parseAddonCompatData(aElements) {
michael@0 1401 let compatData = {};
michael@0 1402 Array.forEach(aElements, this._parseAddonCompatElement.bind(this, compatData));
michael@0 1403 return compatData;
michael@0 1404 },
michael@0 1405
michael@0 1406 // Begins a new search if one isn't currently executing
michael@0 1407 _beginSearch: function(aURI, aMaxResults, aCallback, aHandleResults, aTimeout) {
michael@0 1408 if (this._searching || aURI == null || aMaxResults <= 0) {
michael@0 1409 logger.warn("AddonRepository search failed: searching " + this._searching + " aURI " + aURI +
michael@0 1410 " aMaxResults " + aMaxResults);
michael@0 1411 aCallback.searchFailed();
michael@0 1412 return;
michael@0 1413 }
michael@0 1414
michael@0 1415 this._searching = true;
michael@0 1416 this._callback = aCallback;
michael@0 1417 this._maxResults = aMaxResults;
michael@0 1418
michael@0 1419 logger.debug("Requesting " + aURI);
michael@0 1420
michael@0 1421 this._request = new XHRequest();
michael@0 1422 this._request.mozBackgroundRequest = true;
michael@0 1423 this._request.open("GET", aURI, true);
michael@0 1424 this._request.overrideMimeType("text/xml");
michael@0 1425 if (aTimeout) {
michael@0 1426 this._request.timeout = aTimeout;
michael@0 1427 }
michael@0 1428
michael@0 1429 this._request.addEventListener("error", aEvent => this._reportFailure(), false);
michael@0 1430 this._request.addEventListener("timeout", aEvent => this._reportFailure(), false);
michael@0 1431 this._request.addEventListener("load", aEvent => {
michael@0 1432 logger.debug("Got metadata search load event");
michael@0 1433 let request = aEvent.target;
michael@0 1434 let responseXML = request.responseXML;
michael@0 1435
michael@0 1436 if (!responseXML || responseXML.documentElement.namespaceURI == XMLURI_PARSE_ERROR ||
michael@0 1437 (request.status != 200 && request.status != 0)) {
michael@0 1438 this._reportFailure();
michael@0 1439 return;
michael@0 1440 }
michael@0 1441
michael@0 1442 let documentElement = responseXML.documentElement;
michael@0 1443 let elements = documentElement.getElementsByTagName("addon");
michael@0 1444 let totalResults = elements.length;
michael@0 1445 let parsedTotalResults = parseInt(documentElement.getAttribute("total_results"));
michael@0 1446 // Parsed value of total results only makes sense if >= elements.length
michael@0 1447 if (parsedTotalResults >= totalResults)
michael@0 1448 totalResults = parsedTotalResults;
michael@0 1449
michael@0 1450 let compatElements = documentElement.getElementsByTagName("addon_compatibility");
michael@0 1451 let compatData = this._parseAddonCompatData(compatElements);
michael@0 1452
michael@0 1453 aHandleResults(elements, totalResults, compatData);
michael@0 1454 }, false);
michael@0 1455 this._request.send(null);
michael@0 1456 },
michael@0 1457
michael@0 1458 // Gets the id's of local add-ons, and the sourceURI's of local installs,
michael@0 1459 // passing the results to aCallback
michael@0 1460 _getLocalAddonIds: function AddonRepo_getLocalAddonIds(aCallback) {
michael@0 1461 let self = this;
michael@0 1462 let localAddonIds = {ids: null, sourceURIs: null};
michael@0 1463
michael@0 1464 AddonManager.getAllAddons(function getLocalAddonIds_getAllAddons(aAddons) {
michael@0 1465 localAddonIds.ids = [a.id for each (a in aAddons)];
michael@0 1466 if (localAddonIds.sourceURIs)
michael@0 1467 aCallback(localAddonIds);
michael@0 1468 });
michael@0 1469
michael@0 1470 AddonManager.getAllInstalls(function getLocalAddonIds_getAllInstalls(aInstalls) {
michael@0 1471 localAddonIds.sourceURIs = [];
michael@0 1472 aInstalls.forEach(function(aInstall) {
michael@0 1473 if (aInstall.state != AddonManager.STATE_AVAILABLE)
michael@0 1474 localAddonIds.sourceURIs.push(aInstall.sourceURI.spec);
michael@0 1475 });
michael@0 1476
michael@0 1477 if (localAddonIds.ids)
michael@0 1478 aCallback(localAddonIds);
michael@0 1479 });
michael@0 1480 },
michael@0 1481
michael@0 1482 // Create url from preference, returning null if preference does not exist
michael@0 1483 _formatURLPref: function AddonRepo_formatURLPref(aPreference, aSubstitutions) {
michael@0 1484 let url = null;
michael@0 1485 try {
michael@0 1486 url = Services.prefs.getCharPref(aPreference);
michael@0 1487 } catch(e) {
michael@0 1488 logger.warn("_formatURLPref: Couldn't get pref: " + aPreference);
michael@0 1489 return null;
michael@0 1490 }
michael@0 1491
michael@0 1492 url = url.replace(/%([A-Z_]+)%/g, function urlSubstitution(aMatch, aKey) {
michael@0 1493 return (aKey in aSubstitutions) ? aSubstitutions[aKey] : aMatch;
michael@0 1494 });
michael@0 1495
michael@0 1496 return Services.urlFormatter.formatURL(url);
michael@0 1497 },
michael@0 1498
michael@0 1499 // Find a AddonCompatibilityOverride that matches a given aAddonVersion and
michael@0 1500 // application/platform version.
michael@0 1501 findMatchingCompatOverride: function AddonRepo_findMatchingCompatOverride(aAddonVersion,
michael@0 1502 aCompatOverrides,
michael@0 1503 aAppVersion,
michael@0 1504 aPlatformVersion) {
michael@0 1505 for (let override of aCompatOverrides) {
michael@0 1506
michael@0 1507 let appVersion = null;
michael@0 1508 if (override.appID == TOOLKIT_ID)
michael@0 1509 appVersion = aPlatformVersion || Services.appinfo.platformVersion;
michael@0 1510 else
michael@0 1511 appVersion = aAppVersion || Services.appinfo.version;
michael@0 1512
michael@0 1513 if (Services.vc.compare(override.minVersion, aAddonVersion) <= 0 &&
michael@0 1514 Services.vc.compare(aAddonVersion, override.maxVersion) <= 0 &&
michael@0 1515 Services.vc.compare(override.appMinVersion, appVersion) <= 0 &&
michael@0 1516 Services.vc.compare(appVersion, override.appMaxVersion) <= 0) {
michael@0 1517 return override;
michael@0 1518 }
michael@0 1519 }
michael@0 1520 return null;
michael@0 1521 },
michael@0 1522
michael@0 1523 flush: function() {
michael@0 1524 return AddonDatabase.flush();
michael@0 1525 }
michael@0 1526 };
michael@0 1527
michael@0 1528 var AddonDatabase = {
michael@0 1529 // true if the database connection has been opened
michael@0 1530 initialized: false,
michael@0 1531 // false if there was an unrecoverable error openning the database
michael@0 1532 databaseOk: true,
michael@0 1533
michael@0 1534 // the in-memory database
michael@0 1535 DB: BLANK_DB(),
michael@0 1536
michael@0 1537 /**
michael@0 1538 * A getter to retrieve an nsIFile pointer to the DB
michael@0 1539 */
michael@0 1540 get jsonFile() {
michael@0 1541 delete this.jsonFile;
michael@0 1542 return this.jsonFile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_DATABASE], true);
michael@0 1543 },
michael@0 1544
michael@0 1545 /**
michael@0 1546 * Synchronously opens a new connection to the database file.
michael@0 1547 */
michael@0 1548 openConnection: function() {
michael@0 1549 this.DB = BLANK_DB();
michael@0 1550 this.initialized = true;
michael@0 1551 delete this.connection;
michael@0 1552
michael@0 1553 let inputDB, fstream, cstream, schema;
michael@0 1554
michael@0 1555 try {
michael@0 1556 let data = "";
michael@0 1557 fstream = Cc["@mozilla.org/network/file-input-stream;1"]
michael@0 1558 .createInstance(Ci.nsIFileInputStream);
michael@0 1559 cstream = Cc["@mozilla.org/intl/converter-input-stream;1"]
michael@0 1560 .createInstance(Ci.nsIConverterInputStream);
michael@0 1561
michael@0 1562 fstream.init(this.jsonFile, -1, 0, 0);
michael@0 1563 cstream.init(fstream, "UTF-8", 0, 0);
michael@0 1564 let (str = {}) {
michael@0 1565 let read = 0;
michael@0 1566 do {
michael@0 1567 read = cstream.readString(0xffffffff, str); // read as much as we can and put it in str.value
michael@0 1568 data += str.value;
michael@0 1569 } while (read != 0);
michael@0 1570 }
michael@0 1571
michael@0 1572 inputDB = JSON.parse(data);
michael@0 1573
michael@0 1574 if (!inputDB.hasOwnProperty("addons") ||
michael@0 1575 !Array.isArray(inputDB.addons)) {
michael@0 1576 throw new Error("No addons array.");
michael@0 1577 }
michael@0 1578
michael@0 1579 if (!inputDB.hasOwnProperty("schema")) {
michael@0 1580 throw new Error("No schema specified.");
michael@0 1581 }
michael@0 1582
michael@0 1583 schema = parseInt(inputDB.schema, 10);
michael@0 1584
michael@0 1585 if (!Number.isInteger(schema) ||
michael@0 1586 schema < DB_MIN_JSON_SCHEMA) {
michael@0 1587 throw new Error("Invalid schema value.");
michael@0 1588 }
michael@0 1589
michael@0 1590 } catch (e if e.result == Cr.NS_ERROR_FILE_NOT_FOUND) {
michael@0 1591 logger.debug("No " + FILE_DATABASE + " found.");
michael@0 1592
michael@0 1593 // Create a blank addons.json file
michael@0 1594 this._saveDBToDisk();
michael@0 1595
michael@0 1596 let dbSchema = 0;
michael@0 1597 try {
michael@0 1598 dbSchema = Services.prefs.getIntPref(PREF_GETADDONS_DB_SCHEMA);
michael@0 1599 } catch (e) {}
michael@0 1600
michael@0 1601 if (dbSchema < DB_MIN_JSON_SCHEMA) {
michael@0 1602 this._migrationInProgress = AddonRepository_SQLiteMigrator.migrate((results) => {
michael@0 1603 if (results.length)
michael@0 1604 this.insertAddons(results);
michael@0 1605
michael@0 1606 if (this._postMigrationCallback) {
michael@0 1607 this._postMigrationCallback();
michael@0 1608 this._postMigrationCallback = null;
michael@0 1609 }
michael@0 1610
michael@0 1611 this._migrationInProgress = false;
michael@0 1612 });
michael@0 1613
michael@0 1614 Services.prefs.setIntPref(PREF_GETADDONS_DB_SCHEMA, DB_SCHEMA);
michael@0 1615 }
michael@0 1616
michael@0 1617 return;
michael@0 1618
michael@0 1619 } catch (e) {
michael@0 1620 logger.error("Malformed " + FILE_DATABASE + ": " + e);
michael@0 1621 this.databaseOk = false;
michael@0 1622 return;
michael@0 1623
michael@0 1624 } finally {
michael@0 1625 cstream.close();
michael@0 1626 fstream.close();
michael@0 1627 }
michael@0 1628
michael@0 1629 Services.prefs.setIntPref(PREF_GETADDONS_DB_SCHEMA, DB_SCHEMA);
michael@0 1630
michael@0 1631 // We use _insertAddon manually instead of calling
michael@0 1632 // insertAddons to avoid the write to disk which would
michael@0 1633 // be a waste since this is the data that was just read.
michael@0 1634 for (let addon of inputDB.addons) {
michael@0 1635 this._insertAddon(addon);
michael@0 1636 }
michael@0 1637 },
michael@0 1638
michael@0 1639 /**
michael@0 1640 * A lazy getter for the database connection.
michael@0 1641 */
michael@0 1642 get connection() {
michael@0 1643 return this.openConnection();
michael@0 1644 },
michael@0 1645
michael@0 1646 /**
michael@0 1647 * Asynchronously shuts down the database connection and releases all
michael@0 1648 * cached objects
michael@0 1649 *
michael@0 1650 * @param aCallback
michael@0 1651 * An optional callback to call once complete
michael@0 1652 * @param aSkipFlush
michael@0 1653 * An optional boolean to skip flushing data to disk. Useful
michael@0 1654 * when the database is going to be deleted afterwards.
michael@0 1655 */
michael@0 1656 shutdown: function AD_shutdown(aSkipFlush) {
michael@0 1657 this.databaseOk = true;
michael@0 1658
michael@0 1659 if (!this.initialized) {
michael@0 1660 return Promise.resolve(0);
michael@0 1661 }
michael@0 1662
michael@0 1663 this.initialized = false;
michael@0 1664
michael@0 1665 this.__defineGetter__("connection", function shutdown_connectionGetter() {
michael@0 1666 return this.openConnection();
michael@0 1667 });
michael@0 1668
michael@0 1669 if (aSkipFlush) {
michael@0 1670 return Promise.resolve(0);
michael@0 1671 } else {
michael@0 1672 return this.Writer.flush();
michael@0 1673 }
michael@0 1674 },
michael@0 1675
michael@0 1676 /**
michael@0 1677 * Asynchronously deletes the database, shutting down the connection
michael@0 1678 * first if initialized
michael@0 1679 *
michael@0 1680 * @param aCallback
michael@0 1681 * An optional callback to call once complete
michael@0 1682 */
michael@0 1683 delete: function AD_delete(aCallback) {
michael@0 1684 this.DB = BLANK_DB();
michael@0 1685
michael@0 1686 this._deleting = this.Writer.flush()
michael@0 1687 .then(null, () => {})
michael@0 1688 // shutdown(true) never rejects
michael@0 1689 .then(() => this.shutdown(true))
michael@0 1690 .then(() => OS.File.remove(this.jsonFile.path, {}))
michael@0 1691 .then(null, error => logger.error("Unable to delete Addon Repository file " +
michael@0 1692 this.jsonFile.path, error))
michael@0 1693 .then(() => this._deleting = null)
michael@0 1694 .then(aCallback);
michael@0 1695 },
michael@0 1696
michael@0 1697 toJSON: function AD_toJSON() {
michael@0 1698 let json = {
michael@0 1699 schema: this.DB.schema,
michael@0 1700 addons: []
michael@0 1701 }
michael@0 1702
michael@0 1703 for (let [, value] of this.DB.addons)
michael@0 1704 json.addons.push(value);
michael@0 1705
michael@0 1706 return json;
michael@0 1707 },
michael@0 1708
michael@0 1709 /*
michael@0 1710 * This is a deferred task writer that is used
michael@0 1711 * to batch operations done within 50ms of each
michael@0 1712 * other and thus generating only one write to disk
michael@0 1713 */
michael@0 1714 get Writer() {
michael@0 1715 delete this.Writer;
michael@0 1716 this.Writer = new DeferredSave(
michael@0 1717 this.jsonFile.path,
michael@0 1718 () => { return JSON.stringify(this); },
michael@0 1719 DB_BATCH_TIMEOUT_MS
michael@0 1720 );
michael@0 1721 return this.Writer;
michael@0 1722 },
michael@0 1723
michael@0 1724 /**
michael@0 1725 * Flush any pending I/O on the addons.json file
michael@0 1726 * @return: Promise{null}
michael@0 1727 * Resolves when the pending I/O (writing out or deleting
michael@0 1728 * addons.json) completes
michael@0 1729 */
michael@0 1730 flush: function() {
michael@0 1731 if (this._deleting) {
michael@0 1732 return this._deleting;
michael@0 1733 }
michael@0 1734 return this.Writer.flush();
michael@0 1735 },
michael@0 1736
michael@0 1737 /**
michael@0 1738 * Asynchronously retrieve all add-ons from the database, and pass it
michael@0 1739 * to the specified callback
michael@0 1740 *
michael@0 1741 * @param aCallback
michael@0 1742 * The callback to pass the add-ons back to
michael@0 1743 */
michael@0 1744 retrieveStoredData: function AD_retrieveStoredData(aCallback) {
michael@0 1745 if (!this.initialized)
michael@0 1746 this.openConnection();
michael@0 1747
michael@0 1748 let gatherResults = () => {
michael@0 1749 let result = {};
michael@0 1750 for (let [key, value] of this.DB.addons)
michael@0 1751 result[key] = value;
michael@0 1752
michael@0 1753 executeSoon(function() aCallback(result));
michael@0 1754 };
michael@0 1755
michael@0 1756 if (this._migrationInProgress)
michael@0 1757 this._postMigrationCallback = gatherResults;
michael@0 1758 else
michael@0 1759 gatherResults();
michael@0 1760 },
michael@0 1761
michael@0 1762 /**
michael@0 1763 * Asynchronously repopulates the database so it only contains the
michael@0 1764 * specified add-ons
michael@0 1765 *
michael@0 1766 * @param aAddons
michael@0 1767 * The array of add-ons to repopulate the database with
michael@0 1768 * @param aCallback
michael@0 1769 * An optional callback to call once complete
michael@0 1770 */
michael@0 1771 repopulate: function AD_repopulate(aAddons, aCallback) {
michael@0 1772 this.DB.addons.clear();
michael@0 1773 this.insertAddons(aAddons, aCallback);
michael@0 1774 },
michael@0 1775
michael@0 1776 /**
michael@0 1777 * Asynchronously inserts an array of add-ons into the database
michael@0 1778 *
michael@0 1779 * @param aAddons
michael@0 1780 * The array of add-ons to insert
michael@0 1781 * @param aCallback
michael@0 1782 * An optional callback to call once complete
michael@0 1783 */
michael@0 1784 insertAddons: function AD_insertAddons(aAddons, aCallback) {
michael@0 1785 if (!this.initialized)
michael@0 1786 this.openConnection();
michael@0 1787
michael@0 1788 for (let addon of aAddons) {
michael@0 1789 this._insertAddon(addon);
michael@0 1790 }
michael@0 1791
michael@0 1792 this._saveDBToDisk();
michael@0 1793
michael@0 1794 if (aCallback)
michael@0 1795 executeSoon(aCallback);
michael@0 1796 },
michael@0 1797
michael@0 1798 /**
michael@0 1799 * Inserts an individual add-on into the database. If the add-on already
michael@0 1800 * exists in the database (by id), then the specified add-on will not be
michael@0 1801 * inserted.
michael@0 1802 *
michael@0 1803 * @param aAddon
michael@0 1804 * The add-on to insert into the database
michael@0 1805 * @param aCallback
michael@0 1806 * The callback to call once complete
michael@0 1807 */
michael@0 1808 _insertAddon: function AD__insertAddon(aAddon) {
michael@0 1809 let newAddon = this._parseAddon(aAddon);
michael@0 1810 if (!newAddon ||
michael@0 1811 !newAddon.id ||
michael@0 1812 this.DB.addons.has(newAddon.id))
michael@0 1813 return;
michael@0 1814
michael@0 1815 this.DB.addons.set(newAddon.id, newAddon);
michael@0 1816 },
michael@0 1817
michael@0 1818 /*
michael@0 1819 * Creates an AddonSearchResult by parsing an object structure
michael@0 1820 * retrieved from the DB JSON representation.
michael@0 1821 *
michael@0 1822 * @param aObj
michael@0 1823 * The object to parse
michael@0 1824 * @return Returns an AddonSearchResult object.
michael@0 1825 */
michael@0 1826 _parseAddon: function (aObj) {
michael@0 1827 if (aObj instanceof AddonSearchResult)
michael@0 1828 return aObj;
michael@0 1829
michael@0 1830 let id = aObj.id;
michael@0 1831 if (!aObj.id)
michael@0 1832 return null;
michael@0 1833
michael@0 1834 let addon = new AddonSearchResult(id);
michael@0 1835
michael@0 1836 for (let [expectedProperty,] of Iterator(AddonSearchResult.prototype)) {
michael@0 1837 if (!(expectedProperty in aObj) ||
michael@0 1838 typeof(aObj[expectedProperty]) === "function")
michael@0 1839 continue;
michael@0 1840
michael@0 1841 let value = aObj[expectedProperty];
michael@0 1842
michael@0 1843 try {
michael@0 1844 switch (expectedProperty) {
michael@0 1845 case "sourceURI":
michael@0 1846 addon.sourceURI = value ? NetUtil.newURI(value) : null;
michael@0 1847 break;
michael@0 1848
michael@0 1849 case "creator":
michael@0 1850 addon.creator = value
michael@0 1851 ? this._makeDeveloper(value)
michael@0 1852 : null;
michael@0 1853 break;
michael@0 1854
michael@0 1855 case "updateDate":
michael@0 1856 addon.updateDate = value ? new Date(value) : null;
michael@0 1857 break;
michael@0 1858
michael@0 1859 case "developers":
michael@0 1860 if (!addon.developers) addon.developers = [];
michael@0 1861 for (let developer of value) {
michael@0 1862 addon.developers.push(this._makeDeveloper(developer));
michael@0 1863 }
michael@0 1864 break;
michael@0 1865
michael@0 1866 case "screenshots":
michael@0 1867 if (!addon.screenshots) addon.screenshots = [];
michael@0 1868 for (let screenshot of value) {
michael@0 1869 addon.screenshots.push(this._makeScreenshot(screenshot));
michael@0 1870 }
michael@0 1871 break;
michael@0 1872
michael@0 1873 case "compatibilityOverrides":
michael@0 1874 if (!addon.compatibilityOverrides) addon.compatibilityOverrides = [];
michael@0 1875 for (let override of value) {
michael@0 1876 addon.compatibilityOverrides.push(
michael@0 1877 this._makeCompatOverride(override)
michael@0 1878 );
michael@0 1879 }
michael@0 1880 break;
michael@0 1881
michael@0 1882 case "icons":
michael@0 1883 if (!addon.icons) addon.icons = {};
michael@0 1884 for (let [size, url] of Iterator(aObj.icons)) {
michael@0 1885 addon.icons[size] = url;
michael@0 1886 }
michael@0 1887 break;
michael@0 1888
michael@0 1889 case "iconURL":
michael@0 1890 break;
michael@0 1891
michael@0 1892 default:
michael@0 1893 addon[expectedProperty] = value;
michael@0 1894 }
michael@0 1895 } catch (ex) {
michael@0 1896 logger.warn("Error in parsing property value for " + expectedProperty + " | " + ex);
michael@0 1897 }
michael@0 1898
michael@0 1899 // delete property from obj to indicate we've already
michael@0 1900 // handled it. The remaining public properties will
michael@0 1901 // be stored separately and just passed through to
michael@0 1902 // be written back to the DB.
michael@0 1903 delete aObj[expectedProperty];
michael@0 1904 }
michael@0 1905
michael@0 1906 // Copy remaining properties to a separate object
michael@0 1907 // to prevent accidental access on downgraded versions.
michael@0 1908 // The properties will be merged in the same object
michael@0 1909 // prior to being written back through toJSON.
michael@0 1910 for (let remainingProperty of Object.keys(aObj)) {
michael@0 1911 switch (typeof(aObj[remainingProperty])) {
michael@0 1912 case "boolean":
michael@0 1913 case "number":
michael@0 1914 case "string":
michael@0 1915 case "object":
michael@0 1916 // these types are accepted
michael@0 1917 break;
michael@0 1918 default:
michael@0 1919 continue;
michael@0 1920 }
michael@0 1921
michael@0 1922 if (!remainingProperty.startsWith("_"))
michael@0 1923 addon._unsupportedProperties[remainingProperty] =
michael@0 1924 aObj[remainingProperty];
michael@0 1925 }
michael@0 1926
michael@0 1927 return addon;
michael@0 1928 },
michael@0 1929
michael@0 1930 /**
michael@0 1931 * Write the in-memory DB to disk, after waiting for
michael@0 1932 * the DB_BATCH_TIMEOUT_MS timeout.
michael@0 1933 *
michael@0 1934 * @return Promise A promise that resolves after the
michael@0 1935 * write to disk has completed.
michael@0 1936 */
michael@0 1937 _saveDBToDisk: function() {
michael@0 1938 return this.Writer.saveChanges().then(
michael@0 1939 null,
michael@0 1940 e => logger.error("SaveDBToDisk failed", e));
michael@0 1941 },
michael@0 1942
michael@0 1943 /**
michael@0 1944 * Make a developer object from a vanilla
michael@0 1945 * JS object from the JSON database
michael@0 1946 *
michael@0 1947 * @param aObj
michael@0 1948 * The JS object to use
michael@0 1949 * @return The created developer
michael@0 1950 */
michael@0 1951 _makeDeveloper: function (aObj) {
michael@0 1952 let name = aObj.name;
michael@0 1953 let url = aObj.url;
michael@0 1954 return new AddonManagerPrivate.AddonAuthor(name, url);
michael@0 1955 },
michael@0 1956
michael@0 1957 /**
michael@0 1958 * Make a screenshot object from a vanilla
michael@0 1959 * JS object from the JSON database
michael@0 1960 *
michael@0 1961 * @param aObj
michael@0 1962 * The JS object to use
michael@0 1963 * @return The created screenshot
michael@0 1964 */
michael@0 1965 _makeScreenshot: function (aObj) {
michael@0 1966 let url = aObj.url;
michael@0 1967 let width = aObj.width;
michael@0 1968 let height = aObj.height;
michael@0 1969 let thumbnailURL = aObj.thumbnailURL;
michael@0 1970 let thumbnailWidth = aObj.thumbnailWidth;
michael@0 1971 let thumbnailHeight = aObj.thumbnailHeight;
michael@0 1972 let caption = aObj.caption;
michael@0 1973 return new AddonManagerPrivate.AddonScreenshot(url, width, height, thumbnailURL,
michael@0 1974 thumbnailWidth, thumbnailHeight, caption);
michael@0 1975 },
michael@0 1976
michael@0 1977 /**
michael@0 1978 * Make a CompatibilityOverride from a vanilla
michael@0 1979 * JS object from the JSON database
michael@0 1980 *
michael@0 1981 * @param aObj
michael@0 1982 * The JS object to use
michael@0 1983 * @return The created CompatibilityOverride
michael@0 1984 */
michael@0 1985 _makeCompatOverride: function (aObj) {
michael@0 1986 let type = aObj.type;
michael@0 1987 let minVersion = aObj.minVersion;
michael@0 1988 let maxVersion = aObj.maxVersion;
michael@0 1989 let appID = aObj.appID;
michael@0 1990 let appMinVersion = aObj.appMinVersion;
michael@0 1991 let appMaxVersion = aObj.appMaxVersion;
michael@0 1992 return new AddonManagerPrivate.AddonCompatibilityOverride(type,
michael@0 1993 minVersion,
michael@0 1994 maxVersion,
michael@0 1995 appID,
michael@0 1996 appMinVersion,
michael@0 1997 appMaxVersion);
michael@0 1998 },
michael@0 1999 };
michael@0 2000
michael@0 2001 function executeSoon(aCallback) {
michael@0 2002 Services.tm.mainThread.dispatch(aCallback, Ci.nsIThread.DISPATCH_NORMAL);
michael@0 2003 }

mercurial