toolkit/mozapps/extensions/internal/AddonRepository.jsm

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

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

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

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

mercurial