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