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