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
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.
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";
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);
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",
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 = {
98 homepage: "homepageURL",
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",
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) {
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");
131 converter.convert("text/html", input, input.data.length, "text/unicode",
134 if (output.value instanceof Ci.nsISupportsString)
135 return output.value.data.replace("\r\n", "\n", "g");
139 function getAddonsToCache(aIds, aCallback) {
141 var types = Services.prefs.getCharPref(PREF_GETADDONS_CACHE_TYPES);
145 types = DEFAULT_CACHE_TYPES;
147 types = types.split(",");
149 AddonManager.getAddonsByIDs(aIds, function(aAddons) {
151 for (var i = 0; i < aIds.length; i++) {
152 var preference = PREF_GETADDONS_CACHE_ID_ENABLED.replace("%ID%", aIds[i]);
154 if (!Services.prefs.getBoolPref(preference))
157 // If the preference doesn't exist caching is enabled by default
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))
165 enabledIds.push(aIds[i]);
168 aCallback(enabledIds);
172 function AddonSearchResult(aId) {
176 AddonSearchResult.prototype = {
178 * The ID of the add-on
183 * The add-on type (e.g. "extension" or "theme")
188 * The name of the add-on
193 * The version of the add-on
198 * The creator of the add-on
203 * The developers of the add-on
208 * A short description of the add-on
213 * The full description of the add-on
215 fullDescription: null,
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)
222 developerComments: null,
225 * The end-user licensing agreement (EULA) of the add-on
230 * The url of the add-on's icon
235 * An array of screenshot urls for the add-on
240 * The homepage for the add-on
245 * The support URL for the add-on
250 * The contribution url of the add-on
252 contributionURL: null,
255 * The suggested contribution amount
257 contributionAmount: null,
260 * The URL to visit in order to purchase the add-on
265 * The numerical cost of the add-on in some currency, for sorting purposes
268 purchaseAmount: null,
271 * The display cost of the add-on, for display purposes only
273 purchaseDisplayAmount: null,
276 * The rating of the add-on, 0-5
281 * The number of reviews for this add-on
286 * The URL to the list of reviews for this add-on
291 * The total number of times the add-on was downloaded
293 totalDownloads: null,
296 * The number of times the add-on was downloaded the current week
298 weeklyDownloads: null,
301 * The number of daily users for the add-on
306 * AddonInstall object generated from the add-on XPI url
311 * nsIURI storing where this add-on was installed from
316 * The status of the add-on in the repository (e.g. 4 = "Public")
318 repositoryStatus: null,
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.
327 * The Date that the add-on was most recently updated
332 * True or false depending on whether the add-on is compatible with the
333 * current version of the application
338 * True or false depending on whether the add-on is compatible with the
341 isPlatformCompatible: true,
344 * True if the add-on has a secure means of updating
346 providesUpdatesSecurely: true,
349 * The current blocklist state of the add-on
351 blocklistState: Ci.nsIBlocklistService.STATE_NOT_BLOCKED,
354 * True if this add-on cannot be used in the application based on version
355 * compatibility, dependencies and blocklisting
360 * True if the user wants this add-on to be disabled
365 * Indicates what scope the add-on is installed in, per profile, user,
366 * system or application
368 scope: AddonManager.SCOPE_PROFILE,
371 * True if the add-on is currently functional
376 * A bitfield holding all of the current operations that are waiting to be
377 * performed for this add-on
379 pendingOperations: AddonManager.PENDING_NONE,
382 * A bitfield holding all the the operations that can be performed on
388 * Tests whether this add-on is known to be compatible with a
389 * particular application and platform version.
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
397 isCompatibleWith: function(aAppVerison, aPlatformVersion) {
402 * Starts an update check for this add-on. This will perform
403 * asynchronously and deliver results to the given listener.
406 * An UpdateListener for the update process
408 * A reason code for performing the update
410 * An application version to check for updates for
411 * @param aPlatformVersion
412 * A platform version to check for updates for
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);
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
435 var AddonRepository = {
437 * Whether caching is currently enabled
440 // Act as though caching is disabled if there was an unrecoverable error
441 // openning the database.
442 if (!AddonDatabase.databaseOk)
445 let preference = PREF_GETADDONS_CACHE_ENABLED;
448 enabled = Services.prefs.getBoolPref(preference);
450 WARN("cacheEnabled: Couldn't get pref: " + preference);
456 // A cache of the add-ons stored in the database
459 // An array of callbacks pending the retrieval of add-ons from AddonDatabase
460 _pendingCallbacks: null,
462 // Whether a search is currently in progress
465 // XHR associated with the current request
469 * Addon search results callback object that contains two functions
471 * searchSucceeded - Called when a search has suceeded.
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.
478 * The length of aAddons
479 * @param aTotalResults
480 * The total results actually available in the repository
483 * searchFailed - Called when an error occurred when performing a search.
487 // Maximum number of results to return
491 * Initialize AddonRepository.
493 initialize: function() {
494 Services.obs.addObserver(this, "xpcom-shutdown", false);
498 * Observe xpcom-shutdown notification, so we can shutdown cleanly.
500 observe: function (aSubject, aTopic, aData) {
501 if (aTopic == "xpcom-shutdown") {
502 Services.obs.removeObserver(this, "xpcom-shutdown");
508 * Shut down AddonRepository
510 shutdown: function() {
514 this._pendingCallbacks = null;
515 AddonDatabase.shutdown(function() {
516 Services.obs.notifyObservers(null, "addon-repository-shutdown", null);
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.
526 * The id of the add-on to get
528 * The callback to pass the result back to
530 getCachedAddonByID: function(aId, aCallback) {
531 if (!aId || !this.cacheEnabled) {
537 function getAddon(aAddons) {
538 aCallback((aId in aAddons) ? aAddons[aId] : null);
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)
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));
564 // Data is being retrieved from the database, so wait
565 this._pendingCallbacks.push(getAddon);
569 // Data has been retrieved, so immediately return result
570 getAddon(this._addons);
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.
579 * The array of add-on ids to repopulate the cache with
581 * The optional callback to call once complete
583 repopulateCache: function(aIds, aCallback) {
584 // Completely remove cache if caching is not enabled
585 if (!this.cacheEnabled) {
587 this._pendingCallbacks = null;
588 AddonDatabase.delete(aCallback);
593 getAddonsToCache(aIds, function(aAddons) {
594 // Completely remove cache if there are no add-ons to cache
595 if (aAddons.length == 0) {
597 this._pendingCallbacks = null;
598 AddonDatabase.delete(aCallback);
602 self.getAddonsByIDs(aAddons, {
603 searchSucceeded: function(aAddons) {
605 aAddons.forEach(function(aAddon) { self._addons[aAddon.id] = aAddon; });
606 AddonDatabase.repopulate(aAddons, aCallback);
608 searchFailed: function() {
609 WARN("Search failed when repopulating cache");
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.
623 * The array of add-on ids to add to the cache
625 * The optional callback to call once complete
627 cacheAddons: function(aIds, aCallback) {
628 if (!this.cacheEnabled) {
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) {
643 self.getAddonsByIDs(aAddons, {
644 searchSucceeded: function(aAddons) {
645 aAddons.forEach(function(aAddon) { self._addons[aAddon.id] = aAddon; });
646 AddonDatabase.insertAddons(aAddons, aCallback);
648 searchFailed: function() {
649 WARN("Search failed when adding add-ons to cache");
658 * The homepage for visiting this repository. If the corresponding preference
659 * is not defined, defaults to about:blank.
662 let url = this._formatURLPref(PREF_GETADDONS_BROWSEADDONS, {});
663 return (url != null) ? url : "about:blank";
667 * Returns whether this instance is currently performing a search. New
668 * searches will not be performed while this is the case.
671 return this._searching;
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.
678 getRecommendedURL: function() {
679 let url = this._formatURLPref(PREF_GETADDONS_BROWSERECOMMENDED, {});
680 return (url != null) ? url : "about:blank";
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
688 * @param aSearchTerms
689 * Search terms used to search the repository
691 getSearchURL: function(aSearchTerms) {
692 let url = this._formatURLPref(PREF_GETADDONS_BROWSESEARCHRESULTS, {
693 TERMS : encodeURIComponent(aSearchTerms)
695 return (url != null) ? url : "about:blank";
699 * Cancels the search in progress. If there is no search in progress this
702 cancelSearch: function() {
703 this._searching = false;
705 this._request.abort();
706 this._request = null;
708 this._callback = null;
712 * Begins a search for add-ons in this repository by ID. Results will be
713 * passed to the given callback.
716 * The array of ids to search for
718 * The callback to pass results to
720 getAddonsByIDs: function(aIDs, aCallback) {
721 let startupInfo = Cc["@mozilla.org/toolkit/app-startup;1"].
722 getService(Ci.nsIAppStartup_MOZILLA_2_0).
725 let ids = aIDs.slice(0);
728 API_VERSION : API_VERSION,
729 IDS : ids.map(encodeURIComponent).join(',')
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 -
742 let url = this._formatURLPref(PREF_GETADDONS_BYIDS, params);
745 function handleResults(aElements, aTotalResults) {
746 // Don't use this._parseAddons() so that, for example,
747 // incompatible add-ons are not filtered out
749 for (let i = 0; i < aElements.length && results.length < self._maxResults; i++) {
750 let result = self._parseAddon(aElements[i]);
754 // Ignore add-on if it wasn't actually requested
755 let idIndex = ids.indexOf(result.addon.id);
759 results.push(result);
760 // Ignore this add-on from now on
761 ids.splice(idIndex, 1);
764 // aTotalResults irrelevant
765 self._reportSuccess(results, -1);
768 this._beginSearch(url, ids.length, aCallback, handleResults);
772 * Begins a search for recommended add-ons in this repository. Results will
773 * be passed to the given callback.
776 * The maximum number of results to return
778 * The callback to pass results to
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
789 function handleResults(aElements, aTotalResults) {
790 self._getLocalAddonIds(function(aLocalAddonIds) {
791 // aTotalResults irrelevant
792 self._parseAddons(aElements, -1, aLocalAddonIds);
796 this._beginSearch(url, aMaxResults, aCallback, handleResults);
800 * Begins a search for add-ons in this repository. Results will be passed to
801 * the given callback.
803 * @param aSearchTerms
804 * The terms to search for
806 * The maximum number of results to return
808 * The callback to pass results to
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
820 function handleResults(aElements, aTotalResults) {
821 self._getLocalAddonIds(function(aLocalAddonIds) {
822 self._parseAddons(aElements, aTotalResults, aLocalAddonIds);
826 this._beginSearch(url, aMaxResults, aCallback, handleResults);
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);
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();
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;
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;
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;
870 * Creates an AddonSearchResult by parsing an <addon> element
873 * The <addon> element to parse
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.
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)
887 let addon = new AddonSearchResult(guid);
895 for (let node = aElement.firstChild; node; node = node.nextSibling) {
896 if (!(node instanceof Ci.nsIDOMElement))
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);
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));
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));
917 addon[INTEGER_KEY_MAP[localName]] = value;
921 // Handle cases that aren't as simple as grabbing the text content
924 // Map AMO's type id to corresponding string
925 let id = parseInt(node.getAttribute("id"));
928 addon.type = "extension";
931 addon.type = "theme";
934 WARN("Unknown type id when parsing addon: " + id);
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)
945 let author = new AddonManagerPrivate.AddonAuthor(name, link);
946 if (addon.creator == null)
947 addon.creator = author;
949 if (addon.developers == null)
950 addon.developers = [];
952 addon.developers.push(author);
957 let previewNodes = node.getElementsByTagName("preview");
958 Array.forEach(previewNodes, function(aPreviewNode) {
959 let full = self._getDescendantTextContent(aPreviewNode, "full");
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);
973 addon.screenshots.push(screenshot);
977 addon.homepageURL = addon.homepageURL || this._getTextContent(node);
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;
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;
999 let averageRating = parseInt(this._getTextContent(node));
1000 if (averageRating >= 0)
1001 addon.averageRating = Math.min(5, averageRating);
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;
1012 let repositoryStatus = parseInt(node.getAttribute("id"));
1013 if (!isNaN(repositoryStatus))
1014 addon.repositoryStatus = repositoryStatus;
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();
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())
1032 let xpiURL = this._getTextContent(node);
1036 if (skipSourceURIs.indexOf(xpiURL) != -1)
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;
1050 case "last_updated":
1051 let epoch = parseInt(node.getAttribute("epoch"));
1053 addon.updateDate = new Date(1000 * epoch);
1061 _parseAddons: function(aElements, aTotalResults, aSkip) {
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)
1073 // Ignore add-ons not compatible with this Application
1074 let tags = this._getUniqueDescendant(element, "compatible_applications");
1078 let applications = tags.getElementsByTagName("appID");
1079 let compatible = Array.some(applications, function(aAppNode) {
1080 if (self._getTextContent(aAppNode) != Services.appinfo.ID)
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)
1089 let currentVersion = Services.appinfo.version;
1090 return (Services.vc.compare(minVersion, currentVersion) <= 0 &&
1091 Services.vc.compare(currentVersion, maxVersion) <= 0);
1097 // Add-on meets all requirements, so parse out data
1098 let result = this._parseAddon(element, aSkip);
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]))
1107 // Add only if the add-on is compatible with the platform
1108 if (!result.addon.isPlatformCompatible)
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)
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);
1121 // Immediately report success if no AddonInstall instances to create
1122 let pendingResults = results.length;
1123 if (pendingResults == 0) {
1124 this._reportSuccess(results, aTotalResults);
1128 // Create an AddonInstall for each result
1130 results.forEach(function(aResult) {
1131 let addon = aResult.addon;
1132 let callback = function(aInstall) {
1133 addon.install = aInstall;
1135 if (pendingResults == 0)
1136 self._reportSuccess(results, aTotalResults);
1139 if (aResult.xpiURL) {
1140 AddonManager.getInstallForURL(aResult.xpiURL, callback,
1141 "application/x-xpinstall", aResult.xpiHash,
1142 addon.name, addon.iconURL, addon.version);
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();
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");
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();
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);
1190 this._request.send(null);
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) {
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);
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);
1212 if (localAddonIds.ids)
1213 aCallback(localAddonIds);
1217 // Create url from preference, returning null if preference does not exist
1218 _formatURLPref: function(aPreference, aSubstitutions) {
1221 url = Services.prefs.getCharPref(aPreference);
1223 WARN("_formatURLPref: Couldn't get pref: " + aPreference);
1227 url = url.replace(/%([A-Z_]+)%/g, function(aMatch, aKey) {
1228 return (aKey in aSubstitutions) ? aSubstitutions[aKey] : aMatch;
1231 return Services.urlFormatter.formatURL(url);
1234 AddonRepository.initialize();
1236 var AddonDatabase = {
1237 // true if the database connection has been opened
1239 // false if there was an unrecoverable error openning the database
1241 // A cache of statements that are used and need to be finalized on shutdown
1244 // The statements used by the database
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 " +
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"
1277 * A helper function to log an SQL error.
1280 * The storage error code associated with the error
1281 * @param aErrorString
1284 logSQLError: function AD_logSQLError(aError, aErrorString) {
1285 ERROR("SQL error " + aError + ": " + aErrorString);
1289 * A helper function to log any errors that occur during async statements.
1292 * A mozIStorageError to log
1294 asyncErrorLogger: function AD_asyncErrorLogger(aError) {
1295 ERROR("Async SQL error " + aError.result + ": " + aError.message);
1299 * Synchronously opens a new connection to the database file.
1301 * @param aSecondAttempt
1302 * Whether this is a second attempt to open the database
1303 * @return the mozIStorageConnection for the database
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();
1313 this.connection = Services.storage.openUnsharedDatabase(dbfile);
1315 this.initialized = false;
1316 ERROR("Failed to open database", e);
1317 if (aSecondAttempt || dbMissing) {
1318 this.databaseOk = false;
1322 LOG("Deleting database, and attempting openConnection again");
1323 dbfile.remove(false);
1324 return this.openConnection(true);
1327 this.connection.executeSimpleSQL("PRAGMA locking_mode = EXCLUSIVE");
1328 if (dbMissing || this.connection.schemaVersion == 0)
1329 this._createSchema();
1331 return this.connection;
1335 * A lazy getter for the database connection.
1338 return this.openConnection();
1342 * Asynchronously shuts down the database connection and releases all
1346 * An optional callback to call once complete
1348 shutdown: function AD_shutdown(aCallback) {
1349 this.databaseOk = true;
1350 if (!this.initialized) {
1356 this.initialized = false;
1358 for each (let stmt in this.statementCache)
1360 this.statementCache = {};
1362 if (this.connection.transactionInProgress) {
1363 ERROR("Outstanding transaction, rolling back.");
1364 this.connection.rollbackTransaction();
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();
1376 connection.asyncClose(aCallback);
1380 * Asynchronously deletes the database, shutting down the connection
1381 * first if initialized
1384 * An optional callback to call once complete
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);
1398 * Gets a cached statement or creates a new statement if it doesn't already
1402 * A unique key to reference the statement
1403 * @return a mozIStorageStatement for the SQL corresponding to the unique key
1405 getStatement: function AD_getStatement(aKey) {
1406 if (aKey in this.statementCache)
1407 return this.statementCache[aKey];
1409 let sql = this.statements[aKey];
1411 return this.statementCache[aKey] = this.connection.createStatement(sql);
1413 ERROR("Error creating statement " + aKey + " (" + aSql + ")");
1419 * Asynchronously retrieve all add-ons from the database, and pass it
1420 * to the specified callback
1423 * The callback to pass the add-ons back to
1425 retrieveStoredData: function AD_retrieveStoredData(aCallback) {
1429 // Retrieve all data from the addon table
1430 function getAllAddons() {
1431 self.getStatement("getAllAddons").executeAsync({
1432 handleResult: function(aResults) {
1434 while (row = aResults.getNextRow()) {
1435 let internal_id = row.getResultByName("internal_id");
1436 addons[internal_id] = self._makeAddonFromAsyncRow(row);
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");
1454 // Retrieve all data from the developer table
1455 function getAllDevelopers() {
1456 self.getStatement("getAllDevelopers").executeAsync({
1457 handleResult: function(aResults) {
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");
1466 let addon = addons[addon_internal_id];
1467 if (!addon.developers)
1468 addon.developers = [];
1470 addon.developers.push(self._makeDeveloperFromAsyncRow(row));
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");
1483 getAllScreenshots();
1488 // Retrieve all data from the screenshot table
1489 function getAllScreenshots() {
1490 self.getStatement("getAllScreenshots").executeAsync({
1491 handleResult: function(aResults) {
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");
1500 let addon = addons[addon_internal_id];
1501 if (!addon.screenshots)
1502 addon.screenshots = [];
1503 addon.screenshots.push(self._makeScreenshotFromAsyncRow(row));
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");
1516 let returnedAddons = {};
1517 for each (addon in addons)
1518 returnedAddons[addon.id] = addon;
1519 aCallback(returnedAddons);
1524 // Begin asynchronous process
1529 * Asynchronously repopulates the database so it only contains the
1533 * The array of add-ons to repopulate the database with
1535 * An optional callback to call once complete
1537 repopulate: function AD_repopulate(aAddons, aCallback) {
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);
1558 * Asynchronously inserts an array of add-ons into the database
1561 * The array of add-ons to insert
1563 * An optional callback to call once complete
1565 insertAddons: function AD_insertAddons(aAddons, aCallback) {
1567 let currentAddon = -1;
1570 function insertNextAddon() {
1571 if (++currentAddon == aAddons.length) {
1577 self._insertAddon(aAddons[currentAddon], insertNextAddon);
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
1589 * The add-on to insert into the database
1591 * The callback to call once complete
1593 _insertAddon: function AD__insertAddon(aAddon, aCallback) {
1595 let internal_id = null;
1596 this.connection.beginTransaction();
1598 // Simultaneously insert the developers and screenshots of the add-on
1599 function insertDevelopersAndScreenshots() {
1602 // Initialize statement and parameters for inserting an array
1603 function initializeArrayInsert(aStatementKey, aArray, aAddParams) {
1604 if (!aArray || aArray.length == 0)
1607 let stmt = self.getStatement(aStatementKey);
1608 let params = stmt.newBindingParamsArray();
1609 aArray.forEach(function(aElement, aIndex) {
1610 aAddParams(params, internal_id, aElement, aIndex);
1613 stmt.bindParameters(params);
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();
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();
1639 self.connection.commitTransaction();
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();
1660 internal_id = self.connection.lastInsertRowID;
1661 insertDevelopersAndScreenshots();
1667 * Make an asynchronous statement that will insert the specified add-on
1670 * The add-on to make the statement for
1671 * @return The asynchronous mozIStorageStatement
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) {
1680 params.sourceURI = aAddon.sourceURI ? aAddon.sourceURI.spec : null;
1683 params.creator = aAddon.creator ? aAddon.creator.name : null;
1684 params.creatorURL = aAddon.creator ? aAddon.creator.url : null;
1687 params.updateDate = aAddon.updateDate ? aAddon.updateDate.getTime() : null;
1690 params[aProperty] = aAddon[aProperty];
1698 * Add developer parameters to the specified mozIStorageBindingParamsArray
1701 * The mozIStorageBindingParamsArray to add the parameters to
1702 * @param aInternalID
1703 * The internal_id of the add-on that this developer is for
1705 * The developer to make the parameters from
1707 * The index of this developer
1708 * @return The asynchronous mozIStorageStatement
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);
1721 * Add screenshot parameters to the specified mozIStorageBindingParamsArray
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
1730 * The index of this screenshot
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);
1744 * Make add-on from an asynchronous row
1745 * Note: This add-on will be lacking both developers and screenshots
1748 * The asynchronous row to use
1749 * @return The created add-on
1751 _makeAddonFromAsyncRow: function AD__makeAddonFromAsyncRow(aRow) {
1754 PROP_SINGLE.forEach(function(aProperty) {
1755 let value = aRow.getResultByName(aProperty);
1757 switch (aProperty) {
1759 addon.sourceURI = value ? NetUtil.newURI(value) : null;
1762 let creatorURL = aRow.getResultByName("creatorURL");
1763 if (value || creatorURL)
1764 addon.creator = new AddonManagerPrivate.AddonAuthor(value, creatorURL);
1766 addon.creator = null;
1769 addon.updateDate = value ? new Date(value) : null;
1772 addon[aProperty] = value;
1780 * Make a developer from an asynchronous row
1783 * The asynchronous row to use
1784 * @return The created developer
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);
1793 * Make a screenshot from an asynchronous row
1796 * The asynchronous row to use
1797 * @return The created screenshot
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);
1807 * Synchronously creates the schema in the database.
1809 _createSchema: function AD__createSchema() {
1810 LOG("Creating database schema");
1811 this.connection.beginTransaction();
1813 // Any errors in here should rollback
1815 this.connection.createTable("addon",
1816 "internal_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
1817 "id TEXT UNIQUE, " +
1822 "creatorURL TEXT, " +
1823 "description TEXT, " +
1824 "fullDescription TEXT, " +
1825 "developerComments 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, " +
1841 "updateDate INTEGER");
1843 this.connection.createTable("developer",
1844 "addon_internal_id INTEGER, " +
1848 "PRIMARY KEY (addon_internal_id, num)");
1850 this.connection.createTable("screenshot",
1851 "addon_internal_id INTEGER, " +
1854 "thumbnailURL TEXT, " +
1856 "PRIMARY KEY (addon_internal_id, num)");
1858 this.connection.executeSimpleSQL("CREATE TRIGGER delete_addon AFTER DELETE " +
1860 "DELETE FROM developer WHERE addon_internal_id=old.internal_id; " +
1861 "DELETE FROM screenshot WHERE addon_internal_id=old.internal_id; " +
1864 this.connection.schemaVersion = DB_SCHEMA;
1865 this.connection.commitTransaction();
1867 ERROR("Failed to create database schema", e);
1868 this.logSQLError(this.connection.lastError, this.connection.lastErrorString);
1869 this.connection.rollbackTransaction();