CLOSED TREE: TraceMonkey merge head. (a=blockers)
[mozilla-central.git] / toolkit / mozapps / extensions / AddonRepository.jsm
blob9f2689973d5a2943ae98968c4c25b33095eee200
1 /*
2 # ***** BEGIN LICENSE BLOCK *****
3 # Version: MPL 1.1/GPL 2.0/LGPL 2.1
5 # The contents of this file are subject to the Mozilla Public License Version
6 # 1.1 (the "License"); you may not use this file except in compliance with
7 # the License. You may obtain a copy of the License at
8 # http://www.mozilla.org/MPL/
10 # Software distributed under the License is distributed on an "AS IS" basis,
11 # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
12 # for the specific language governing rights and limitations under the
13 # License.
15 # The Original Code is the Extension Manager.
17 # The Initial Developer of the Original Code is mozilla.org
18 # Portions created by the Initial Developer are Copyright (C) 2008
19 # the Initial Developer. All Rights Reserved.
21 # Contributor(s):
22 #   Dave Townsend <dtownsend@oxymoronical.com>
23 #   Ben Parr <bparr@bparr.com>
25 # Alternatively, the contents of this file may be used under the terms of
26 # either the GNU General Public License Version 2 or later (the "GPL"), or
27 # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
28 # in which case the provisions of the GPL or the LGPL are applicable instead
29 # of those above. If you wish to allow use of your version of this file only
30 # under the terms of either the GPL or the LGPL, and not to allow others to
31 # use your version of this file under the terms of the MPL, indicate your
32 # decision by deleting the provisions above and replace them with the notice
33 # and other provisions required by the GPL or the LGPL. If you do not delete
34 # the provisions above, a recipient may use your version of this file under
35 # the terms of any one of the MPL, the GPL or the LGPL.
37 # ***** END LICENSE BLOCK *****
40 const Cc = Components.classes;
41 const Ci = Components.interfaces;
42 const Cu = Components.utils;
44 Components.utils.import("resource://gre/modules/FileUtils.jsm");
45 Components.utils.import("resource://gre/modules/NetUtil.jsm");
46 Components.utils.import("resource://gre/modules/Services.jsm");
47 Components.utils.import("resource://gre/modules/AddonManager.jsm");
49 var EXPORTED_SYMBOLS = [ "AddonRepository" ];
51 const PREF_GETADDONS_CACHE_ENABLED       = "extensions.getAddons.cache.enabled";
52 const PREF_GETADDONS_CACHE_TYPES         = "extensions.getAddons.cache.types";
53 const PREF_GETADDONS_CACHE_ID_ENABLED    = "extensions.%ID%.getAddons.cache.enabled"
54 const PREF_GETADDONS_BROWSEADDONS        = "extensions.getAddons.browseAddons";
55 const PREF_GETADDONS_BYIDS               = "extensions.getAddons.get.url";
56 const PREF_GETADDONS_BROWSERECOMMENDED   = "extensions.getAddons.recommended.browseURL";
57 const PREF_GETADDONS_GETRECOMMENDED      = "extensions.getAddons.recommended.url";
58 const PREF_GETADDONS_BROWSESEARCHRESULTS = "extensions.getAddons.search.browseURL";
59 const PREF_GETADDONS_GETSEARCHRESULTS    = "extensions.getAddons.search.url";
61 const XMLURI_PARSE_ERROR  = "http://www.mozilla.org/newlayout/xml/parsererror.xml";
63 const API_VERSION = "1.5";
64 const DEFAULT_CACHE_TYPES = "extension,theme,locale";
66 const KEY_PROFILEDIR = "ProfD";
67 const FILE_DATABASE  = "addons.sqlite";
68 const DB_SCHEMA      = 1;
70 ["LOG", "WARN", "ERROR"].forEach(function(aName) {
71   this.__defineGetter__(aName, function() {
72     Components.utils.import("resource://gre/modules/AddonLogging.jsm");
74     LogManager.getLogger("addons.repository", this);
75     return this[aName];
76   });
77 }, this);
80 // Add-on properties parsed out of AMO results
81 // Note: the 'install' property is added for results from
82 // retrieveRecommendedAddons and searchAddons
83 const PROP_SINGLE = ["id", "type", "name", "version", "creator", "description",
84                      "fullDescription", "developerComments", "eula", "iconURL",
85                      "homepageURL", "supportURL", "contributionURL",
86                      "contributionAmount", "averageRating", "reviewCount",
87                      "reviewURL", "totalDownloads", "weeklyDownloads",
88                      "dailyUsers", "sourceURI", "repositoryStatus", "size",
89                      "updateDate"];
90 const PROP_MULTI = ["developers", "screenshots"]
92 // A map between XML keys to AddonSearchResult keys for string values
93 // that require no extra parsing from XML
94 const STRING_KEY_MAP = {
95   name:               "name",
96   version:            "version",
97   icon:               "iconURL",
98   homepage:           "homepageURL",
99   support:            "supportURL"
102 // A map between XML keys to AddonSearchResult keys for string values
103 // that require parsing from HTML
104 const HTML_KEY_MAP = {
105   summary:            "description",
106   description:        "fullDescription",
107   developer_comments: "developerComments",
108   eula:               "eula"
111 // A map between XML keys to AddonSearchResult keys for integer values
112 // that require no extra parsing from XML
113 const INTEGER_KEY_MAP = {
114   total_downloads:  "totalDownloads",
115   weekly_downloads: "weeklyDownloads",
116   daily_users:      "dailyUsers"
120 function convertHTMLToPlainText(html) {
121   if (!html)
122     return html;
123   var converter = Cc["@mozilla.org/widget/htmlformatconverter;1"].
124                   createInstance(Ci.nsIFormatConverter);
126   var input = Cc["@mozilla.org/supports-string;1"].
127               createInstance(Ci.nsISupportsString);
128   input.data = html.replace("\n", "<br>", "g");
130   var output = {};
131   converter.convert("text/html", input, input.data.length, "text/unicode",
132                     output, {});
134   if (output.value instanceof Ci.nsISupportsString)
135     return output.value.data.replace("\r\n", "\n", "g");
136   return html;
139 function getAddonsToCache(aIds, aCallback) {
140   try {
141     var types = Services.prefs.getCharPref(PREF_GETADDONS_CACHE_TYPES);
142   }
143   catch (e) { }
144   if (!types)
145     types = DEFAULT_CACHE_TYPES;
147   types = types.split(",");
149   AddonManager.getAddonsByIDs(aIds, function(aAddons) {
150     let enabledIds = [];
151     for (var i = 0; i < aIds.length; i++) {
152       var preference = PREF_GETADDONS_CACHE_ID_ENABLED.replace("%ID%", aIds[i]);
153       try {
154         if (!Services.prefs.getBoolPref(preference))
155           continue;
156       } catch(e) {
157         // If the preference doesn't exist caching is enabled by default
158       }
160       // The add-ons manager may not know about this ID yet if it is a pending
161       // install. In that case we'll just cache it regardless
162       if (aAddons[i] && (types.indexOf(aAddons[i].type) == -1))
163         continue;
165       enabledIds.push(aIds[i]);
166     }
168     aCallback(enabledIds);
169   });
172 function AddonSearchResult(aId) {
173   this.id = aId;
176 AddonSearchResult.prototype = {
177   /**
178    * The ID of the add-on
179    */
180   id: null,
182   /**
183    * The add-on type (e.g. "extension" or "theme")
184    */
185   type: null,
187   /**
188    * The name of the add-on
189    */
190   name: null,
192   /**
193    * The version of the add-on
194    */
195   version: null,
197   /**
198    * The creator of the add-on
199    */
200   creator: null,
202   /**
203    * The developers of the add-on
204    */
205   developers: null,
207   /**
208    * A short description of the add-on
209    */
210   description: null,
212   /**
213    * The full description of the add-on
214    */
215   fullDescription: null,
217   /**
218    * The developer comments for the add-on. This includes any information
219    * that may be helpful to end users that isn't necessarily applicable to
220    * the add-on description (e.g. known major bugs)
221    */
222   developerComments: null,
224   /**
225    * The end-user licensing agreement (EULA) of the add-on
226    */
227   eula: null,
229   /**
230    * The url of the add-on's icon
231    */
232   iconURL: null,
234   /**
235    * An array of screenshot urls for the add-on
236    */
237   screenshots: null,
239   /**
240    * The homepage for the add-on
241    */
242   homepageURL: null,
244   /**
245    * The support URL for the add-on
246    */
247   supportURL: null,
249   /**
250    * The contribution url of the add-on
251    */
252   contributionURL: null,
254   /**
255    * The suggested contribution amount
256    */
257   contributionAmount: null,
259   /**
260    * The URL to visit in order to purchase the add-on
261    */
262   purchaseURL: null,
264   /**
265    * The numerical cost of the add-on in some currency, for sorting purposes
266    * only
267    */
268   purchaseAmount: null,
270   /**
271    * The display cost of the add-on, for display purposes only
272    */
273   purchaseDisplayAmount: null,
275   /**
276    * The rating of the add-on, 0-5
277    */
278   averageRating: null,
280   /**
281    * The number of reviews for this add-on
282    */
283   reviewCount: null,
285   /**
286    * The URL to the list of reviews for this add-on
287    */
288   reviewURL: null,
290   /**
291    * The total number of times the add-on was downloaded
292    */
293   totalDownloads: null,
295   /**
296    * The number of times the add-on was downloaded the current week
297    */
298   weeklyDownloads: null,
300   /**
301    * The number of daily users for the add-on
302    */
303   dailyUsers: null,
305   /**
306    * AddonInstall object generated from the add-on XPI url
307    */
308   install: null,
310   /**
311    * nsIURI storing where this add-on was installed from
312    */
313   sourceURI: null,
315   /**
316    * The status of the add-on in the repository (e.g. 4 = "Public")
317    */
318   repositoryStatus: null,
320   /**
321    * The size of the add-on's files in bytes. For an add-on that have not yet
322    * been downloaded this may be an estimated value.
323    */
324   size: null,
326   /**
327    * The Date that the add-on was most recently updated
328    */
329   updateDate: null,
331   /**
332    * True or false depending on whether the add-on is compatible with the
333    * current version of the application
334    */
335   isCompatible: true,
337   /**
338    * True or false depending on whether the add-on is compatible with the
339    * current platform
340    */
341   isPlatformCompatible: true,
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(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(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   }
425  * The add-on repository is a source of add-ons that can be installed. It can
426  * be searched in three ways. The first takes a list of IDs and returns a
427  * list of the corresponding add-ons. The second returns a list of add-ons that
428  * come highly recommended. This list should change frequently. The third is to
429  * search for specific search terms entered by the user. Searches are
430  * asynchronous and results should be passed to the provided callback object
431  * when complete. The results passed to the callback should only include add-ons
432  * that are compatible with the current application and are not already
433  * installed.
434  */
435 var AddonRepository = {
436   /**
437    * Whether caching is currently enabled
438    */
439   get cacheEnabled() {
440     // Act as though caching is disabled if there was an unrecoverable error
441     // openning the database.
442     if (!AddonDatabase.databaseOk)
443       return false;
445     let preference = PREF_GETADDONS_CACHE_ENABLED;
446     let enabled = false;
447     try {
448       enabled = Services.prefs.getBoolPref(preference);
449     } catch(e) {
450       WARN("cacheEnabled: Couldn't get pref: " + preference);
451     }
453     return enabled;
454   },
456   // A cache of the add-ons stored in the database
457   _addons: null,
459   // An array of callbacks pending the retrieval of add-ons from AddonDatabase
460   _pendingCallbacks: null,
462   // Whether a search is currently in progress
463   _searching: false,
465   // XHR associated with the current request
466   _request: null,
468   /*
469    * Addon search results callback object that contains two functions
470    *
471    * searchSucceeded - Called when a search has suceeded.
472    *
473    * @param  aAddons
474    *         An array of the add-on results. In the case of searching for
475    *         specific terms the ordering of results may be determined by
476    *         the search provider.
477    * @param  aAddonCount
478    *         The length of aAddons
479    * @param  aTotalResults
480    *         The total results actually available in the repository
481    *
482    *
483    * searchFailed - Called when an error occurred when performing a search.
484    */
485   _callback: null,
487   // Maximum number of results to return
488   _maxResults: null,
489   
490   /**
491    * Initialize AddonRepository.
492    */
493   initialize: function() {
494     Services.obs.addObserver(this, "xpcom-shutdown", false);
495   },
497   /**
498    * Observe xpcom-shutdown notification, so we can shutdown cleanly.
499    */
500   observe: function (aSubject, aTopic, aData) {
501     if (aTopic == "xpcom-shutdown") {
502       Services.obs.removeObserver(this, "xpcom-shutdown");
503       this.shutdown();
504     }
505   },
507   /**
508    * Shut down AddonRepository
509    */
510   shutdown: function() {
511     this.cancelSearch();
513     this._addons = null;
514     this._pendingCallbacks = null;
515     AddonDatabase.shutdown(function() {
516       Services.obs.notifyObservers(null, "addon-repository-shutdown", null);
517     });
518   },
520   /**
521    * Asynchronously get a cached add-on by id. The add-on (or null if the
522    * add-on is not found) is passed to the specified callback. If caching is
523    * disabled, null is passed to the specified callback.
524    *
525    * @param  aId
526    *         The id of the add-on to get
527    * @param  aCallback
528    *         The callback to pass the result back to
529    */
530   getCachedAddonByID: function(aId, aCallback) {
531     if (!aId || !this.cacheEnabled) {
532       aCallback(null);
533       return;
534     }
536     let self = this;
537     function getAddon(aAddons) {
538       aCallback((aId in aAddons) ? aAddons[aId] : null);
539     }
541     if (this._addons == null) {
542       if (this._pendingCallbacks == null) {
543         // Data has not been retrieved from the database, so retrieve it
544         this._pendingCallbacks = [];
545         this._pendingCallbacks.push(getAddon);
546         AddonDatabase.retrieveStoredData(function(aAddons) {
547           let pendingCallbacks = self._pendingCallbacks;
549           // Check if cache was shutdown or deleted before callback was called
550           if (pendingCallbacks == null)
551             return;
553           // Callbacks may want to trigger a other caching operations that may
554           // affect _addons and _pendingCallbacks, so set to final values early
555           self._pendingCallbacks = null;
556           self._addons = aAddons;
558           pendingCallbacks.forEach(function(aCallback) aCallback(aAddons));
559         });
561         return;
562       }
564       // Data is being retrieved from the database, so wait
565       this._pendingCallbacks.push(getAddon);
566       return;
567     }
569     // Data has been retrieved, so immediately return result
570     getAddon(this._addons);
571   },
573   /**
574    * Asynchronously repopulate cache so it only contains the add-ons
575    * corresponding to the specified ids. If caching is disabled,
576    * the cache is completely removed.
577    *
578    * @param  aIds
579    *         The array of add-on ids to repopulate the cache with
580    * @param  aCallback
581    *         The optional callback to call once complete
582    */
583   repopulateCache: function(aIds, aCallback) {
584     // Completely remove cache if caching is not enabled
585     if (!this.cacheEnabled) {
586       this._addons = null;
587       this._pendingCallbacks = null;
588       AddonDatabase.delete(aCallback);
589       return;
590     }
592     let self = this;
593     getAddonsToCache(aIds, function(aAddons) {
594       // Completely remove cache if there are no add-ons to cache
595       if (aAddons.length == 0) {
596         this._addons = null;
597         this._pendingCallbacks = null;
598         AddonDatabase.delete(aCallback);
599         return;
600       }
602       self.getAddonsByIDs(aAddons, {
603         searchSucceeded: function(aAddons) {
604           self._addons = {};
605           aAddons.forEach(function(aAddon) { self._addons[aAddon.id] = aAddon; });
606           AddonDatabase.repopulate(aAddons, aCallback);
607         },
608         searchFailed: function() {
609           WARN("Search failed when repopulating cache");
610           if (aCallback)
611             aCallback();
612         }
613       });
614     });
615   },
617   /**
618    * Asynchronously add add-ons to the cache corresponding to the specified
619    * ids. If caching is disabled, the cache is unchanged and the callback is
620    * immediatly called if it is defined.
621    *
622    * @param  aIds
623    *         The array of add-on ids to add to the cache
624    * @param  aCallback
625    *         The optional callback to call once complete
626    */
627   cacheAddons: function(aIds, aCallback) {
628     if (!this.cacheEnabled) {
629       if (aCallback)
630         aCallback();
631       return;
632     }
634     let self = this;
635     getAddonsToCache(aIds, function(aAddons) {
636       // If there are no add-ons to cache, act as if caching is disabled
637       if (aAddons.length == 0) {
638         if (aCallback)
639           aCallback();
640         return;
641       }
643       self.getAddonsByIDs(aAddons, {
644         searchSucceeded: function(aAddons) {
645           aAddons.forEach(function(aAddon) { self._addons[aAddon.id] = aAddon; });
646           AddonDatabase.insertAddons(aAddons, aCallback);
647         },
648         searchFailed: function() {
649           WARN("Search failed when adding add-ons to cache");
650           if (aCallback)
651             aCallback();
652         }
653       });
654     });
655   },
657   /**
658    * The homepage for visiting this repository. If the corresponding preference
659    * is not defined, defaults to about:blank.
660    */
661   get homepageURL() {
662     let url = this._formatURLPref(PREF_GETADDONS_BROWSEADDONS, {});
663     return (url != null) ? url : "about:blank";
664   },
666   /**
667    * Returns whether this instance is currently performing a search. New
668    * searches will not be performed while this is the case.
669    */
670   get isSearching() {
671     return this._searching;
672   },
674   /**
675    * The url that can be visited to see recommended add-ons in this repository.
676    * If the corresponding preference is not defined, defaults to about:blank.
677    */
678   getRecommendedURL: function() {
679     let url = this._formatURLPref(PREF_GETADDONS_BROWSERECOMMENDED, {});
680     return (url != null) ? url : "about:blank";
681   },
683   /**
684    * Retrieves the url that can be visited to see search results for the given
685    * terms. If the corresponding preference is not defined, defaults to
686    * about:blank.
687    *
688    * @param  aSearchTerms
689    *         Search terms used to search the repository
690    */
691   getSearchURL: function(aSearchTerms) {
692     let url = this._formatURLPref(PREF_GETADDONS_BROWSESEARCHRESULTS, {
693       TERMS : encodeURIComponent(aSearchTerms)
694     });
695     return (url != null) ? url : "about:blank";
696   },
698   /**
699    * Cancels the search in progress. If there is no search in progress this
700    * does nothing.
701    */
702   cancelSearch: function() {
703     this._searching = false;
704     if (this._request) {
705       this._request.abort();
706       this._request = null;
707     }
708     this._callback = null;
709   },
711   /**
712    * Begins a search for add-ons in this repository by ID. Results will be
713    * passed to the given callback.
714    *
715    * @param  aIDs
716    *         The array of ids to search for
717    * @param  aCallback
718    *         The callback to pass results to
719    */
720   getAddonsByIDs: function(aIDs, aCallback) {
721     let startupInfo = Cc["@mozilla.org/toolkit/app-startup;1"].
722                       getService(Ci.nsIAppStartup_MOZILLA_2_0).
723                       getStartupInfo();
725     let ids = aIDs.slice(0);
727     let params = {
728       API_VERSION : API_VERSION,
729       IDS : ids.map(encodeURIComponent).join(',')
730     };
732     if (startupInfo.process) {
733       if (startupInfo.main)
734         params.TIME_MAIN = startupInfo.main - startupInfo.process;
735       if (startupInfo.firstPaint)
736         params.TIME_FIRST_PAINT = startupInfo.firstPaint - startupInfo.process;
737       if (startupInfo.sessionRestored)
738         params.TIME_SESSION_RESTORED = startupInfo.sessionRestored -
739                                        startupInfo.process;
740     };
742     let url = this._formatURLPref(PREF_GETADDONS_BYIDS, params);
744     let self = this;
745     function handleResults(aElements, aTotalResults) {
746       // Don't use this._parseAddons() so that, for example,
747       // incompatible add-ons are not filtered out
748       let results = [];
749       for (let i = 0; i < aElements.length && results.length < self._maxResults; i++) {
750         let result = self._parseAddon(aElements[i]);
751         if (result == null)
752           continue;
754         // Ignore add-on if it wasn't actually requested
755         let idIndex = ids.indexOf(result.addon.id);
756         if (idIndex == -1)
757           continue;
759         results.push(result);
760         // Ignore this add-on from now on
761         ids.splice(idIndex, 1);
762       }
764       // aTotalResults irrelevant
765       self._reportSuccess(results, -1);
766     }
768     this._beginSearch(url, ids.length, aCallback, handleResults);
769   },
771   /**
772    * Begins a search for recommended add-ons in this repository. Results will
773    * be passed to the given callback.
774    *
775    * @param  aMaxResults
776    *         The maximum number of results to return
777    * @param  aCallback
778    *         The callback to pass results to
779    */
780   retrieveRecommendedAddons: function(aMaxResults, aCallback) {
781     let url = this._formatURLPref(PREF_GETADDONS_GETRECOMMENDED, {
782       API_VERSION : API_VERSION,
784       // Get twice as many results to account for potential filtering
785       MAX_RESULTS : 2 * aMaxResults
786     });
788     let self = this;
789     function handleResults(aElements, aTotalResults) {
790       self._getLocalAddonIds(function(aLocalAddonIds) {
791         // aTotalResults irrelevant
792         self._parseAddons(aElements, -1, aLocalAddonIds);
793       });
794     }
796     this._beginSearch(url, aMaxResults, aCallback, handleResults);
797   },
799   /**
800    * Begins a search for add-ons in this repository. Results will be passed to
801    * the given callback.
802    *
803    * @param  aSearchTerms
804    *         The terms to search for
805    * @param  aMaxResults
806    *         The maximum number of results to return
807    * @param  aCallback
808    *         The callback to pass results to
809    */
810   searchAddons: function(aSearchTerms, aMaxResults, aCallback) {
811     let url = this._formatURLPref(PREF_GETADDONS_GETSEARCHRESULTS, {
812       API_VERSION : API_VERSION,
813       TERMS : encodeURIComponent(aSearchTerms),
815       // Get twice as many results to account for potential filtering
816       MAX_RESULTS : 2 * aMaxResults
817     });
819     let self = this;
820     function handleResults(aElements, aTotalResults) {
821       self._getLocalAddonIds(function(aLocalAddonIds) {
822         self._parseAddons(aElements, aTotalResults, aLocalAddonIds);
823       });
824     }
826     this._beginSearch(url, aMaxResults, aCallback, handleResults);
827   },
829   // Posts results to the callback
830   _reportSuccess: function(aResults, aTotalResults) {
831     this._searching = false;
832     this._request = null;
833     // The callback may want to trigger a new search so clear references early
834     let addons = [result.addon for each(result in aResults)];
835     let callback = this._callback;
836     this._callback = null;
837     callback.searchSucceeded(addons, addons.length, aTotalResults);
838   },
840   // Notifies the callback of a failure
841   _reportFailure: function() {
842     this._searching = false;
843     this._request = null;
844     // The callback may want to trigger a new search so clear references early
845     let callback = this._callback;
846     this._callback = null;
847     callback.searchFailed();
848   },
850   // Get descendant by unique tag name. Returns null if not unique tag name.
851   _getUniqueDescendant: function(aElement, aTagName) {
852     let elementsList = aElement.getElementsByTagName(aTagName);
853     return (elementsList.length == 1) ? elementsList[0] : null;
854   },
856   // Parse out trimmed text content. Returns null if text content empty.
857   _getTextContent: function(aElement) {
858     let textContent = aElement.textContent.trim();
859     return (textContent.length > 0) ? textContent : null;
860   },
862   // Parse out trimmed text content of a descendant with the specified tag name
863   // Returns null if the parsing unsuccessful.
864   _getDescendantTextContent: function(aElement, aTagName) {
865     let descendant = this._getUniqueDescendant(aElement, aTagName);
866     return (descendant != null) ? this._getTextContent(descendant) : null;
867   },
869   /*
870    * Creates an AddonSearchResult by parsing an <addon> element
871    *
872    * @param  aElement
873    *         The <addon> element to parse
874    * @param  aSkip
875    *         Object containing ids and sourceURIs of add-ons to skip.
876    * @return Result object containing the parsed AddonSearchResult, xpiURL and
877    *         xpiHash if the parsing was successful. Otherwise returns null.
878    */
879   _parseAddon: function(aElement, aSkip) {
880     let skipIDs = (aSkip && aSkip.ids) ? aSkip.ids : [];
881     let skipSourceURIs = (aSkip && aSkip.sourceURIs) ? aSkip.sourceURIs : [];
883     let guid = this._getDescendantTextContent(aElement, "guid");
884     if (guid == null || skipIDs.indexOf(guid) != -1)
885       return null;
887     let addon = new AddonSearchResult(guid);
888     let result = {
889       addon: addon,
890       xpiURL: null,
891       xpiHash: null
892     };
894     let self = this;
895     for (let node = aElement.firstChild; node; node = node.nextSibling) {
896       if (!(node instanceof Ci.nsIDOMElement))
897         continue;
899       let localName = node.localName;
901       // Handle case where the wanted string value is located in text content
902       if (localName in STRING_KEY_MAP) {
903         addon[STRING_KEY_MAP[localName]] = this._getTextContent(node);
904         continue;
905       }
907       // Handle case where the wanted string value is html located in text content
908       if (localName in HTML_KEY_MAP) {
909         addon[HTML_KEY_MAP[localName]] = convertHTMLToPlainText(this._getTextContent(node));
910         continue;
911       }
913       // Handle case where the wanted integer value is located in text content
914       if (localName in INTEGER_KEY_MAP) {
915         let value = parseInt(this._getTextContent(node));
916         if (value >= 0)
917           addon[INTEGER_KEY_MAP[localName]] = value;
918         continue;
919       }
921       // Handle cases that aren't as simple as grabbing the text content
922       switch (localName) {
923         case "type":
924           // Map AMO's type id to corresponding string
925           let id = parseInt(node.getAttribute("id"));
926           switch (id) {
927             case 1:
928               addon.type = "extension";
929               break;
930             case 2:
931               addon.type = "theme";
932               break;
933             default:
934               WARN("Unknown type id when parsing addon: " + id);
935           }
936           break;
937         case "authors":
938           let authorNodes = node.getElementsByTagName("author");
939           Array.forEach(authorNodes, function(aAuthorNode) {
940             let name = self._getDescendantTextContent(aAuthorNode, "name");
941             let link = self._getDescendantTextContent(aAuthorNode, "link");
942             if (name == null || link == null)
943               return;
945             let author = new AddonManagerPrivate.AddonAuthor(name, link);
946             if (addon.creator == null)
947               addon.creator = author;
948             else {
949               if (addon.developers == null)
950                 addon.developers = [];
952               addon.developers.push(author);
953             }
954           });
955           break;
956         case "previews":
957           let previewNodes = node.getElementsByTagName("preview");
958           Array.forEach(previewNodes, function(aPreviewNode) {
959             let full = self._getDescendantTextContent(aPreviewNode, "full");
960             if (full == null)
961               return;
963             let thumbnail = self._getDescendantTextContent(aPreviewNode, "thumbnail");
964             let caption = self._getDescendantTextContent(aPreviewNode, "caption");
965             let screenshot = new AddonManagerPrivate.AddonScreenshot(full, thumbnail, caption);
967             if (addon.screenshots == null)
968               addon.screenshots = [];
970             if (aPreviewNode.getAttribute("primary") == 1)
971               addon.screenshots.unshift(screenshot);
972             else
973               addon.screenshots.push(screenshot);
974           });
975           break;
976         case "learnmore":
977           addon.homepageURL = addon.homepageURL || this._getTextContent(node);
978           break;
979         case "contribution_data":
980           let meetDevelopers = this._getDescendantTextContent(node, "meet_developers");
981           let suggestedAmount = this._getDescendantTextContent(node, "suggested_amount");
982           if (meetDevelopers != null) {
983             addon.contributionURL = meetDevelopers;
984             addon.contributionAmount = suggestedAmount;
985           }
986           break
987         case "payment_data":
988           let link = this._getDescendantTextContent(node, "link");
989           let amountTag = this._getUniqueDescendant(node, "amount");
990           let amount = parseFloat(amountTag.getAttribute("amount"));
991           let displayAmount = this._getTextContent(amountTag);
992           if (link != null && amount != null && displayAmount != null) {
993             addon.purchaseURL = link;
994             addon.purchaseAmount = amount;
995             addon.purchaseDisplayAmount = displayAmount;
996           }
997           break
998         case "rating":
999           let averageRating = parseInt(this._getTextContent(node));
1000           if (averageRating >= 0)
1001             addon.averageRating = Math.min(5, averageRating);
1002           break;
1003         case "reviews":
1004           let url = this._getTextContent(node);
1005           let num = parseInt(node.getAttribute("num"));
1006           if (url != null && num >= 0) {
1007             addon.reviewURL = url;
1008             addon.reviewCount = num;
1009           }
1010           break;
1011         case "status":
1012           let repositoryStatus = parseInt(node.getAttribute("id"));
1013           if (!isNaN(repositoryStatus))
1014             addon.repositoryStatus = repositoryStatus;
1015           break;
1016         case "all_compatible_os":
1017           let nodes = node.getElementsByTagName("os");
1018           addon.isPlatformCompatible = Array.some(nodes, function(aNode) {
1019             let text = aNode.textContent.toLowerCase().trim();
1020             return text == "all" || text == Services.appinfo.OS.toLowerCase();
1021           });
1022           break;
1023         case "install":
1024           // No os attribute means the xpi is compatible with any os
1025           if (node.hasAttribute("os")) {
1026             let os = node.getAttribute("os").trim().toLowerCase();
1027             // If the os is not ALL and not the current OS then ignore this xpi
1028             if (os != "all" && os != Services.appinfo.OS.toLowerCase())
1029               break;
1030           }
1032           let xpiURL = this._getTextContent(node);
1033           if (xpiURL == null)
1034             break;
1036           if (skipSourceURIs.indexOf(xpiURL) != -1)
1037             return null;
1039           result.xpiURL = xpiURL;
1040           addon.sourceURI = NetUtil.newURI(xpiURL);
1042           let size = parseInt(node.getAttribute("size"));
1043           addon.size = (size >= 0) ? size : null;
1045           let xpiHash = node.getAttribute("hash");
1046           if (xpiHash != null)
1047             xpiHash = xpiHash.trim();
1048           result.xpiHash = xpiHash ? xpiHash : null;
1049           break;
1050         case "last_updated":
1051           let epoch = parseInt(node.getAttribute("epoch"));
1052           if (!isNaN(epoch))
1053             addon.updateDate = new Date(1000 * epoch);
1054           break;
1055       }
1056     }
1058     return result;
1059   },
1061   _parseAddons: function(aElements, aTotalResults, aSkip) {
1062     let self = this;
1063     let results = [];
1064     for (let i = 0; i < aElements.length && results.length < this._maxResults; i++) {
1065       let element = aElements[i];
1067       // Ignore sandboxed add-ons
1068       let status = this._getUniqueDescendant(element, "status");
1069       // The status element has a unique id for each status type. 4 is Public.
1070       if (status == null || status.getAttribute("id") != 4)
1071         continue;
1073       // Ignore add-ons not compatible with this Application
1074       let tags = this._getUniqueDescendant(element, "compatible_applications");
1075       if (tags == null)
1076         continue;
1078       let applications = tags.getElementsByTagName("appID");
1079       let compatible = Array.some(applications, function(aAppNode) {
1080         if (self._getTextContent(aAppNode) != Services.appinfo.ID)
1081           return false;
1083         let parent = aAppNode.parentNode;
1084         let minVersion = self._getDescendantTextContent(parent, "min_version");
1085         let maxVersion = self._getDescendantTextContent(parent, "max_version");
1086         if (minVersion == null || maxVersion == null)
1087           return false;
1089         let currentVersion = Services.appinfo.version;
1090         return (Services.vc.compare(minVersion, currentVersion) <= 0 &&
1091                 Services.vc.compare(currentVersion, maxVersion) <= 0);
1092       });
1094       if (!compatible)
1095         continue;
1097       // Add-on meets all requirements, so parse out data
1098       let result = this._parseAddon(element, aSkip);
1099       if (result == null)
1100         continue;
1102       // Ignore add-on missing a required attribute
1103       let requiredAttributes = ["id", "name", "version", "type", "creator"];
1104       if (requiredAttributes.some(function(aAttribute) !result.addon[aAttribute]))
1105         continue;
1107       // Add only if the add-on is compatible with the platform
1108       if (!result.addon.isPlatformCompatible)
1109         continue;
1111       // Add only if there was an xpi compatible with this OS or there was a
1112       // way to purchase the add-on
1113       if (!result.xpiURL && !result.addon.purchaseURL)
1114         continue;
1116       results.push(result);
1117       // Ignore this add-on from now on by adding it to the skip array
1118       aSkip.ids.push(result.addon.id);
1119     }
1121     // Immediately report success if no AddonInstall instances to create
1122     let pendingResults = results.length;
1123     if (pendingResults == 0) {
1124       this._reportSuccess(results, aTotalResults);
1125       return;
1126     }
1128     // Create an AddonInstall for each result
1129     let self = this;
1130     results.forEach(function(aResult) {
1131       let addon = aResult.addon;
1132       let callback = function(aInstall) {
1133         addon.install = aInstall;
1134         pendingResults--;
1135         if (pendingResults == 0)
1136           self._reportSuccess(results, aTotalResults);
1137       }
1139       if (aResult.xpiURL) {
1140         AddonManager.getInstallForURL(aResult.xpiURL, callback,
1141                                       "application/x-xpinstall", aResult.xpiHash,
1142                                       addon.name, addon.iconURL, addon.version);
1143       }
1144       else {
1145         callback(null);
1146       }
1147     });
1148   },
1150   // Begins a new search if one isn't currently executing
1151   _beginSearch: function(aURI, aMaxResults, aCallback, aHandleResults) {
1152     if (this._searching || aURI == null || aMaxResults <= 0) {
1153       aCallback.searchFailed();
1154       return;
1155     }
1157     this._searching = true;
1158     this._callback = aCallback;
1159     this._maxResults = aMaxResults;
1161     LOG("Requesting " + aURI);
1163     this._request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
1164                     createInstance(Ci.nsIXMLHttpRequest);
1165     this._request.open("GET", aURI, true);
1166     this._request.overrideMimeType("text/xml");
1168     let self = this;
1169     this._request.onerror = function(aEvent) { self._reportFailure(); };
1170     this._request.onload = function(aEvent) {
1171       let request = aEvent.target;
1172       let responseXML = request.responseXML;
1174       if (!responseXML || responseXML.documentElement.namespaceURI == XMLURI_PARSE_ERROR ||
1175           (request.status != 200 && request.status != 0)) {
1176         self._reportFailure();
1177         return;
1178       }
1180       let documentElement = responseXML.documentElement;
1181       let elements = documentElement.getElementsByTagName("addon");
1182       let totalResults = elements.length;
1183       let parsedTotalResults = parseInt(documentElement.getAttribute("total_results"));
1184       // Parsed value of total results only makes sense if >= elements.length
1185       if (parsedTotalResults >= totalResults)
1186         totalResults = parsedTotalResults;
1188       aHandleResults(elements, totalResults);
1189     };
1190     this._request.send(null);
1191   },
1193   // Gets the id's of local add-ons, and the sourceURI's of local installs,
1194   // passing the results to aCallback
1195   _getLocalAddonIds: function(aCallback) {
1196     let self = this;
1197     let localAddonIds = {ids: null, sourceURIs: null};
1199     AddonManager.getAllAddons(function(aAddons) {
1200       localAddonIds.ids = [a.id for each (a in aAddons)];
1201       if (localAddonIds.sourceURIs)
1202         aCallback(localAddonIds);
1203     });
1205     AddonManager.getAllInstalls(function(aInstalls) {
1206       localAddonIds.sourceURIs = [];
1207       aInstalls.forEach(function(aInstall) {
1208         if (aInstall.state != AddonManager.STATE_AVAILABLE)
1209           localAddonIds.sourceURIs.push(aInstall.sourceURI.spec);
1210       });
1212       if (localAddonIds.ids)
1213         aCallback(localAddonIds);
1214     });
1215   },
1217   // Create url from preference, returning null if preference does not exist
1218   _formatURLPref: function(aPreference, aSubstitutions) {
1219     let url = null;
1220     try {
1221       url = Services.prefs.getCharPref(aPreference);
1222     } catch(e) {
1223       WARN("_formatURLPref: Couldn't get pref: " + aPreference);
1224       return null;
1225     }
1227     url = url.replace(/%([A-Z_]+)%/g, function(aMatch, aKey) {
1228       return (aKey in aSubstitutions) ? aSubstitutions[aKey] : aMatch;
1229     });
1231     return Services.urlFormatter.formatURL(url);
1232   }
1234 AddonRepository.initialize();
1236 var AddonDatabase = {
1237   // true if the database connection has been opened
1238   initialized: false,
1239   // false if there was an unrecoverable error openning the database
1240   databaseOk: true,
1241   // A cache of statements that are used and need to be finalized on shutdown
1242   statementCache: {},
1244   // The statements used by the database
1245   statements: {
1246     getAllAddons: "SELECT internal_id, id, type, name, version, " +
1247                   "creator, creatorURL, description, fullDescription, " +
1248                   "developerComments, eula, iconURL, homepageURL, supportURL, " +
1249                   "contributionURL, contributionAmount, averageRating, " +
1250                   "reviewCount, reviewURL, totalDownloads, weeklyDownloads, " +
1251                   "dailyUsers, sourceURI, repositoryStatus, size, updateDate " +
1252                   "FROM addon",
1254     getAllDevelopers: "SELECT addon_internal_id, name, url FROM developer " +
1255                       "ORDER BY addon_internal_id, num",
1257     getAllScreenshots: "SELECT addon_internal_id, url, thumbnailURL, caption " +
1258                        "FROM screenshot ORDER BY addon_internal_id, num",
1260     insertAddon: "INSERT INTO addon VALUES (NULL, :id, :type, :name, :version, " +
1261                  ":creator, :creatorURL, :description, :fullDescription, " +
1262                  ":developerComments, :eula, :iconURL, :homepageURL, :supportURL, " +
1263                  ":contributionURL, :contributionAmount, :averageRating, " +
1264                  ":reviewCount, :reviewURL, :totalDownloads, :weeklyDownloads, " +
1265                  ":dailyUsers, :sourceURI, :repositoryStatus, :size, :updateDate)",
1267     insertDeveloper:  "INSERT INTO developer VALUES (:addon_internal_id, " +
1268                       ":num, :name, :url)",
1270     insertScreenshot: "INSERT INTO screenshot VALUES (:addon_internal_id, " +
1271                       ":num, :url, :thumbnailURL, :caption)",
1273     emptyAddon:       "DELETE FROM addon"
1274   },
1276   /**
1277    * A helper function to log an SQL error.
1278    *
1279    * @param  aError
1280    *         The storage error code associated with the error
1281    * @param  aErrorString
1282    *         An error message
1283    */
1284   logSQLError: function AD_logSQLError(aError, aErrorString) {
1285     ERROR("SQL error " + aError + ": " + aErrorString);
1286   },
1288   /**
1289    * A helper function to log any errors that occur during async statements.
1290    *
1291    * @param  aError
1292    *         A mozIStorageError to log
1293    */
1294   asyncErrorLogger: function AD_asyncErrorLogger(aError) {
1295     ERROR("Async SQL error " + aError.result + ": " + aError.message);
1296   },
1298   /**
1299    * Synchronously opens a new connection to the database file.
1300    *
1301    * @param  aSecondAttempt
1302    *         Whether this is a second attempt to open the database
1303    * @return the mozIStorageConnection for the database
1304    */
1305   openConnection: function AD_openConnection(aSecondAttempt) {
1306     this.initialized = true;
1307     delete this.connection;
1309     let dbfile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_DATABASE], true);
1310     let dbMissing = !dbfile.exists();
1312     try {
1313       this.connection = Services.storage.openUnsharedDatabase(dbfile);
1314     } catch (e) {
1315       this.initialized = false;
1316       ERROR("Failed to open database", e);
1317       if (aSecondAttempt || dbMissing) {
1318         this.databaseOk = false;
1319         throw e;
1320       }
1322       LOG("Deleting database, and attempting openConnection again");
1323       dbfile.remove(false);
1324       return this.openConnection(true);
1325     }
1327     this.connection.executeSimpleSQL("PRAGMA locking_mode = EXCLUSIVE");
1328     if (dbMissing || this.connection.schemaVersion == 0)
1329       this._createSchema();
1331     return this.connection;
1332   },
1334   /**
1335    * A lazy getter for the database connection.
1336    */
1337   get connection() {
1338     return this.openConnection();
1339   },
1341   /**
1342    * Asynchronously shuts down the database connection and releases all
1343    * cached objects
1344    *
1345    * @param  aCallback
1346    *         An optional callback to call once complete
1347    */
1348   shutdown: function AD_shutdown(aCallback) {
1349     this.databaseOk = true;
1350     if (!this.initialized) {
1351       if (aCallback)
1352         aCallback();
1353       return;
1354     }
1356     this.initialized = false;
1358     for each (let stmt in this.statementCache)
1359       stmt.finalize();
1360     this.statementCache = {};
1362     if (this.connection.transactionInProgress) {
1363       ERROR("Outstanding transaction, rolling back.");
1364       this.connection.rollbackTransaction();
1365     }
1367     let connection = this.connection;
1368     delete this.connection;
1370     // Re-create the connection smart getter to allow the database to be
1371     // re-loaded during testing.
1372     this.__defineGetter__("connection", function() {
1373       return this.openConnection();
1374     });
1376     connection.asyncClose(aCallback);
1377   },
1379   /**
1380    * Asynchronously deletes the database, shutting down the connection
1381    * first if initialized
1382    *
1383    * @param  aCallback
1384    *         An optional callback to call once complete
1385    */
1386   delete: function AD_delete(aCallback) {
1387     this.shutdown(function() {
1388       let dbfile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_DATABASE], true);
1389       if (dbfile.exists())
1390         dbfile.remove(false);
1392       if (aCallback)
1393         aCallback();
1394     });
1395   },
1397   /**
1398    * Gets a cached statement or creates a new statement if it doesn't already
1399    * exist.
1400    *
1401    * @param  aKey
1402    *         A unique key to reference the statement
1403    * @return a mozIStorageStatement for the SQL corresponding to the unique key
1404    */
1405   getStatement: function AD_getStatement(aKey) {
1406     if (aKey in this.statementCache)
1407       return this.statementCache[aKey];
1409     let sql = this.statements[aKey];
1410     try {
1411       return this.statementCache[aKey] = this.connection.createStatement(sql);
1412     } catch (e) {
1413       ERROR("Error creating statement " + aKey + " (" + aSql + ")");
1414       throw e;
1415     }
1416   },
1418   /**
1419    * Asynchronously retrieve all add-ons from the database, and pass it
1420    * to the specified callback
1421    *
1422    * @param  aCallback
1423    *         The callback to pass the add-ons back to
1424    */
1425   retrieveStoredData: function AD_retrieveStoredData(aCallback) {
1426     let self = this;
1427     let addons = {};
1429     // Retrieve all data from the addon table
1430     function getAllAddons() {
1431       self.getStatement("getAllAddons").executeAsync({
1432         handleResult: function(aResults) {
1433           let row = null;
1434           while (row = aResults.getNextRow()) {
1435             let internal_id = row.getResultByName("internal_id");
1436             addons[internal_id] = self._makeAddonFromAsyncRow(row);
1437           }
1438         },
1440         handleError: self.asyncErrorLogger,
1442         handleCompletion: function(aReason) {
1443           if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) {
1444             ERROR("Error retrieving add-ons from database. Returning empty results");
1445             aCallback({});
1446             return;
1447           }
1449           getAllDevelopers();
1450         }
1451       });
1452     }
1454     // Retrieve all data from the developer table
1455     function getAllDevelopers() {
1456       self.getStatement("getAllDevelopers").executeAsync({
1457         handleResult: function(aResults) {
1458           let row = null;
1459           while (row = aResults.getNextRow()) {
1460             let addon_internal_id = row.getResultByName("addon_internal_id");
1461             if (!(addon_internal_id in addons)) {
1462               WARN("Found a developer not linked to an add-on in database");
1463               continue;
1464             }
1466             let addon = addons[addon_internal_id];
1467             if (!addon.developers)
1468               addon.developers = [];
1470             addon.developers.push(self._makeDeveloperFromAsyncRow(row));
1471           }
1472         },
1474         handleError: self.asyncErrorLogger,
1476         handleCompletion: function(aReason) {
1477           if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) {
1478             ERROR("Error retrieving developers from database. Returning empty results");
1479             aCallback({});
1480             return;
1481           }
1483           getAllScreenshots();
1484         }
1485       });
1486     }
1488     // Retrieve all data from the screenshot table
1489     function getAllScreenshots() {
1490       self.getStatement("getAllScreenshots").executeAsync({
1491         handleResult: function(aResults) {
1492           let row = null;
1493           while (row = aResults.getNextRow()) {
1494             let addon_internal_id = row.getResultByName("addon_internal_id");
1495             if (!(addon_internal_id in addons)) {
1496               WARN("Found a screenshot not linked to an add-on in database");
1497               continue;
1498             }
1500             let addon = addons[addon_internal_id];
1501             if (!addon.screenshots)
1502               addon.screenshots = [];
1503             addon.screenshots.push(self._makeScreenshotFromAsyncRow(row));
1504           }
1505         },
1507         handleError: self.asyncErrorLogger,
1509         handleCompletion: function(aReason) {
1510           if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) {
1511             ERROR("Error retrieving screenshots from database. Returning empty results");
1512             aCallback({});
1513             return;
1514           }
1516           let returnedAddons = {};
1517           for each (addon in addons)
1518             returnedAddons[addon.id] = addon;
1519           aCallback(returnedAddons);
1520         }
1521       });
1522     }
1524     // Begin asynchronous process
1525     getAllAddons();
1526   },
1528   /**
1529    * Asynchronously repopulates the database so it only contains the
1530    * specified add-ons
1531    *
1532    * @param  aAddons
1533    *         The array of add-ons to repopulate the database with
1534    * @param  aCallback
1535    *         An optional callback to call once complete
1536    */
1537   repopulate: function AD_repopulate(aAddons, aCallback) {
1538     let self = this;
1540     // Completely empty the database
1541     let stmts = [this.getStatement("emptyAddon")];
1543     this.connection.executeAsync(stmts, stmts.length, {
1544       handleResult: function() {},
1545       handleError: self.asyncErrorLogger,
1547       handleCompletion: function(aReason) {
1548         if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED)
1549           ERROR("Error emptying database. Attempting to continue repopulating database");
1551         // Insert the specified add-ons
1552         self.insertAddons(aAddons, aCallback);
1553       }
1554     });
1555   },
1557   /**
1558    * Asynchronously inserts an array of add-ons into the database
1559    *
1560    * @param  aAddons
1561    *         The array of add-ons to insert
1562    * @param  aCallback
1563    *         An optional callback to call once complete
1564    */
1565   insertAddons: function AD_insertAddons(aAddons, aCallback) {
1566     let self = this;
1567     let currentAddon = -1;
1569     // Chain insertions
1570     function insertNextAddon() {
1571       if (++currentAddon == aAddons.length) {
1572         if (aCallback)
1573           aCallback();
1574         return;
1575       }
1577       self._insertAddon(aAddons[currentAddon], insertNextAddon);
1578     }
1580     insertNextAddon();
1581   },
1583   /**
1584    * Inserts an individual add-on into the database. If the add-on already
1585    * exists in the database (by id), then the specified add-on will not be
1586    * inserted.
1587    *
1588    * @param  aAddon
1589    *         The add-on to insert into the database
1590    * @param  aCallback
1591    *         The callback to call once complete
1592    */
1593   _insertAddon: function AD__insertAddon(aAddon, aCallback) {
1594     let self = this;
1595     let internal_id = null;
1596     this.connection.beginTransaction();
1598     // Simultaneously insert the developers and screenshots of the add-on
1599     function insertDevelopersAndScreenshots() {
1600       let stmts = [];
1602       // Initialize statement and parameters for inserting an array
1603       function initializeArrayInsert(aStatementKey, aArray, aAddParams) {
1604         if (!aArray || aArray.length == 0)
1605           return;
1607         let stmt = self.getStatement(aStatementKey);
1608         let params = stmt.newBindingParamsArray();
1609         aArray.forEach(function(aElement, aIndex) {
1610           aAddParams(params, internal_id, aElement, aIndex);
1611         });
1613         stmt.bindParameters(params);
1614         stmts.push(stmt);
1615       }
1617       // Initialize statements to insert developers and screenshots
1618       initializeArrayInsert("insertDeveloper", aAddon.developers,
1619                             self._addDeveloperParams);
1620       initializeArrayInsert("insertScreenshot", aAddon.screenshots,
1621                             self._addScreenshotParams);
1623       // Immediately call callback if nothing to insert
1624       if (stmts.length == 0) {
1625         self.connection.commitTransaction();
1626         aCallback();
1627         return;
1628       }
1630       self.connection.executeAsync(stmts, stmts.length, {
1631         handleResult: function() {},
1632         handleError: self.asyncErrorLogger,
1633         handleCompletion: function(aReason) {
1634           if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) {
1635             ERROR("Error inserting developers and screenshots into database. Attempting to continue");
1636             self.connection.rollbackTransaction();
1637           }
1638           else {
1639             self.connection.commitTransaction();
1640           }
1642           aCallback();
1643         }
1644       });
1645     }
1647     // Insert add-on into database
1648     this._makeAddonStatement(aAddon).executeAsync({
1649       handleResult: function() {},
1650       handleError: self.asyncErrorLogger,
1652       handleCompletion: function(aReason) {
1653         if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) {
1654           ERROR("Error inserting add-ons into database. Attempting to continue.");
1655           self.connection.rollbackTransaction();
1656           aCallback();
1657           return;
1658         }
1660         internal_id = self.connection.lastInsertRowID;
1661         insertDevelopersAndScreenshots();
1662       }
1663     });
1664   },
1666   /**
1667    * Make an asynchronous statement that will insert the specified add-on
1668    *
1669    * @param  aAddon
1670    *         The add-on to make the statement for
1671    * @return The asynchronous mozIStorageStatement
1672    */
1673   _makeAddonStatement: function AD__makeAddonStatement(aAddon) {
1674     let stmt = this.getStatement("insertAddon");
1675     let params = stmt.params;
1677     PROP_SINGLE.forEach(function(aProperty) {
1678       switch (aProperty) {
1679         case "sourceURI":
1680           params.sourceURI = aAddon.sourceURI ? aAddon.sourceURI.spec : null;
1681           break;
1682         case "creator":
1683           params.creator =  aAddon.creator ? aAddon.creator.name : null;
1684           params.creatorURL =  aAddon.creator ? aAddon.creator.url : null;
1685           break;
1686         case "updateDate":
1687           params.updateDate = aAddon.updateDate ? aAddon.updateDate.getTime() : null;
1688           break;
1689         default:
1690           params[aProperty] = aAddon[aProperty];
1691       }
1692     });
1694     return stmt;
1695   },
1697   /**
1698    * Add developer parameters to the specified mozIStorageBindingParamsArray
1699    *
1700    * @param  aParams
1701    *         The mozIStorageBindingParamsArray to add the parameters to
1702    * @param  aInternalID
1703    *         The internal_id of the add-on that this developer is for
1704    * @param  aDeveloper
1705    *         The developer to make the parameters from
1706    * @param  aIndex
1707    *         The index of this developer
1708    * @return The asynchronous mozIStorageStatement
1709    */
1710   _addDeveloperParams: function AD__addDeveloperParams(aParams, aInternalID,
1711                                                        aDeveloper, aIndex) {
1712     let bp = aParams.newBindingParams();
1713     bp.bindByName("addon_internal_id", aInternalID);
1714     bp.bindByName("num", aIndex);
1715     bp.bindByName("name", aDeveloper.name);
1716     bp.bindByName("url", aDeveloper.url);
1717     aParams.addParams(bp);
1718   },
1720   /**
1721    * Add screenshot parameters to the specified mozIStorageBindingParamsArray
1722    *
1723    * @param  aParams
1724    *         The mozIStorageBindingParamsArray to add the parameters to
1725    * @param  aInternalID
1726    *         The internal_id of the add-on that this screenshot is for
1727    * @param  aScreenshot
1728    *         The screenshot to make the parameters from
1729    * @param  aIndex
1730    *         The index of this screenshot
1731    */
1732   _addScreenshotParams: function AD__addScreenshotParams(aParams, aInternalID,
1733                                                          aScreenshot, aIndex) {
1734     let bp = aParams.newBindingParams();
1735     bp.bindByName("addon_internal_id", aInternalID);
1736     bp.bindByName("num", aIndex);
1737     bp.bindByName("url", aScreenshot.url);
1738     bp.bindByName("thumbnailURL", aScreenshot.thumbnailURL);
1739     bp.bindByName("caption", aScreenshot.caption);
1740     aParams.addParams(bp);
1741   },
1743   /**
1744    * Make add-on from an asynchronous row
1745    * Note: This add-on will be lacking both developers and screenshots
1746    *
1747    * @param  aRow
1748    *         The asynchronous row to use
1749    * @return The created add-on
1750    */
1751   _makeAddonFromAsyncRow: function AD__makeAddonFromAsyncRow(aRow) {
1752     let addon = {};
1754     PROP_SINGLE.forEach(function(aProperty) {
1755       let value = aRow.getResultByName(aProperty);
1757       switch (aProperty) {
1758         case "sourceURI":
1759           addon.sourceURI = value ? NetUtil.newURI(value) : null;
1760           break;
1761         case "creator":
1762           let creatorURL = aRow.getResultByName("creatorURL");
1763           if (value || creatorURL)
1764             addon.creator = new AddonManagerPrivate.AddonAuthor(value, creatorURL);
1765           else
1766             addon.creator = null;
1767           break;
1768         case "updateDate":
1769           addon.updateDate = value ? new Date(value) : null;
1770           break;
1771         default:
1772           addon[aProperty] = value;
1773       }
1774     });
1776     return addon;
1777   },
1779   /**
1780    * Make a developer from an asynchronous row
1781    *
1782    * @param  aRow
1783    *         The asynchronous row to use
1784    * @return The created developer
1785    */
1786   _makeDeveloperFromAsyncRow: function AD__makeDeveloperFromAsyncRow(aRow) {
1787     let name = aRow.getResultByName("name");
1788     let url = aRow.getResultByName("url")
1789     return new AddonManagerPrivate.AddonAuthor(name, url);
1790   },
1792   /**
1793    * Make a screenshot from an asynchronous row
1794    *
1795    * @param  aRow
1796    *         The asynchronous row to use
1797    * @return The created screenshot
1798    */
1799   _makeScreenshotFromAsyncRow: function AD__makeScreenshotFromAsyncRow(aRow) {
1800     let url = aRow.getResultByName("url");
1801     let thumbnailURL = aRow.getResultByName("thumbnailURL");
1802     let caption =aRow.getResultByName("caption");
1803     return new AddonManagerPrivate.AddonScreenshot(url, thumbnailURL, caption);
1804   },
1806   /**
1807    * Synchronously creates the schema in the database.
1808    */
1809   _createSchema: function AD__createSchema() {
1810     LOG("Creating database schema");
1811     this.connection.beginTransaction();
1813     // Any errors in here should rollback
1814     try {
1815       this.connection.createTable("addon",
1816                                   "internal_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
1817                                   "id TEXT UNIQUE, " +
1818                                   "type TEXT, " +
1819                                   "name TEXT, " +
1820                                   "version TEXT, " +
1821                                   "creator TEXT, " +
1822                                   "creatorURL TEXT, " +
1823                                   "description TEXT, " +
1824                                   "fullDescription TEXT, " +
1825                                   "developerComments TEXT, " +
1826                                   "eula TEXT, " +
1827                                   "iconURL TEXT, " +
1828                                   "homepageURL TEXT, " +
1829                                   "supportURL TEXT, " +
1830                                   "contributionURL TEXT, " +
1831                                   "contributionAmount TEXT, " +
1832                                   "averageRating INTEGER, " +
1833                                   "reviewCount INTEGER, " +
1834                                   "reviewURL TEXT, " +
1835                                   "totalDownloads INTEGER, " +
1836                                   "weeklyDownloads INTEGER, " +
1837                                   "dailyUsers INTEGER, " +
1838                                   "sourceURI TEXT, " +
1839                                   "repositoryStatus INTEGER, " +
1840                                   "size INTEGER, " +
1841                                   "updateDate INTEGER");
1843       this.connection.createTable("developer",
1844                                   "addon_internal_id INTEGER, " +
1845                                   "num INTEGER, " +
1846                                   "name TEXT, " +
1847                                   "url TEXT, " +
1848                                   "PRIMARY KEY (addon_internal_id, num)");
1850       this.connection.createTable("screenshot",
1851                                   "addon_internal_id INTEGER, " +
1852                                   "num INTEGER, " +
1853                                   "url TEXT, " +
1854                                   "thumbnailURL TEXT, " +
1855                                   "caption TEXT, " +
1856                                   "PRIMARY KEY (addon_internal_id, num)");
1858       this.connection.executeSimpleSQL("CREATE TRIGGER delete_addon AFTER DELETE " +
1859         "ON addon BEGIN " +
1860         "DELETE FROM developer WHERE addon_internal_id=old.internal_id; " +
1861         "DELETE FROM screenshot WHERE addon_internal_id=old.internal_id; " +
1862         "END");
1864       this.connection.schemaVersion = DB_SCHEMA;
1865       this.connection.commitTransaction();
1866     } catch (e) {
1867       ERROR("Failed to create database schema", e);
1868       this.logSQLError(this.connection.lastError, this.connection.lastErrorString);
1869       this.connection.rollbackTransaction();
1870       throw e;
1871     }
1872   }