michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0:
michael@0: "use strict";
michael@0:
michael@0: const Cc = Components.classes;
michael@0: const Ci = Components.interfaces;
michael@0: const Cu = Components.utils;
michael@0: const Cr = Components.results;
michael@0:
michael@0: Components.utils.import("resource://gre/modules/Services.jsm");
michael@0: Components.utils.import("resource://gre/modules/AddonManager.jsm");
michael@0: Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0:
michael@0: XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
michael@0: "resource://gre/modules/FileUtils.jsm");
michael@0: XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
michael@0: "resource://gre/modules/NetUtil.jsm");
michael@0: XPCOMUtils.defineLazyModuleGetter(this, "OS",
michael@0: "resource://gre/modules/osfile.jsm");
michael@0: XPCOMUtils.defineLazyModuleGetter(this, "DeferredSave",
michael@0: "resource://gre/modules/DeferredSave.jsm");
michael@0: XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository_SQLiteMigrator",
michael@0: "resource://gre/modules/addons/AddonRepository_SQLiteMigrator.jsm");
michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Promise",
michael@0: "resource://gre/modules/Promise.jsm");
michael@0:
michael@0: this.EXPORTED_SYMBOLS = [ "AddonRepository" ];
michael@0:
michael@0: const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled";
michael@0: const PREF_GETADDONS_CACHE_TYPES = "extensions.getAddons.cache.types";
michael@0: const PREF_GETADDONS_CACHE_ID_ENABLED = "extensions.%ID%.getAddons.cache.enabled"
michael@0: const PREF_GETADDONS_BROWSEADDONS = "extensions.getAddons.browseAddons";
michael@0: const PREF_GETADDONS_BYIDS = "extensions.getAddons.get.url";
michael@0: const PREF_GETADDONS_BYIDS_PERFORMANCE = "extensions.getAddons.getWithPerformance.url";
michael@0: const PREF_GETADDONS_BROWSERECOMMENDED = "extensions.getAddons.recommended.browseURL";
michael@0: const PREF_GETADDONS_GETRECOMMENDED = "extensions.getAddons.recommended.url";
michael@0: const PREF_GETADDONS_BROWSESEARCHRESULTS = "extensions.getAddons.search.browseURL";
michael@0: const PREF_GETADDONS_GETSEARCHRESULTS = "extensions.getAddons.search.url";
michael@0: const PREF_GETADDONS_DB_SCHEMA = "extensions.getAddons.databaseSchema"
michael@0:
michael@0: const XMLURI_PARSE_ERROR = "http://www.mozilla.org/newlayout/xml/parsererror.xml";
michael@0:
michael@0: const API_VERSION = "1.5";
michael@0: const DEFAULT_CACHE_TYPES = "extension,theme,locale,dictionary";
michael@0:
michael@0: const KEY_PROFILEDIR = "ProfD";
michael@0: const FILE_DATABASE = "addons.json";
michael@0: const DB_SCHEMA = 5;
michael@0: const DB_MIN_JSON_SCHEMA = 5;
michael@0: const DB_BATCH_TIMEOUT_MS = 50;
michael@0:
michael@0: const BLANK_DB = function() {
michael@0: return {
michael@0: addons: new Map(),
michael@0: schema: DB_SCHEMA
michael@0: };
michael@0: }
michael@0:
michael@0: const TOOLKIT_ID = "toolkit@mozilla.org";
michael@0:
michael@0: Cu.import("resource://gre/modules/Log.jsm");
michael@0: const LOGGER_ID = "addons.repository";
michael@0:
michael@0: // Create a new logger for use by the Addons Repository
michael@0: // (Requires AddonManager.jsm)
michael@0: let logger = Log.repository.getLogger(LOGGER_ID);
michael@0:
michael@0: // A map between XML keys to AddonSearchResult keys for string values
michael@0: // that require no extra parsing from XML
michael@0: const STRING_KEY_MAP = {
michael@0: name: "name",
michael@0: version: "version",
michael@0: homepage: "homepageURL",
michael@0: support: "supportURL"
michael@0: };
michael@0:
michael@0: // A map between XML keys to AddonSearchResult keys for string values
michael@0: // that require parsing from HTML
michael@0: const HTML_KEY_MAP = {
michael@0: summary: "description",
michael@0: description: "fullDescription",
michael@0: developer_comments: "developerComments",
michael@0: eula: "eula"
michael@0: };
michael@0:
michael@0: // A map between XML keys to AddonSearchResult keys for integer values
michael@0: // that require no extra parsing from XML
michael@0: const INTEGER_KEY_MAP = {
michael@0: total_downloads: "totalDownloads",
michael@0: weekly_downloads: "weeklyDownloads",
michael@0: daily_users: "dailyUsers"
michael@0: };
michael@0:
michael@0: // Wrap the XHR factory so that tests can override with a mock
michael@0: let XHRequest = Components.Constructor("@mozilla.org/xmlextras/xmlhttprequest;1",
michael@0: "nsIXMLHttpRequest");
michael@0:
michael@0: function convertHTMLToPlainText(html) {
michael@0: if (!html)
michael@0: return html;
michael@0: var converter = Cc["@mozilla.org/widget/htmlformatconverter;1"].
michael@0: createInstance(Ci.nsIFormatConverter);
michael@0:
michael@0: var input = Cc["@mozilla.org/supports-string;1"].
michael@0: createInstance(Ci.nsISupportsString);
michael@0: input.data = html.replace(/\n/g, "
");
michael@0:
michael@0: var output = {};
michael@0: converter.convert("text/html", input, input.data.length, "text/unicode",
michael@0: output, {});
michael@0:
michael@0: if (output.value instanceof Ci.nsISupportsString)
michael@0: return output.value.data.replace(/\r\n/g, "\n");
michael@0: return html;
michael@0: }
michael@0:
michael@0: function getAddonsToCache(aIds, aCallback) {
michael@0: try {
michael@0: var types = Services.prefs.getCharPref(PREF_GETADDONS_CACHE_TYPES);
michael@0: }
michael@0: catch (e) { }
michael@0: if (!types)
michael@0: types = DEFAULT_CACHE_TYPES;
michael@0:
michael@0: types = types.split(",");
michael@0:
michael@0: AddonManager.getAddonsByIDs(aIds, function getAddonsToCache_getAddonsByIDs(aAddons) {
michael@0: let enabledIds = [];
michael@0: for (var i = 0; i < aIds.length; i++) {
michael@0: var preference = PREF_GETADDONS_CACHE_ID_ENABLED.replace("%ID%", aIds[i]);
michael@0: try {
michael@0: if (!Services.prefs.getBoolPref(preference))
michael@0: continue;
michael@0: } catch(e) {
michael@0: // If the preference doesn't exist caching is enabled by default
michael@0: }
michael@0:
michael@0: // The add-ons manager may not know about this ID yet if it is a pending
michael@0: // install. In that case we'll just cache it regardless
michael@0: if (aAddons[i] && (types.indexOf(aAddons[i].type) == -1))
michael@0: continue;
michael@0:
michael@0: enabledIds.push(aIds[i]);
michael@0: }
michael@0:
michael@0: aCallback(enabledIds);
michael@0: });
michael@0: }
michael@0:
michael@0: function AddonSearchResult(aId) {
michael@0: this.id = aId;
michael@0: this.icons = {};
michael@0: this._unsupportedProperties = {};
michael@0: }
michael@0:
michael@0: AddonSearchResult.prototype = {
michael@0: /**
michael@0: * The ID of the add-on
michael@0: */
michael@0: id: null,
michael@0:
michael@0: /**
michael@0: * The add-on type (e.g. "extension" or "theme")
michael@0: */
michael@0: type: null,
michael@0:
michael@0: /**
michael@0: * The name of the add-on
michael@0: */
michael@0: name: null,
michael@0:
michael@0: /**
michael@0: * The version of the add-on
michael@0: */
michael@0: version: null,
michael@0:
michael@0: /**
michael@0: * The creator of the add-on
michael@0: */
michael@0: creator: null,
michael@0:
michael@0: /**
michael@0: * The developers of the add-on
michael@0: */
michael@0: developers: null,
michael@0:
michael@0: /**
michael@0: * A short description of the add-on
michael@0: */
michael@0: description: null,
michael@0:
michael@0: /**
michael@0: * The full description of the add-on
michael@0: */
michael@0: fullDescription: null,
michael@0:
michael@0: /**
michael@0: * The developer comments for the add-on. This includes any information
michael@0: * that may be helpful to end users that isn't necessarily applicable to
michael@0: * the add-on description (e.g. known major bugs)
michael@0: */
michael@0: developerComments: null,
michael@0:
michael@0: /**
michael@0: * The end-user licensing agreement (EULA) of the add-on
michael@0: */
michael@0: eula: null,
michael@0:
michael@0: /**
michael@0: * The url of the add-on's icon
michael@0: */
michael@0: get iconURL() {
michael@0: return this.icons && this.icons[32];
michael@0: },
michael@0:
michael@0: /**
michael@0: * The URLs of the add-on's icons, as an object with icon size as key
michael@0: */
michael@0: icons: null,
michael@0:
michael@0: /**
michael@0: * An array of screenshot urls for the add-on
michael@0: */
michael@0: screenshots: null,
michael@0:
michael@0: /**
michael@0: * The homepage for the add-on
michael@0: */
michael@0: homepageURL: null,
michael@0:
michael@0: /**
michael@0: * The homepage for the add-on
michael@0: */
michael@0: learnmoreURL: null,
michael@0:
michael@0: /**
michael@0: * The support URL for the add-on
michael@0: */
michael@0: supportURL: null,
michael@0:
michael@0: /**
michael@0: * The contribution url of the add-on
michael@0: */
michael@0: contributionURL: null,
michael@0:
michael@0: /**
michael@0: * The suggested contribution amount
michael@0: */
michael@0: contributionAmount: null,
michael@0:
michael@0: /**
michael@0: * The URL to visit in order to purchase the add-on
michael@0: */
michael@0: purchaseURL: null,
michael@0:
michael@0: /**
michael@0: * The numerical cost of the add-on in some currency, for sorting purposes
michael@0: * only
michael@0: */
michael@0: purchaseAmount: null,
michael@0:
michael@0: /**
michael@0: * The display cost of the add-on, for display purposes only
michael@0: */
michael@0: purchaseDisplayAmount: null,
michael@0:
michael@0: /**
michael@0: * The rating of the add-on, 0-5
michael@0: */
michael@0: averageRating: null,
michael@0:
michael@0: /**
michael@0: * The number of reviews for this add-on
michael@0: */
michael@0: reviewCount: null,
michael@0:
michael@0: /**
michael@0: * The URL to the list of reviews for this add-on
michael@0: */
michael@0: reviewURL: null,
michael@0:
michael@0: /**
michael@0: * The total number of times the add-on was downloaded
michael@0: */
michael@0: totalDownloads: null,
michael@0:
michael@0: /**
michael@0: * The number of times the add-on was downloaded the current week
michael@0: */
michael@0: weeklyDownloads: null,
michael@0:
michael@0: /**
michael@0: * The number of daily users for the add-on
michael@0: */
michael@0: dailyUsers: null,
michael@0:
michael@0: /**
michael@0: * AddonInstall object generated from the add-on XPI url
michael@0: */
michael@0: install: null,
michael@0:
michael@0: /**
michael@0: * nsIURI storing where this add-on was installed from
michael@0: */
michael@0: sourceURI: null,
michael@0:
michael@0: /**
michael@0: * The status of the add-on in the repository (e.g. 4 = "Public")
michael@0: */
michael@0: repositoryStatus: null,
michael@0:
michael@0: /**
michael@0: * The size of the add-on's files in bytes. For an add-on that have not yet
michael@0: * been downloaded this may be an estimated value.
michael@0: */
michael@0: size: null,
michael@0:
michael@0: /**
michael@0: * The Date that the add-on was most recently updated
michael@0: */
michael@0: updateDate: null,
michael@0:
michael@0: /**
michael@0: * True or false depending on whether the add-on is compatible with the
michael@0: * current version of the application
michael@0: */
michael@0: isCompatible: true,
michael@0:
michael@0: /**
michael@0: * True or false depending on whether the add-on is compatible with the
michael@0: * current platform
michael@0: */
michael@0: isPlatformCompatible: true,
michael@0:
michael@0: /**
michael@0: * Array of AddonCompatibilityOverride objects, that describe overrides for
michael@0: * compatibility with an application versions.
michael@0: **/
michael@0: compatibilityOverrides: null,
michael@0:
michael@0: /**
michael@0: * True if the add-on has a secure means of updating
michael@0: */
michael@0: providesUpdatesSecurely: true,
michael@0:
michael@0: /**
michael@0: * The current blocklist state of the add-on
michael@0: */
michael@0: blocklistState: Ci.nsIBlocklistService.STATE_NOT_BLOCKED,
michael@0:
michael@0: /**
michael@0: * True if this add-on cannot be used in the application based on version
michael@0: * compatibility, dependencies and blocklisting
michael@0: */
michael@0: appDisabled: false,
michael@0:
michael@0: /**
michael@0: * True if the user wants this add-on to be disabled
michael@0: */
michael@0: userDisabled: false,
michael@0:
michael@0: /**
michael@0: * Indicates what scope the add-on is installed in, per profile, user,
michael@0: * system or application
michael@0: */
michael@0: scope: AddonManager.SCOPE_PROFILE,
michael@0:
michael@0: /**
michael@0: * True if the add-on is currently functional
michael@0: */
michael@0: isActive: true,
michael@0:
michael@0: /**
michael@0: * A bitfield holding all of the current operations that are waiting to be
michael@0: * performed for this add-on
michael@0: */
michael@0: pendingOperations: AddonManager.PENDING_NONE,
michael@0:
michael@0: /**
michael@0: * A bitfield holding all the the operations that can be performed on
michael@0: * this add-on
michael@0: */
michael@0: permissions: 0,
michael@0:
michael@0: /**
michael@0: * Tests whether this add-on is known to be compatible with a
michael@0: * particular application and platform version.
michael@0: *
michael@0: * @param appVersion
michael@0: * An application version to test against
michael@0: * @param platformVersion
michael@0: * A platform version to test against
michael@0: * @return Boolean representing if the add-on is compatible
michael@0: */
michael@0: isCompatibleWith: function ASR_isCompatibleWith(aAppVerison, aPlatformVersion) {
michael@0: return true;
michael@0: },
michael@0:
michael@0: /**
michael@0: * Starts an update check for this add-on. This will perform
michael@0: * asynchronously and deliver results to the given listener.
michael@0: *
michael@0: * @param aListener
michael@0: * An UpdateListener for the update process
michael@0: * @param aReason
michael@0: * A reason code for performing the update
michael@0: * @param aAppVersion
michael@0: * An application version to check for updates for
michael@0: * @param aPlatformVersion
michael@0: * A platform version to check for updates for
michael@0: */
michael@0: findUpdates: function ASR_findUpdates(aListener, aReason, aAppVersion, aPlatformVersion) {
michael@0: if ("onNoCompatibilityUpdateAvailable" in aListener)
michael@0: aListener.onNoCompatibilityUpdateAvailable(this);
michael@0: if ("onNoUpdateAvailable" in aListener)
michael@0: aListener.onNoUpdateAvailable(this);
michael@0: if ("onUpdateFinished" in aListener)
michael@0: aListener.onUpdateFinished(this);
michael@0: },
michael@0:
michael@0: toJSON: function() {
michael@0: let json = {};
michael@0:
michael@0: for (let [property, value] of Iterator(this)) {
michael@0: if (property.startsWith("_") ||
michael@0: typeof(value) === "function")
michael@0: continue;
michael@0:
michael@0: try {
michael@0: switch (property) {
michael@0: case "sourceURI":
michael@0: json.sourceURI = value ? value.spec : "";
michael@0: break;
michael@0:
michael@0: case "updateDate":
michael@0: json.updateDate = value ? value.getTime() : "";
michael@0: break;
michael@0:
michael@0: default:
michael@0: json[property] = value;
michael@0: }
michael@0: } catch (ex) {
michael@0: logger.warn("Error writing property value for " + property);
michael@0: }
michael@0: }
michael@0:
michael@0: for (let [property, value] of Iterator(this._unsupportedProperties)) {
michael@0: if (!property.startsWith("_"))
michael@0: json[property] = value;
michael@0: }
michael@0:
michael@0: return json;
michael@0: }
michael@0: }
michael@0:
michael@0: /**
michael@0: * The add-on repository is a source of add-ons that can be installed. It can
michael@0: * be searched in three ways. The first takes a list of IDs and returns a
michael@0: * list of the corresponding add-ons. The second returns a list of add-ons that
michael@0: * come highly recommended. This list should change frequently. The third is to
michael@0: * search for specific search terms entered by the user. Searches are
michael@0: * asynchronous and results should be passed to the provided callback object
michael@0: * when complete. The results passed to the callback should only include add-ons
michael@0: * that are compatible with the current application and are not already
michael@0: * installed.
michael@0: */
michael@0: this.AddonRepository = {
michael@0: /**
michael@0: * Whether caching is currently enabled
michael@0: */
michael@0: get cacheEnabled() {
michael@0: // Act as though caching is disabled if there was an unrecoverable error
michael@0: // openning the database.
michael@0: if (!AddonDatabase.databaseOk) {
michael@0: logger.warn("Cache is disabled because database is not OK");
michael@0: return false;
michael@0: }
michael@0:
michael@0: let preference = PREF_GETADDONS_CACHE_ENABLED;
michael@0: let enabled = false;
michael@0: try {
michael@0: enabled = Services.prefs.getBoolPref(preference);
michael@0: } catch(e) {
michael@0: logger.warn("cacheEnabled: Couldn't get pref: " + preference);
michael@0: }
michael@0:
michael@0: return enabled;
michael@0: },
michael@0:
michael@0: // A cache of the add-ons stored in the database
michael@0: _addons: null,
michael@0:
michael@0: // An array of callbacks pending the retrieval of add-ons from AddonDatabase
michael@0: _pendingCallbacks: null,
michael@0:
michael@0: // Whether a migration in currently in progress
michael@0: _migrationInProgress: false,
michael@0:
michael@0: // A callback to be called when migration finishes
michael@0: _postMigrationCallback: null,
michael@0:
michael@0: // Whether a search is currently in progress
michael@0: _searching: false,
michael@0:
michael@0: // XHR associated with the current request
michael@0: _request: null,
michael@0:
michael@0: /*
michael@0: * Addon search results callback object that contains two functions
michael@0: *
michael@0: * searchSucceeded - Called when a search has suceeded.
michael@0: *
michael@0: * @param aAddons
michael@0: * An array of the add-on results. In the case of searching for
michael@0: * specific terms the ordering of results may be determined by
michael@0: * the search provider.
michael@0: * @param aAddonCount
michael@0: * The length of aAddons
michael@0: * @param aTotalResults
michael@0: * The total results actually available in the repository
michael@0: *
michael@0: *
michael@0: * searchFailed - Called when an error occurred when performing a search.
michael@0: */
michael@0: _callback: null,
michael@0:
michael@0: // Maximum number of results to return
michael@0: _maxResults: null,
michael@0:
michael@0: /**
michael@0: * Shut down AddonRepository
michael@0: * return: promise{integer} resolves with the result of flushing
michael@0: * the AddonRepository database
michael@0: */
michael@0: shutdown: function AddonRepo_shutdown() {
michael@0: this.cancelSearch();
michael@0:
michael@0: this._addons = null;
michael@0: this._pendingCallbacks = null;
michael@0: return AddonDatabase.shutdown(false);
michael@0: },
michael@0:
michael@0: /**
michael@0: * Asynchronously get a cached add-on by id. The add-on (or null if the
michael@0: * add-on is not found) is passed to the specified callback. If caching is
michael@0: * disabled, null is passed to the specified callback.
michael@0: *
michael@0: * @param aId
michael@0: * The id of the add-on to get
michael@0: * @param aCallback
michael@0: * The callback to pass the result back to
michael@0: */
michael@0: getCachedAddonByID: function AddonRepo_getCachedAddonByID(aId, aCallback) {
michael@0: if (!aId || !this.cacheEnabled) {
michael@0: aCallback(null);
michael@0: return;
michael@0: }
michael@0:
michael@0: let self = this;
michael@0: function getAddon(aAddons) {
michael@0: aCallback((aId in aAddons) ? aAddons[aId] : null);
michael@0: }
michael@0:
michael@0: if (this._addons == null) {
michael@0: if (this._pendingCallbacks == null) {
michael@0: // Data has not been retrieved from the database, so retrieve it
michael@0: this._pendingCallbacks = [];
michael@0: this._pendingCallbacks.push(getAddon);
michael@0: AddonDatabase.retrieveStoredData(function getCachedAddonByID_retrieveData(aAddons) {
michael@0: let pendingCallbacks = self._pendingCallbacks;
michael@0:
michael@0: // Check if cache was shutdown or deleted before callback was called
michael@0: if (pendingCallbacks == null)
michael@0: return;
michael@0:
michael@0: // Callbacks may want to trigger a other caching operations that may
michael@0: // affect _addons and _pendingCallbacks, so set to final values early
michael@0: self._pendingCallbacks = null;
michael@0: self._addons = aAddons;
michael@0:
michael@0: pendingCallbacks.forEach(function(aCallback) aCallback(aAddons));
michael@0: });
michael@0:
michael@0: return;
michael@0: }
michael@0:
michael@0: // Data is being retrieved from the database, so wait
michael@0: this._pendingCallbacks.push(getAddon);
michael@0: return;
michael@0: }
michael@0:
michael@0: // Data has been retrieved, so immediately return result
michael@0: getAddon(this._addons);
michael@0: },
michael@0:
michael@0: /**
michael@0: * Asynchronously repopulate cache so it only contains the add-ons
michael@0: * corresponding to the specified ids. If caching is disabled,
michael@0: * the cache is completely removed.
michael@0: *
michael@0: * @param aIds
michael@0: * The array of add-on ids to repopulate the cache with
michael@0: * @param aCallback
michael@0: * The optional callback to call once complete
michael@0: * @param aTimeout
michael@0: * (Optional) timeout in milliseconds to abandon the XHR request
michael@0: * if we have not received a response from the server.
michael@0: */
michael@0: repopulateCache: function(aIds, aCallback, aTimeout) {
michael@0: this._repopulateCacheInternal(aIds, aCallback, false, aTimeout);
michael@0: },
michael@0:
michael@0: _repopulateCacheInternal: function (aIds, aCallback, aSendPerformance, aTimeout) {
michael@0: // Always call AddonManager updateAddonRepositoryData after we refill the cache
michael@0: function repopulateAddonManager() {
michael@0: AddonManagerPrivate.updateAddonRepositoryData(aCallback);
michael@0: }
michael@0:
michael@0: logger.debug("Repopulate add-on cache with " + aIds.toSource());
michael@0: // Completely remove cache if caching is not enabled
michael@0: if (!this.cacheEnabled) {
michael@0: logger.debug("Clearing cache because it is disabled");
michael@0: this._addons = null;
michael@0: this._pendingCallbacks = null;
michael@0: AddonDatabase.delete(repopulateAddonManager);
michael@0: return;
michael@0: }
michael@0:
michael@0: let self = this;
michael@0: getAddonsToCache(aIds, function repopulateCache_getAddonsToCache(aAddons) {
michael@0: // Completely remove cache if there are no add-ons to cache
michael@0: if (aAddons.length == 0) {
michael@0: logger.debug("Clearing cache because 0 add-ons were requested");
michael@0: self._addons = null;
michael@0: self._pendingCallbacks = null;
michael@0: AddonDatabase.delete(repopulateAddonManager);
michael@0: return;
michael@0: }
michael@0:
michael@0: self._beginGetAddons(aAddons, {
michael@0: searchSucceeded: function repopulateCacheInternal_searchSucceeded(aAddons) {
michael@0: self._addons = {};
michael@0: aAddons.forEach(function(aAddon) { self._addons[aAddon.id] = aAddon; });
michael@0: AddonDatabase.repopulate(aAddons, repopulateAddonManager);
michael@0: },
michael@0: searchFailed: function repopulateCacheInternal_searchFailed() {
michael@0: logger.warn("Search failed when repopulating cache");
michael@0: repopulateAddonManager();
michael@0: }
michael@0: }, aSendPerformance, aTimeout);
michael@0: });
michael@0: },
michael@0:
michael@0: /**
michael@0: * Asynchronously add add-ons to the cache corresponding to the specified
michael@0: * ids. If caching is disabled, the cache is unchanged and the callback is
michael@0: * immediately called if it is defined.
michael@0: *
michael@0: * @param aIds
michael@0: * The array of add-on ids to add to the cache
michael@0: * @param aCallback
michael@0: * The optional callback to call once complete
michael@0: */
michael@0: cacheAddons: function AddonRepo_cacheAddons(aIds, aCallback) {
michael@0: logger.debug("cacheAddons: enabled " + this.cacheEnabled + " IDs " + aIds.toSource());
michael@0: if (!this.cacheEnabled) {
michael@0: if (aCallback)
michael@0: aCallback();
michael@0: return;
michael@0: }
michael@0:
michael@0: let self = this;
michael@0: getAddonsToCache(aIds, function cacheAddons_getAddonsToCache(aAddons) {
michael@0: // If there are no add-ons to cache, act as if caching is disabled
michael@0: if (aAddons.length == 0) {
michael@0: if (aCallback)
michael@0: aCallback();
michael@0: return;
michael@0: }
michael@0:
michael@0: self.getAddonsByIDs(aAddons, {
michael@0: searchSucceeded: function cacheAddons_searchSucceeded(aAddons) {
michael@0: aAddons.forEach(function(aAddon) { self._addons[aAddon.id] = aAddon; });
michael@0: AddonDatabase.insertAddons(aAddons, aCallback);
michael@0: },
michael@0: searchFailed: function cacheAddons_searchFailed() {
michael@0: logger.warn("Search failed when adding add-ons to cache");
michael@0: if (aCallback)
michael@0: aCallback();
michael@0: }
michael@0: });
michael@0: });
michael@0: },
michael@0:
michael@0: /**
michael@0: * The homepage for visiting this repository. If the corresponding preference
michael@0: * is not defined, defaults to about:blank.
michael@0: */
michael@0: get homepageURL() {
michael@0: let url = this._formatURLPref(PREF_GETADDONS_BROWSEADDONS, {});
michael@0: return (url != null) ? url : "about:blank";
michael@0: },
michael@0:
michael@0: /**
michael@0: * Returns whether this instance is currently performing a search. New
michael@0: * searches will not be performed while this is the case.
michael@0: */
michael@0: get isSearching() {
michael@0: return this._searching;
michael@0: },
michael@0:
michael@0: /**
michael@0: * The url that can be visited to see recommended add-ons in this repository.
michael@0: * If the corresponding preference is not defined, defaults to about:blank.
michael@0: */
michael@0: getRecommendedURL: function AddonRepo_getRecommendedURL() {
michael@0: let url = this._formatURLPref(PREF_GETADDONS_BROWSERECOMMENDED, {});
michael@0: return (url != null) ? url : "about:blank";
michael@0: },
michael@0:
michael@0: /**
michael@0: * Retrieves the url that can be visited to see search results for the given
michael@0: * terms. If the corresponding preference is not defined, defaults to
michael@0: * about:blank.
michael@0: *
michael@0: * @param aSearchTerms
michael@0: * Search terms used to search the repository
michael@0: */
michael@0: getSearchURL: function AddonRepo_getSearchURL(aSearchTerms) {
michael@0: let url = this._formatURLPref(PREF_GETADDONS_BROWSESEARCHRESULTS, {
michael@0: TERMS : encodeURIComponent(aSearchTerms)
michael@0: });
michael@0: return (url != null) ? url : "about:blank";
michael@0: },
michael@0:
michael@0: /**
michael@0: * Cancels the search in progress. If there is no search in progress this
michael@0: * does nothing.
michael@0: */
michael@0: cancelSearch: function AddonRepo_cancelSearch() {
michael@0: this._searching = false;
michael@0: if (this._request) {
michael@0: this._request.abort();
michael@0: this._request = null;
michael@0: }
michael@0: this._callback = null;
michael@0: },
michael@0:
michael@0: /**
michael@0: * Begins a search for add-ons in this repository by ID. Results will be
michael@0: * passed to the given callback.
michael@0: *
michael@0: * @param aIDs
michael@0: * The array of ids to search for
michael@0: * @param aCallback
michael@0: * The callback to pass results to
michael@0: */
michael@0: getAddonsByIDs: function AddonRepo_getAddonsByIDs(aIDs, aCallback) {
michael@0: return this._beginGetAddons(aIDs, aCallback, false);
michael@0: },
michael@0:
michael@0: /**
michael@0: * Begins a search of add-ons, potentially sending performance data.
michael@0: *
michael@0: * @param aIDs
michael@0: * Array of ids to search for.
michael@0: * @param aCallback
michael@0: * Function to pass results to.
michael@0: * @param aSendPerformance
michael@0: * Boolean indicating whether to send performance data with the
michael@0: * request.
michael@0: * @param aTimeout
michael@0: * (Optional) timeout in milliseconds to abandon the XHR request
michael@0: * if we have not received a response from the server.
michael@0: */
michael@0: _beginGetAddons: function(aIDs, aCallback, aSendPerformance, aTimeout) {
michael@0: let ids = aIDs.slice(0);
michael@0:
michael@0: let params = {
michael@0: API_VERSION : API_VERSION,
michael@0: IDS : ids.map(encodeURIComponent).join(',')
michael@0: };
michael@0:
michael@0: let pref = PREF_GETADDONS_BYIDS;
michael@0:
michael@0: if (aSendPerformance) {
michael@0: let type = Services.prefs.getPrefType(PREF_GETADDONS_BYIDS_PERFORMANCE);
michael@0: if (type == Services.prefs.PREF_STRING) {
michael@0: pref = PREF_GETADDONS_BYIDS_PERFORMANCE;
michael@0:
michael@0: let startupInfo = Cc["@mozilla.org/toolkit/app-startup;1"].
michael@0: getService(Ci.nsIAppStartup).
michael@0: getStartupInfo();
michael@0:
michael@0: params.TIME_MAIN = "";
michael@0: params.TIME_FIRST_PAINT = "";
michael@0: params.TIME_SESSION_RESTORED = "";
michael@0: if (startupInfo.process) {
michael@0: if (startupInfo.main) {
michael@0: params.TIME_MAIN = startupInfo.main - startupInfo.process;
michael@0: }
michael@0: if (startupInfo.firstPaint) {
michael@0: params.TIME_FIRST_PAINT = startupInfo.firstPaint -
michael@0: startupInfo.process;
michael@0: }
michael@0: if (startupInfo.sessionRestored) {
michael@0: params.TIME_SESSION_RESTORED = startupInfo.sessionRestored -
michael@0: startupInfo.process;
michael@0: }
michael@0: }
michael@0: }
michael@0: }
michael@0:
michael@0: let url = this._formatURLPref(pref, params);
michael@0:
michael@0: let self = this;
michael@0: function handleResults(aElements, aTotalResults, aCompatData) {
michael@0: // Don't use this._parseAddons() so that, for example,
michael@0: // incompatible add-ons are not filtered out
michael@0: let results = [];
michael@0: for (let i = 0; i < aElements.length && results.length < self._maxResults; i++) {
michael@0: let result = self._parseAddon(aElements[i], null, aCompatData);
michael@0: if (result == null)
michael@0: continue;
michael@0:
michael@0: // Ignore add-on if it wasn't actually requested
michael@0: let idIndex = ids.indexOf(result.addon.id);
michael@0: if (idIndex == -1)
michael@0: continue;
michael@0:
michael@0: results.push(result);
michael@0: // Ignore this add-on from now on
michael@0: ids.splice(idIndex, 1);
michael@0: }
michael@0:
michael@0: // Include any compatibility overrides for addons not hosted by the
michael@0: // remote repository.
michael@0: for each (let addonCompat in aCompatData) {
michael@0: if (addonCompat.hosted)
michael@0: continue;
michael@0:
michael@0: let addon = new AddonSearchResult(addonCompat.id);
michael@0: // Compatibility overrides can only be for extensions.
michael@0: addon.type = "extension";
michael@0: addon.compatibilityOverrides = addonCompat.compatRanges;
michael@0: let result = {
michael@0: addon: addon,
michael@0: xpiURL: null,
michael@0: xpiHash: null
michael@0: };
michael@0: results.push(result);
michael@0: }
michael@0:
michael@0: // aTotalResults irrelevant
michael@0: self._reportSuccess(results, -1);
michael@0: }
michael@0:
michael@0: this._beginSearch(url, ids.length, aCallback, handleResults, aTimeout);
michael@0: },
michael@0:
michael@0: /**
michael@0: * Performs the daily background update check.
michael@0: *
michael@0: * This API both searches for the add-on IDs specified and sends performance
michael@0: * data. It is meant to be called as part of the daily update ping. It should
michael@0: * not be used for any other purpose. Use repopulateCache instead.
michael@0: *
michael@0: * @param aIDs
michael@0: * Array of add-on IDs to repopulate the cache with.
michael@0: * @param aCallback
michael@0: * Function to call when data is received. Function must be an object
michael@0: * with the keys searchSucceeded and searchFailed.
michael@0: */
michael@0: backgroundUpdateCheck: function AddonRepo_backgroundUpdateCheck(aIDs, aCallback) {
michael@0: this._repopulateCacheInternal(aIDs, aCallback, true);
michael@0: },
michael@0:
michael@0: /**
michael@0: * Begins a search for recommended add-ons in this repository. Results will
michael@0: * be passed to the given callback.
michael@0: *
michael@0: * @param aMaxResults
michael@0: * The maximum number of results to return
michael@0: * @param aCallback
michael@0: * The callback to pass results to
michael@0: */
michael@0: retrieveRecommendedAddons: function AddonRepo_retrieveRecommendedAddons(aMaxResults, aCallback) {
michael@0: let url = this._formatURLPref(PREF_GETADDONS_GETRECOMMENDED, {
michael@0: API_VERSION : API_VERSION,
michael@0:
michael@0: // Get twice as many results to account for potential filtering
michael@0: MAX_RESULTS : 2 * aMaxResults
michael@0: });
michael@0:
michael@0: let self = this;
michael@0: function handleResults(aElements, aTotalResults) {
michael@0: self._getLocalAddonIds(function retrieveRecommendedAddons_getLocalAddonIds(aLocalAddonIds) {
michael@0: // aTotalResults irrelevant
michael@0: self._parseAddons(aElements, -1, aLocalAddonIds);
michael@0: });
michael@0: }
michael@0:
michael@0: this._beginSearch(url, aMaxResults, aCallback, handleResults);
michael@0: },
michael@0:
michael@0: /**
michael@0: * Begins a search for add-ons in this repository. Results will be passed to
michael@0: * the given callback.
michael@0: *
michael@0: * @param aSearchTerms
michael@0: * The terms to search for
michael@0: * @param aMaxResults
michael@0: * The maximum number of results to return
michael@0: * @param aCallback
michael@0: * The callback to pass results to
michael@0: */
michael@0: searchAddons: function AddonRepo_searchAddons(aSearchTerms, aMaxResults, aCallback) {
michael@0: let compatMode = "normal";
michael@0: if (!AddonManager.checkCompatibility)
michael@0: compatMode = "ignore";
michael@0: else if (AddonManager.strictCompatibility)
michael@0: compatMode = "strict";
michael@0:
michael@0: let substitutions = {
michael@0: API_VERSION : API_VERSION,
michael@0: TERMS : encodeURIComponent(aSearchTerms),
michael@0: // Get twice as many results to account for potential filtering
michael@0: MAX_RESULTS : 2 * aMaxResults,
michael@0: COMPATIBILITY_MODE : compatMode,
michael@0: };
michael@0:
michael@0: let url = this._formatURLPref(PREF_GETADDONS_GETSEARCHRESULTS, substitutions);
michael@0:
michael@0: let self = this;
michael@0: function handleResults(aElements, aTotalResults) {
michael@0: self._getLocalAddonIds(function searchAddons_getLocalAddonIds(aLocalAddonIds) {
michael@0: self._parseAddons(aElements, aTotalResults, aLocalAddonIds);
michael@0: });
michael@0: }
michael@0:
michael@0: this._beginSearch(url, aMaxResults, aCallback, handleResults);
michael@0: },
michael@0:
michael@0: // Posts results to the callback
michael@0: _reportSuccess: function AddonRepo_reportSuccess(aResults, aTotalResults) {
michael@0: this._searching = false;
michael@0: this._request = null;
michael@0: // The callback may want to trigger a new search so clear references early
michael@0: let addons = [result.addon for each(result in aResults)];
michael@0: let callback = this._callback;
michael@0: this._callback = null;
michael@0: callback.searchSucceeded(addons, addons.length, aTotalResults);
michael@0: },
michael@0:
michael@0: // Notifies the callback of a failure
michael@0: _reportFailure: function AddonRepo_reportFailure() {
michael@0: this._searching = false;
michael@0: this._request = null;
michael@0: // The callback may want to trigger a new search so clear references early
michael@0: let callback = this._callback;
michael@0: this._callback = null;
michael@0: callback.searchFailed();
michael@0: },
michael@0:
michael@0: // Get descendant by unique tag name. Returns null if not unique tag name.
michael@0: _getUniqueDescendant: function AddonRepo_getUniqueDescendant(aElement, aTagName) {
michael@0: let elementsList = aElement.getElementsByTagName(aTagName);
michael@0: return (elementsList.length == 1) ? elementsList[0] : null;
michael@0: },
michael@0:
michael@0: // Get direct descendant by unique tag name.
michael@0: // Returns null if not unique tag name.
michael@0: _getUniqueDirectDescendant: function AddonRepo_getUniqueDirectDescendant(aElement, aTagName) {
michael@0: let elementsList = Array.filter(aElement.children,
michael@0: function arrayFiltering(aChild) aChild.tagName == aTagName);
michael@0: return (elementsList.length == 1) ? elementsList[0] : null;
michael@0: },
michael@0:
michael@0: // Parse out trimmed text content. Returns null if text content empty.
michael@0: _getTextContent: function AddonRepo_getTextContent(aElement) {
michael@0: let textContent = aElement.textContent.trim();
michael@0: return (textContent.length > 0) ? textContent : null;
michael@0: },
michael@0:
michael@0: // Parse out trimmed text content of a descendant with the specified tag name
michael@0: // Returns null if the parsing unsuccessful.
michael@0: _getDescendantTextContent: function AddonRepo_getDescendantTextContent(aElement, aTagName) {
michael@0: let descendant = this._getUniqueDescendant(aElement, aTagName);
michael@0: return (descendant != null) ? this._getTextContent(descendant) : null;
michael@0: },
michael@0:
michael@0: // Parse out trimmed text content of a direct descendant with the specified
michael@0: // tag name.
michael@0: // Returns null if the parsing unsuccessful.
michael@0: _getDirectDescendantTextContent: function AddonRepo_getDirectDescendantTextContent(aElement, aTagName) {
michael@0: let descendant = this._getUniqueDirectDescendant(aElement, aTagName);
michael@0: return (descendant != null) ? this._getTextContent(descendant) : null;
michael@0: },
michael@0:
michael@0: /*
michael@0: * Creates an AddonSearchResult by parsing an element
michael@0: *
michael@0: * @param aElement
michael@0: * The element to parse
michael@0: * @param aSkip
michael@0: * Object containing ids and sourceURIs of add-ons to skip.
michael@0: * @param aCompatData
michael@0: * Array of parsed addon_compatibility elements to accosiate with the
michael@0: * resulting AddonSearchResult. Optional.
michael@0: * @return Result object containing the parsed AddonSearchResult, xpiURL and
michael@0: * xpiHash if the parsing was successful. Otherwise returns null.
michael@0: */
michael@0: _parseAddon: function AddonRepo_parseAddon(aElement, aSkip, aCompatData) {
michael@0: let skipIDs = (aSkip && aSkip.ids) ? aSkip.ids : [];
michael@0: let skipSourceURIs = (aSkip && aSkip.sourceURIs) ? aSkip.sourceURIs : [];
michael@0:
michael@0: let guid = this._getDescendantTextContent(aElement, "guid");
michael@0: if (guid == null || skipIDs.indexOf(guid) != -1)
michael@0: return null;
michael@0:
michael@0: let addon = new AddonSearchResult(guid);
michael@0: let result = {
michael@0: addon: addon,
michael@0: xpiURL: null,
michael@0: xpiHash: null
michael@0: };
michael@0:
michael@0: if (aCompatData && guid in aCompatData)
michael@0: addon.compatibilityOverrides = aCompatData[guid].compatRanges;
michael@0:
michael@0: let self = this;
michael@0: for (let node = aElement.firstChild; node; node = node.nextSibling) {
michael@0: if (!(node instanceof Ci.nsIDOMElement))
michael@0: continue;
michael@0:
michael@0: let localName = node.localName;
michael@0:
michael@0: // Handle case where the wanted string value is located in text content
michael@0: // but only if the content is not empty
michael@0: if (localName in STRING_KEY_MAP) {
michael@0: addon[STRING_KEY_MAP[localName]] = this._getTextContent(node) || addon[STRING_KEY_MAP[localName]];
michael@0: continue;
michael@0: }
michael@0:
michael@0: // Handle case where the wanted string value is html located in text content
michael@0: if (localName in HTML_KEY_MAP) {
michael@0: addon[HTML_KEY_MAP[localName]] = convertHTMLToPlainText(this._getTextContent(node));
michael@0: continue;
michael@0: }
michael@0:
michael@0: // Handle case where the wanted integer value is located in text content
michael@0: if (localName in INTEGER_KEY_MAP) {
michael@0: let value = parseInt(this._getTextContent(node));
michael@0: if (value >= 0)
michael@0: addon[INTEGER_KEY_MAP[localName]] = value;
michael@0: continue;
michael@0: }
michael@0:
michael@0: // Handle cases that aren't as simple as grabbing the text content
michael@0: switch (localName) {
michael@0: case "type":
michael@0: // Map AMO's type id to corresponding string
michael@0: let id = parseInt(node.getAttribute("id"));
michael@0: switch (id) {
michael@0: case 1:
michael@0: addon.type = "extension";
michael@0: break;
michael@0: case 2:
michael@0: addon.type = "theme";
michael@0: break;
michael@0: case 3:
michael@0: addon.type = "dictionary";
michael@0: break;
michael@0: default:
michael@0: logger.warn("Unknown type id when parsing addon: " + id);
michael@0: }
michael@0: break;
michael@0: case "authors":
michael@0: let authorNodes = node.getElementsByTagName("author");
michael@0: for (let authorNode of authorNodes) {
michael@0: let name = self._getDescendantTextContent(authorNode, "name");
michael@0: let link = self._getDescendantTextContent(authorNode, "link");
michael@0: if (name == null || link == null)
michael@0: continue;
michael@0:
michael@0: let author = new AddonManagerPrivate.AddonAuthor(name, link);
michael@0: if (addon.creator == null)
michael@0: addon.creator = author;
michael@0: else {
michael@0: if (addon.developers == null)
michael@0: addon.developers = [];
michael@0:
michael@0: addon.developers.push(author);
michael@0: }
michael@0: }
michael@0: break;
michael@0: case "previews":
michael@0: let previewNodes = node.getElementsByTagName("preview");
michael@0: for (let previewNode of previewNodes) {
michael@0: let full = self._getUniqueDescendant(previewNode, "full");
michael@0: if (full == null)
michael@0: continue;
michael@0:
michael@0: let fullURL = self._getTextContent(full);
michael@0: let fullWidth = full.getAttribute("width");
michael@0: let fullHeight = full.getAttribute("height");
michael@0:
michael@0: let thumbnailURL, thumbnailWidth, thumbnailHeight;
michael@0: let thumbnail = self._getUniqueDescendant(previewNode, "thumbnail");
michael@0: if (thumbnail) {
michael@0: thumbnailURL = self._getTextContent(thumbnail);
michael@0: thumbnailWidth = thumbnail.getAttribute("width");
michael@0: thumbnailHeight = thumbnail.getAttribute("height");
michael@0: }
michael@0: let caption = self._getDescendantTextContent(previewNode, "caption");
michael@0: let screenshot = new AddonManagerPrivate.AddonScreenshot(fullURL, fullWidth, fullHeight,
michael@0: thumbnailURL, thumbnailWidth,
michael@0: thumbnailHeight, caption);
michael@0:
michael@0: if (addon.screenshots == null)
michael@0: addon.screenshots = [];
michael@0:
michael@0: if (previewNode.getAttribute("primary") == 1)
michael@0: addon.screenshots.unshift(screenshot);
michael@0: else
michael@0: addon.screenshots.push(screenshot);
michael@0: }
michael@0: break;
michael@0: case "learnmore":
michael@0: addon.learnmoreURL = this._getTextContent(node);
michael@0: addon.homepageURL = addon.homepageURL || addon.learnmoreURL;
michael@0: break;
michael@0: case "contribution_data":
michael@0: let meetDevelopers = this._getDescendantTextContent(node, "meet_developers");
michael@0: let suggestedAmount = this._getDescendantTextContent(node, "suggested_amount");
michael@0: if (meetDevelopers != null) {
michael@0: addon.contributionURL = meetDevelopers;
michael@0: addon.contributionAmount = suggestedAmount;
michael@0: }
michael@0: break
michael@0: case "payment_data":
michael@0: let link = this._getDescendantTextContent(node, "link");
michael@0: let amountTag = this._getUniqueDescendant(node, "amount");
michael@0: let amount = parseFloat(amountTag.getAttribute("amount"));
michael@0: let displayAmount = this._getTextContent(amountTag);
michael@0: if (link != null && amount != null && displayAmount != null) {
michael@0: addon.purchaseURL = link;
michael@0: addon.purchaseAmount = amount;
michael@0: addon.purchaseDisplayAmount = displayAmount;
michael@0: }
michael@0: break
michael@0: case "rating":
michael@0: let averageRating = parseInt(this._getTextContent(node));
michael@0: if (averageRating >= 0)
michael@0: addon.averageRating = Math.min(5, averageRating);
michael@0: break;
michael@0: case "reviews":
michael@0: let url = this._getTextContent(node);
michael@0: let num = parseInt(node.getAttribute("num"));
michael@0: if (url != null && num >= 0) {
michael@0: addon.reviewURL = url;
michael@0: addon.reviewCount = num;
michael@0: }
michael@0: break;
michael@0: case "status":
michael@0: let repositoryStatus = parseInt(node.getAttribute("id"));
michael@0: if (!isNaN(repositoryStatus))
michael@0: addon.repositoryStatus = repositoryStatus;
michael@0: break;
michael@0: case "all_compatible_os":
michael@0: let nodes = node.getElementsByTagName("os");
michael@0: addon.isPlatformCompatible = Array.some(nodes, function parseAddon_platformCompatFilter(aNode) {
michael@0: let text = aNode.textContent.toLowerCase().trim();
michael@0: return text == "all" || text == Services.appinfo.OS.toLowerCase();
michael@0: });
michael@0: break;
michael@0: case "install":
michael@0: // No os attribute means the xpi is compatible with any os
michael@0: if (node.hasAttribute("os")) {
michael@0: let os = node.getAttribute("os").trim().toLowerCase();
michael@0: // If the os is not ALL and not the current OS then ignore this xpi
michael@0: if (os != "all" && os != Services.appinfo.OS.toLowerCase())
michael@0: break;
michael@0: }
michael@0:
michael@0: let xpiURL = this._getTextContent(node);
michael@0: if (xpiURL == null)
michael@0: break;
michael@0:
michael@0: if (skipSourceURIs.indexOf(xpiURL) != -1)
michael@0: return null;
michael@0:
michael@0: result.xpiURL = xpiURL;
michael@0: addon.sourceURI = NetUtil.newURI(xpiURL);
michael@0:
michael@0: let size = parseInt(node.getAttribute("size"));
michael@0: addon.size = (size >= 0) ? size : null;
michael@0:
michael@0: let xpiHash = node.getAttribute("hash");
michael@0: if (xpiHash != null)
michael@0: xpiHash = xpiHash.trim();
michael@0: result.xpiHash = xpiHash ? xpiHash : null;
michael@0: break;
michael@0: case "last_updated":
michael@0: let epoch = parseInt(node.getAttribute("epoch"));
michael@0: if (!isNaN(epoch))
michael@0: addon.updateDate = new Date(1000 * epoch);
michael@0: break;
michael@0: case "icon":
michael@0: addon.icons[node.getAttribute("size")] = this._getTextContent(node);
michael@0: break;
michael@0: }
michael@0: }
michael@0:
michael@0: return result;
michael@0: },
michael@0:
michael@0: _parseAddons: function AddonRepo_parseAddons(aElements, aTotalResults, aSkip) {
michael@0: let self = this;
michael@0: let results = [];
michael@0:
michael@0: function isSameApplication(aAppNode) {
michael@0: return self._getTextContent(aAppNode) == Services.appinfo.ID;
michael@0: }
michael@0:
michael@0: for (let i = 0; i < aElements.length && results.length < this._maxResults; i++) {
michael@0: let element = aElements[i];
michael@0:
michael@0: let tags = this._getUniqueDescendant(element, "compatible_applications");
michael@0: if (tags == null)
michael@0: continue;
michael@0:
michael@0: let applications = tags.getElementsByTagName("appID");
michael@0: let compatible = Array.some(applications, function parseAddons_applicationsCompatFilter(aAppNode) {
michael@0: if (!isSameApplication(aAppNode))
michael@0: return false;
michael@0:
michael@0: let parent = aAppNode.parentNode;
michael@0: let minVersion = self._getDescendantTextContent(parent, "min_version");
michael@0: let maxVersion = self._getDescendantTextContent(parent, "max_version");
michael@0: if (minVersion == null || maxVersion == null)
michael@0: return false;
michael@0:
michael@0: let currentVersion = Services.appinfo.version;
michael@0: return (Services.vc.compare(minVersion, currentVersion) <= 0 &&
michael@0: ((!AddonManager.strictCompatibility) ||
michael@0: Services.vc.compare(currentVersion, maxVersion) <= 0));
michael@0: });
michael@0:
michael@0: // Ignore add-ons not compatible with this Application
michael@0: if (!compatible) {
michael@0: if (AddonManager.checkCompatibility)
michael@0: continue;
michael@0:
michael@0: if (!Array.some(applications, isSameApplication))
michael@0: continue;
michael@0: }
michael@0:
michael@0: // Add-on meets all requirements, so parse out data.
michael@0: // Don't pass in compatiblity override data, because that's only returned
michael@0: // in GUID searches, which don't use _parseAddons().
michael@0: let result = this._parseAddon(element, aSkip);
michael@0: if (result == null)
michael@0: continue;
michael@0:
michael@0: // Ignore add-on missing a required attribute
michael@0: let requiredAttributes = ["id", "name", "version", "type", "creator"];
michael@0: if (requiredAttributes.some(function parseAddons_attributeFilter(aAttribute) !result.addon[aAttribute]))
michael@0: continue;
michael@0:
michael@0: // Add only if the add-on is compatible with the platform
michael@0: if (!result.addon.isPlatformCompatible)
michael@0: continue;
michael@0:
michael@0: // Add only if there was an xpi compatible with this OS or there was a
michael@0: // way to purchase the add-on
michael@0: if (!result.xpiURL && !result.addon.purchaseURL)
michael@0: continue;
michael@0:
michael@0: result.addon.isCompatible = compatible;
michael@0:
michael@0: results.push(result);
michael@0: // Ignore this add-on from now on by adding it to the skip array
michael@0: aSkip.ids.push(result.addon.id);
michael@0: }
michael@0:
michael@0: // Immediately report success if no AddonInstall instances to create
michael@0: let pendingResults = results.length;
michael@0: if (pendingResults == 0) {
michael@0: this._reportSuccess(results, aTotalResults);
michael@0: return;
michael@0: }
michael@0:
michael@0: // Create an AddonInstall for each result
michael@0: let self = this;
michael@0: results.forEach(function(aResult) {
michael@0: let addon = aResult.addon;
michael@0: let callback = function addonInstallCallback(aInstall) {
michael@0: addon.install = aInstall;
michael@0: pendingResults--;
michael@0: if (pendingResults == 0)
michael@0: self._reportSuccess(results, aTotalResults);
michael@0: }
michael@0:
michael@0: if (aResult.xpiURL) {
michael@0: AddonManager.getInstallForURL(aResult.xpiURL, callback,
michael@0: "application/x-xpinstall", aResult.xpiHash,
michael@0: addon.name, addon.icons, addon.version);
michael@0: }
michael@0: else {
michael@0: callback(null);
michael@0: }
michael@0: });
michael@0: },
michael@0:
michael@0: // Parses addon_compatibility nodes, that describe compatibility overrides.
michael@0: _parseAddonCompatElement: function AddonRepo_parseAddonCompatElement(aResultObj, aElement) {
michael@0: let guid = this._getDescendantTextContent(aElement, "guid");
michael@0: if (!guid) {
michael@0: logger.debug("Compatibility override is missing guid.");
michael@0: return;
michael@0: }
michael@0:
michael@0: let compat = {id: guid};
michael@0: compat.hosted = aElement.getAttribute("hosted") != "false";
michael@0:
michael@0: function findMatchingAppRange(aNodes) {
michael@0: let toolkitAppRange = null;
michael@0: for (let node of aNodes) {
michael@0: let appID = this._getDescendantTextContent(node, "appID");
michael@0: if (appID != Services.appinfo.ID && appID != TOOLKIT_ID)
michael@0: continue;
michael@0:
michael@0: let minVersion = this._getDescendantTextContent(node, "min_version");
michael@0: let maxVersion = this._getDescendantTextContent(node, "max_version");
michael@0: if (minVersion == null || maxVersion == null)
michael@0: continue;
michael@0:
michael@0: let appRange = { appID: appID,
michael@0: appMinVersion: minVersion,
michael@0: appMaxVersion: maxVersion };
michael@0:
michael@0: // Only use Toolkit app ranges if no ranges match the application ID.
michael@0: if (appID == TOOLKIT_ID)
michael@0: toolkitAppRange = appRange;
michael@0: else
michael@0: return appRange;
michael@0: }
michael@0: return toolkitAppRange;
michael@0: }
michael@0:
michael@0: function parseRangeNode(aNode) {
michael@0: let type = aNode.getAttribute("type");
michael@0: // Only "incompatible" (blacklisting) is supported for now.
michael@0: if (type != "incompatible") {
michael@0: logger.debug("Compatibility override of unsupported type found.");
michael@0: return null;
michael@0: }
michael@0:
michael@0: let override = new AddonManagerPrivate.AddonCompatibilityOverride(type);
michael@0:
michael@0: override.minVersion = this._getDirectDescendantTextContent(aNode, "min_version");
michael@0: override.maxVersion = this._getDirectDescendantTextContent(aNode, "max_version");
michael@0:
michael@0: if (!override.minVersion) {
michael@0: logger.debug("Compatibility override is missing min_version.");
michael@0: return null;
michael@0: }
michael@0: if (!override.maxVersion) {
michael@0: logger.debug("Compatibility override is missing max_version.");
michael@0: return null;
michael@0: }
michael@0:
michael@0: let appRanges = aNode.querySelectorAll("compatible_applications > application");
michael@0: let appRange = findMatchingAppRange.bind(this)(appRanges);
michael@0: if (!appRange) {
michael@0: logger.debug("Compatibility override is missing a valid application range.");
michael@0: return null;
michael@0: }
michael@0:
michael@0: override.appID = appRange.appID;
michael@0: override.appMinVersion = appRange.appMinVersion;
michael@0: override.appMaxVersion = appRange.appMaxVersion;
michael@0:
michael@0: return override;
michael@0: }
michael@0:
michael@0: let rangeNodes = aElement.querySelectorAll("version_ranges > version_range");
michael@0: compat.compatRanges = Array.map(rangeNodes, parseRangeNode.bind(this))
michael@0: .filter(function compatRangesFilter(aItem) !!aItem);
michael@0: if (compat.compatRanges.length == 0)
michael@0: return;
michael@0:
michael@0: aResultObj[compat.id] = compat;
michael@0: },
michael@0:
michael@0: // Parses addon_compatibility elements.
michael@0: _parseAddonCompatData: function AddonRepo_parseAddonCompatData(aElements) {
michael@0: let compatData = {};
michael@0: Array.forEach(aElements, this._parseAddonCompatElement.bind(this, compatData));
michael@0: return compatData;
michael@0: },
michael@0:
michael@0: // Begins a new search if one isn't currently executing
michael@0: _beginSearch: function(aURI, aMaxResults, aCallback, aHandleResults, aTimeout) {
michael@0: if (this._searching || aURI == null || aMaxResults <= 0) {
michael@0: logger.warn("AddonRepository search failed: searching " + this._searching + " aURI " + aURI +
michael@0: " aMaxResults " + aMaxResults);
michael@0: aCallback.searchFailed();
michael@0: return;
michael@0: }
michael@0:
michael@0: this._searching = true;
michael@0: this._callback = aCallback;
michael@0: this._maxResults = aMaxResults;
michael@0:
michael@0: logger.debug("Requesting " + aURI);
michael@0:
michael@0: this._request = new XHRequest();
michael@0: this._request.mozBackgroundRequest = true;
michael@0: this._request.open("GET", aURI, true);
michael@0: this._request.overrideMimeType("text/xml");
michael@0: if (aTimeout) {
michael@0: this._request.timeout = aTimeout;
michael@0: }
michael@0:
michael@0: this._request.addEventListener("error", aEvent => this._reportFailure(), false);
michael@0: this._request.addEventListener("timeout", aEvent => this._reportFailure(), false);
michael@0: this._request.addEventListener("load", aEvent => {
michael@0: logger.debug("Got metadata search load event");
michael@0: let request = aEvent.target;
michael@0: let responseXML = request.responseXML;
michael@0:
michael@0: if (!responseXML || responseXML.documentElement.namespaceURI == XMLURI_PARSE_ERROR ||
michael@0: (request.status != 200 && request.status != 0)) {
michael@0: this._reportFailure();
michael@0: return;
michael@0: }
michael@0:
michael@0: let documentElement = responseXML.documentElement;
michael@0: let elements = documentElement.getElementsByTagName("addon");
michael@0: let totalResults = elements.length;
michael@0: let parsedTotalResults = parseInt(documentElement.getAttribute("total_results"));
michael@0: // Parsed value of total results only makes sense if >= elements.length
michael@0: if (parsedTotalResults >= totalResults)
michael@0: totalResults = parsedTotalResults;
michael@0:
michael@0: let compatElements = documentElement.getElementsByTagName("addon_compatibility");
michael@0: let compatData = this._parseAddonCompatData(compatElements);
michael@0:
michael@0: aHandleResults(elements, totalResults, compatData);
michael@0: }, false);
michael@0: this._request.send(null);
michael@0: },
michael@0:
michael@0: // Gets the id's of local add-ons, and the sourceURI's of local installs,
michael@0: // passing the results to aCallback
michael@0: _getLocalAddonIds: function AddonRepo_getLocalAddonIds(aCallback) {
michael@0: let self = this;
michael@0: let localAddonIds = {ids: null, sourceURIs: null};
michael@0:
michael@0: AddonManager.getAllAddons(function getLocalAddonIds_getAllAddons(aAddons) {
michael@0: localAddonIds.ids = [a.id for each (a in aAddons)];
michael@0: if (localAddonIds.sourceURIs)
michael@0: aCallback(localAddonIds);
michael@0: });
michael@0:
michael@0: AddonManager.getAllInstalls(function getLocalAddonIds_getAllInstalls(aInstalls) {
michael@0: localAddonIds.sourceURIs = [];
michael@0: aInstalls.forEach(function(aInstall) {
michael@0: if (aInstall.state != AddonManager.STATE_AVAILABLE)
michael@0: localAddonIds.sourceURIs.push(aInstall.sourceURI.spec);
michael@0: });
michael@0:
michael@0: if (localAddonIds.ids)
michael@0: aCallback(localAddonIds);
michael@0: });
michael@0: },
michael@0:
michael@0: // Create url from preference, returning null if preference does not exist
michael@0: _formatURLPref: function AddonRepo_formatURLPref(aPreference, aSubstitutions) {
michael@0: let url = null;
michael@0: try {
michael@0: url = Services.prefs.getCharPref(aPreference);
michael@0: } catch(e) {
michael@0: logger.warn("_formatURLPref: Couldn't get pref: " + aPreference);
michael@0: return null;
michael@0: }
michael@0:
michael@0: url = url.replace(/%([A-Z_]+)%/g, function urlSubstitution(aMatch, aKey) {
michael@0: return (aKey in aSubstitutions) ? aSubstitutions[aKey] : aMatch;
michael@0: });
michael@0:
michael@0: return Services.urlFormatter.formatURL(url);
michael@0: },
michael@0:
michael@0: // Find a AddonCompatibilityOverride that matches a given aAddonVersion and
michael@0: // application/platform version.
michael@0: findMatchingCompatOverride: function AddonRepo_findMatchingCompatOverride(aAddonVersion,
michael@0: aCompatOverrides,
michael@0: aAppVersion,
michael@0: aPlatformVersion) {
michael@0: for (let override of aCompatOverrides) {
michael@0:
michael@0: let appVersion = null;
michael@0: if (override.appID == TOOLKIT_ID)
michael@0: appVersion = aPlatformVersion || Services.appinfo.platformVersion;
michael@0: else
michael@0: appVersion = aAppVersion || Services.appinfo.version;
michael@0:
michael@0: if (Services.vc.compare(override.minVersion, aAddonVersion) <= 0 &&
michael@0: Services.vc.compare(aAddonVersion, override.maxVersion) <= 0 &&
michael@0: Services.vc.compare(override.appMinVersion, appVersion) <= 0 &&
michael@0: Services.vc.compare(appVersion, override.appMaxVersion) <= 0) {
michael@0: return override;
michael@0: }
michael@0: }
michael@0: return null;
michael@0: },
michael@0:
michael@0: flush: function() {
michael@0: return AddonDatabase.flush();
michael@0: }
michael@0: };
michael@0:
michael@0: var AddonDatabase = {
michael@0: // true if the database connection has been opened
michael@0: initialized: false,
michael@0: // false if there was an unrecoverable error openning the database
michael@0: databaseOk: true,
michael@0:
michael@0: // the in-memory database
michael@0: DB: BLANK_DB(),
michael@0:
michael@0: /**
michael@0: * A getter to retrieve an nsIFile pointer to the DB
michael@0: */
michael@0: get jsonFile() {
michael@0: delete this.jsonFile;
michael@0: return this.jsonFile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_DATABASE], true);
michael@0: },
michael@0:
michael@0: /**
michael@0: * Synchronously opens a new connection to the database file.
michael@0: */
michael@0: openConnection: function() {
michael@0: this.DB = BLANK_DB();
michael@0: this.initialized = true;
michael@0: delete this.connection;
michael@0:
michael@0: let inputDB, fstream, cstream, schema;
michael@0:
michael@0: try {
michael@0: let data = "";
michael@0: fstream = Cc["@mozilla.org/network/file-input-stream;1"]
michael@0: .createInstance(Ci.nsIFileInputStream);
michael@0: cstream = Cc["@mozilla.org/intl/converter-input-stream;1"]
michael@0: .createInstance(Ci.nsIConverterInputStream);
michael@0:
michael@0: fstream.init(this.jsonFile, -1, 0, 0);
michael@0: cstream.init(fstream, "UTF-8", 0, 0);
michael@0: let (str = {}) {
michael@0: let read = 0;
michael@0: do {
michael@0: read = cstream.readString(0xffffffff, str); // read as much as we can and put it in str.value
michael@0: data += str.value;
michael@0: } while (read != 0);
michael@0: }
michael@0:
michael@0: inputDB = JSON.parse(data);
michael@0:
michael@0: if (!inputDB.hasOwnProperty("addons") ||
michael@0: !Array.isArray(inputDB.addons)) {
michael@0: throw new Error("No addons array.");
michael@0: }
michael@0:
michael@0: if (!inputDB.hasOwnProperty("schema")) {
michael@0: throw new Error("No schema specified.");
michael@0: }
michael@0:
michael@0: schema = parseInt(inputDB.schema, 10);
michael@0:
michael@0: if (!Number.isInteger(schema) ||
michael@0: schema < DB_MIN_JSON_SCHEMA) {
michael@0: throw new Error("Invalid schema value.");
michael@0: }
michael@0:
michael@0: } catch (e if e.result == Cr.NS_ERROR_FILE_NOT_FOUND) {
michael@0: logger.debug("No " + FILE_DATABASE + " found.");
michael@0:
michael@0: // Create a blank addons.json file
michael@0: this._saveDBToDisk();
michael@0:
michael@0: let dbSchema = 0;
michael@0: try {
michael@0: dbSchema = Services.prefs.getIntPref(PREF_GETADDONS_DB_SCHEMA);
michael@0: } catch (e) {}
michael@0:
michael@0: if (dbSchema < DB_MIN_JSON_SCHEMA) {
michael@0: this._migrationInProgress = AddonRepository_SQLiteMigrator.migrate((results) => {
michael@0: if (results.length)
michael@0: this.insertAddons(results);
michael@0:
michael@0: if (this._postMigrationCallback) {
michael@0: this._postMigrationCallback();
michael@0: this._postMigrationCallback = null;
michael@0: }
michael@0:
michael@0: this._migrationInProgress = false;
michael@0: });
michael@0:
michael@0: Services.prefs.setIntPref(PREF_GETADDONS_DB_SCHEMA, DB_SCHEMA);
michael@0: }
michael@0:
michael@0: return;
michael@0:
michael@0: } catch (e) {
michael@0: logger.error("Malformed " + FILE_DATABASE + ": " + e);
michael@0: this.databaseOk = false;
michael@0: return;
michael@0:
michael@0: } finally {
michael@0: cstream.close();
michael@0: fstream.close();
michael@0: }
michael@0:
michael@0: Services.prefs.setIntPref(PREF_GETADDONS_DB_SCHEMA, DB_SCHEMA);
michael@0:
michael@0: // We use _insertAddon manually instead of calling
michael@0: // insertAddons to avoid the write to disk which would
michael@0: // be a waste since this is the data that was just read.
michael@0: for (let addon of inputDB.addons) {
michael@0: this._insertAddon(addon);
michael@0: }
michael@0: },
michael@0:
michael@0: /**
michael@0: * A lazy getter for the database connection.
michael@0: */
michael@0: get connection() {
michael@0: return this.openConnection();
michael@0: },
michael@0:
michael@0: /**
michael@0: * Asynchronously shuts down the database connection and releases all
michael@0: * cached objects
michael@0: *
michael@0: * @param aCallback
michael@0: * An optional callback to call once complete
michael@0: * @param aSkipFlush
michael@0: * An optional boolean to skip flushing data to disk. Useful
michael@0: * when the database is going to be deleted afterwards.
michael@0: */
michael@0: shutdown: function AD_shutdown(aSkipFlush) {
michael@0: this.databaseOk = true;
michael@0:
michael@0: if (!this.initialized) {
michael@0: return Promise.resolve(0);
michael@0: }
michael@0:
michael@0: this.initialized = false;
michael@0:
michael@0: this.__defineGetter__("connection", function shutdown_connectionGetter() {
michael@0: return this.openConnection();
michael@0: });
michael@0:
michael@0: if (aSkipFlush) {
michael@0: return Promise.resolve(0);
michael@0: } else {
michael@0: return this.Writer.flush();
michael@0: }
michael@0: },
michael@0:
michael@0: /**
michael@0: * Asynchronously deletes the database, shutting down the connection
michael@0: * first if initialized
michael@0: *
michael@0: * @param aCallback
michael@0: * An optional callback to call once complete
michael@0: */
michael@0: delete: function AD_delete(aCallback) {
michael@0: this.DB = BLANK_DB();
michael@0:
michael@0: this._deleting = this.Writer.flush()
michael@0: .then(null, () => {})
michael@0: // shutdown(true) never rejects
michael@0: .then(() => this.shutdown(true))
michael@0: .then(() => OS.File.remove(this.jsonFile.path, {}))
michael@0: .then(null, error => logger.error("Unable to delete Addon Repository file " +
michael@0: this.jsonFile.path, error))
michael@0: .then(() => this._deleting = null)
michael@0: .then(aCallback);
michael@0: },
michael@0:
michael@0: toJSON: function AD_toJSON() {
michael@0: let json = {
michael@0: schema: this.DB.schema,
michael@0: addons: []
michael@0: }
michael@0:
michael@0: for (let [, value] of this.DB.addons)
michael@0: json.addons.push(value);
michael@0:
michael@0: return json;
michael@0: },
michael@0:
michael@0: /*
michael@0: * This is a deferred task writer that is used
michael@0: * to batch operations done within 50ms of each
michael@0: * other and thus generating only one write to disk
michael@0: */
michael@0: get Writer() {
michael@0: delete this.Writer;
michael@0: this.Writer = new DeferredSave(
michael@0: this.jsonFile.path,
michael@0: () => { return JSON.stringify(this); },
michael@0: DB_BATCH_TIMEOUT_MS
michael@0: );
michael@0: return this.Writer;
michael@0: },
michael@0:
michael@0: /**
michael@0: * Flush any pending I/O on the addons.json file
michael@0: * @return: Promise{null}
michael@0: * Resolves when the pending I/O (writing out or deleting
michael@0: * addons.json) completes
michael@0: */
michael@0: flush: function() {
michael@0: if (this._deleting) {
michael@0: return this._deleting;
michael@0: }
michael@0: return this.Writer.flush();
michael@0: },
michael@0:
michael@0: /**
michael@0: * Asynchronously retrieve all add-ons from the database, and pass it
michael@0: * to the specified callback
michael@0: *
michael@0: * @param aCallback
michael@0: * The callback to pass the add-ons back to
michael@0: */
michael@0: retrieveStoredData: function AD_retrieveStoredData(aCallback) {
michael@0: if (!this.initialized)
michael@0: this.openConnection();
michael@0:
michael@0: let gatherResults = () => {
michael@0: let result = {};
michael@0: for (let [key, value] of this.DB.addons)
michael@0: result[key] = value;
michael@0:
michael@0: executeSoon(function() aCallback(result));
michael@0: };
michael@0:
michael@0: if (this._migrationInProgress)
michael@0: this._postMigrationCallback = gatherResults;
michael@0: else
michael@0: gatherResults();
michael@0: },
michael@0:
michael@0: /**
michael@0: * Asynchronously repopulates the database so it only contains the
michael@0: * specified add-ons
michael@0: *
michael@0: * @param aAddons
michael@0: * The array of add-ons to repopulate the database with
michael@0: * @param aCallback
michael@0: * An optional callback to call once complete
michael@0: */
michael@0: repopulate: function AD_repopulate(aAddons, aCallback) {
michael@0: this.DB.addons.clear();
michael@0: this.insertAddons(aAddons, aCallback);
michael@0: },
michael@0:
michael@0: /**
michael@0: * Asynchronously inserts an array of add-ons into the database
michael@0: *
michael@0: * @param aAddons
michael@0: * The array of add-ons to insert
michael@0: * @param aCallback
michael@0: * An optional callback to call once complete
michael@0: */
michael@0: insertAddons: function AD_insertAddons(aAddons, aCallback) {
michael@0: if (!this.initialized)
michael@0: this.openConnection();
michael@0:
michael@0: for (let addon of aAddons) {
michael@0: this._insertAddon(addon);
michael@0: }
michael@0:
michael@0: this._saveDBToDisk();
michael@0:
michael@0: if (aCallback)
michael@0: executeSoon(aCallback);
michael@0: },
michael@0:
michael@0: /**
michael@0: * Inserts an individual add-on into the database. If the add-on already
michael@0: * exists in the database (by id), then the specified add-on will not be
michael@0: * inserted.
michael@0: *
michael@0: * @param aAddon
michael@0: * The add-on to insert into the database
michael@0: * @param aCallback
michael@0: * The callback to call once complete
michael@0: */
michael@0: _insertAddon: function AD__insertAddon(aAddon) {
michael@0: let newAddon = this._parseAddon(aAddon);
michael@0: if (!newAddon ||
michael@0: !newAddon.id ||
michael@0: this.DB.addons.has(newAddon.id))
michael@0: return;
michael@0:
michael@0: this.DB.addons.set(newAddon.id, newAddon);
michael@0: },
michael@0:
michael@0: /*
michael@0: * Creates an AddonSearchResult by parsing an object structure
michael@0: * retrieved from the DB JSON representation.
michael@0: *
michael@0: * @param aObj
michael@0: * The object to parse
michael@0: * @return Returns an AddonSearchResult object.
michael@0: */
michael@0: _parseAddon: function (aObj) {
michael@0: if (aObj instanceof AddonSearchResult)
michael@0: return aObj;
michael@0:
michael@0: let id = aObj.id;
michael@0: if (!aObj.id)
michael@0: return null;
michael@0:
michael@0: let addon = new AddonSearchResult(id);
michael@0:
michael@0: for (let [expectedProperty,] of Iterator(AddonSearchResult.prototype)) {
michael@0: if (!(expectedProperty in aObj) ||
michael@0: typeof(aObj[expectedProperty]) === "function")
michael@0: continue;
michael@0:
michael@0: let value = aObj[expectedProperty];
michael@0:
michael@0: try {
michael@0: switch (expectedProperty) {
michael@0: case "sourceURI":
michael@0: addon.sourceURI = value ? NetUtil.newURI(value) : null;
michael@0: break;
michael@0:
michael@0: case "creator":
michael@0: addon.creator = value
michael@0: ? this._makeDeveloper(value)
michael@0: : null;
michael@0: break;
michael@0:
michael@0: case "updateDate":
michael@0: addon.updateDate = value ? new Date(value) : null;
michael@0: break;
michael@0:
michael@0: case "developers":
michael@0: if (!addon.developers) addon.developers = [];
michael@0: for (let developer of value) {
michael@0: addon.developers.push(this._makeDeveloper(developer));
michael@0: }
michael@0: break;
michael@0:
michael@0: case "screenshots":
michael@0: if (!addon.screenshots) addon.screenshots = [];
michael@0: for (let screenshot of value) {
michael@0: addon.screenshots.push(this._makeScreenshot(screenshot));
michael@0: }
michael@0: break;
michael@0:
michael@0: case "compatibilityOverrides":
michael@0: if (!addon.compatibilityOverrides) addon.compatibilityOverrides = [];
michael@0: for (let override of value) {
michael@0: addon.compatibilityOverrides.push(
michael@0: this._makeCompatOverride(override)
michael@0: );
michael@0: }
michael@0: break;
michael@0:
michael@0: case "icons":
michael@0: if (!addon.icons) addon.icons = {};
michael@0: for (let [size, url] of Iterator(aObj.icons)) {
michael@0: addon.icons[size] = url;
michael@0: }
michael@0: break;
michael@0:
michael@0: case "iconURL":
michael@0: break;
michael@0:
michael@0: default:
michael@0: addon[expectedProperty] = value;
michael@0: }
michael@0: } catch (ex) {
michael@0: logger.warn("Error in parsing property value for " + expectedProperty + " | " + ex);
michael@0: }
michael@0:
michael@0: // delete property from obj to indicate we've already
michael@0: // handled it. The remaining public properties will
michael@0: // be stored separately and just passed through to
michael@0: // be written back to the DB.
michael@0: delete aObj[expectedProperty];
michael@0: }
michael@0:
michael@0: // Copy remaining properties to a separate object
michael@0: // to prevent accidental access on downgraded versions.
michael@0: // The properties will be merged in the same object
michael@0: // prior to being written back through toJSON.
michael@0: for (let remainingProperty of Object.keys(aObj)) {
michael@0: switch (typeof(aObj[remainingProperty])) {
michael@0: case "boolean":
michael@0: case "number":
michael@0: case "string":
michael@0: case "object":
michael@0: // these types are accepted
michael@0: break;
michael@0: default:
michael@0: continue;
michael@0: }
michael@0:
michael@0: if (!remainingProperty.startsWith("_"))
michael@0: addon._unsupportedProperties[remainingProperty] =
michael@0: aObj[remainingProperty];
michael@0: }
michael@0:
michael@0: return addon;
michael@0: },
michael@0:
michael@0: /**
michael@0: * Write the in-memory DB to disk, after waiting for
michael@0: * the DB_BATCH_TIMEOUT_MS timeout.
michael@0: *
michael@0: * @return Promise A promise that resolves after the
michael@0: * write to disk has completed.
michael@0: */
michael@0: _saveDBToDisk: function() {
michael@0: return this.Writer.saveChanges().then(
michael@0: null,
michael@0: e => logger.error("SaveDBToDisk failed", e));
michael@0: },
michael@0:
michael@0: /**
michael@0: * Make a developer object from a vanilla
michael@0: * JS object from the JSON database
michael@0: *
michael@0: * @param aObj
michael@0: * The JS object to use
michael@0: * @return The created developer
michael@0: */
michael@0: _makeDeveloper: function (aObj) {
michael@0: let name = aObj.name;
michael@0: let url = aObj.url;
michael@0: return new AddonManagerPrivate.AddonAuthor(name, url);
michael@0: },
michael@0:
michael@0: /**
michael@0: * Make a screenshot object from a vanilla
michael@0: * JS object from the JSON database
michael@0: *
michael@0: * @param aObj
michael@0: * The JS object to use
michael@0: * @return The created screenshot
michael@0: */
michael@0: _makeScreenshot: function (aObj) {
michael@0: let url = aObj.url;
michael@0: let width = aObj.width;
michael@0: let height = aObj.height;
michael@0: let thumbnailURL = aObj.thumbnailURL;
michael@0: let thumbnailWidth = aObj.thumbnailWidth;
michael@0: let thumbnailHeight = aObj.thumbnailHeight;
michael@0: let caption = aObj.caption;
michael@0: return new AddonManagerPrivate.AddonScreenshot(url, width, height, thumbnailURL,
michael@0: thumbnailWidth, thumbnailHeight, caption);
michael@0: },
michael@0:
michael@0: /**
michael@0: * Make a CompatibilityOverride from a vanilla
michael@0: * JS object from the JSON database
michael@0: *
michael@0: * @param aObj
michael@0: * The JS object to use
michael@0: * @return The created CompatibilityOverride
michael@0: */
michael@0: _makeCompatOverride: function (aObj) {
michael@0: let type = aObj.type;
michael@0: let minVersion = aObj.minVersion;
michael@0: let maxVersion = aObj.maxVersion;
michael@0: let appID = aObj.appID;
michael@0: let appMinVersion = aObj.appMinVersion;
michael@0: let appMaxVersion = aObj.appMaxVersion;
michael@0: return new AddonManagerPrivate.AddonCompatibilityOverride(type,
michael@0: minVersion,
michael@0: maxVersion,
michael@0: appID,
michael@0: appMinVersion,
michael@0: appMaxVersion);
michael@0: },
michael@0: };
michael@0:
michael@0: function executeSoon(aCallback) {
michael@0: Services.tm.mainThread.dispatch(aCallback, Ci.nsIThread.DISPATCH_NORMAL);
michael@0: }