|
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/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 const Cc = Components.classes; |
|
8 const Ci = Components.interfaces; |
|
9 const Cu = Components.utils; |
|
10 const Cr = Components.results; |
|
11 |
|
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"); |
|
15 |
|
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"); |
|
28 |
|
29 this.EXPORTED_SYMBOLS = [ "AddonRepository" ]; |
|
30 |
|
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" |
|
42 |
|
43 const XMLURI_PARSE_ERROR = "http://www.mozilla.org/newlayout/xml/parsererror.xml"; |
|
44 |
|
45 const API_VERSION = "1.5"; |
|
46 const DEFAULT_CACHE_TYPES = "extension,theme,locale,dictionary"; |
|
47 |
|
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; |
|
53 |
|
54 const BLANK_DB = function() { |
|
55 return { |
|
56 addons: new Map(), |
|
57 schema: DB_SCHEMA |
|
58 }; |
|
59 } |
|
60 |
|
61 const TOOLKIT_ID = "toolkit@mozilla.org"; |
|
62 |
|
63 Cu.import("resource://gre/modules/Log.jsm"); |
|
64 const LOGGER_ID = "addons.repository"; |
|
65 |
|
66 // Create a new logger for use by the Addons Repository |
|
67 // (Requires AddonManager.jsm) |
|
68 let logger = Log.repository.getLogger(LOGGER_ID); |
|
69 |
|
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 }; |
|
78 |
|
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 }; |
|
87 |
|
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 }; |
|
95 |
|
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"); |
|
99 |
|
100 function convertHTMLToPlainText(html) { |
|
101 if (!html) |
|
102 return html; |
|
103 var converter = Cc["@mozilla.org/widget/htmlformatconverter;1"]. |
|
104 createInstance(Ci.nsIFormatConverter); |
|
105 |
|
106 var input = Cc["@mozilla.org/supports-string;1"]. |
|
107 createInstance(Ci.nsISupportsString); |
|
108 input.data = html.replace(/\n/g, "<br>"); |
|
109 |
|
110 var output = {}; |
|
111 converter.convert("text/html", input, input.data.length, "text/unicode", |
|
112 output, {}); |
|
113 |
|
114 if (output.value instanceof Ci.nsISupportsString) |
|
115 return output.value.data.replace(/\r\n/g, "\n"); |
|
116 return html; |
|
117 } |
|
118 |
|
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; |
|
126 |
|
127 types = types.split(","); |
|
128 |
|
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 } |
|
139 |
|
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; |
|
144 |
|
145 enabledIds.push(aIds[i]); |
|
146 } |
|
147 |
|
148 aCallback(enabledIds); |
|
149 }); |
|
150 } |
|
151 |
|
152 function AddonSearchResult(aId) { |
|
153 this.id = aId; |
|
154 this.icons = {}; |
|
155 this._unsupportedProperties = {}; |
|
156 } |
|
157 |
|
158 AddonSearchResult.prototype = { |
|
159 /** |
|
160 * The ID of the add-on |
|
161 */ |
|
162 id: null, |
|
163 |
|
164 /** |
|
165 * The add-on type (e.g. "extension" or "theme") |
|
166 */ |
|
167 type: null, |
|
168 |
|
169 /** |
|
170 * The name of the add-on |
|
171 */ |
|
172 name: null, |
|
173 |
|
174 /** |
|
175 * The version of the add-on |
|
176 */ |
|
177 version: null, |
|
178 |
|
179 /** |
|
180 * The creator of the add-on |
|
181 */ |
|
182 creator: null, |
|
183 |
|
184 /** |
|
185 * The developers of the add-on |
|
186 */ |
|
187 developers: null, |
|
188 |
|
189 /** |
|
190 * A short description of the add-on |
|
191 */ |
|
192 description: null, |
|
193 |
|
194 /** |
|
195 * The full description of the add-on |
|
196 */ |
|
197 fullDescription: null, |
|
198 |
|
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, |
|
205 |
|
206 /** |
|
207 * The end-user licensing agreement (EULA) of the add-on |
|
208 */ |
|
209 eula: null, |
|
210 |
|
211 /** |
|
212 * The url of the add-on's icon |
|
213 */ |
|
214 get iconURL() { |
|
215 return this.icons && this.icons[32]; |
|
216 }, |
|
217 |
|
218 /** |
|
219 * The URLs of the add-on's icons, as an object with icon size as key |
|
220 */ |
|
221 icons: null, |
|
222 |
|
223 /** |
|
224 * An array of screenshot urls for the add-on |
|
225 */ |
|
226 screenshots: null, |
|
227 |
|
228 /** |
|
229 * The homepage for the add-on |
|
230 */ |
|
231 homepageURL: null, |
|
232 |
|
233 /** |
|
234 * The homepage for the add-on |
|
235 */ |
|
236 learnmoreURL: null, |
|
237 |
|
238 /** |
|
239 * The support URL for the add-on |
|
240 */ |
|
241 supportURL: null, |
|
242 |
|
243 /** |
|
244 * The contribution url of the add-on |
|
245 */ |
|
246 contributionURL: null, |
|
247 |
|
248 /** |
|
249 * The suggested contribution amount |
|
250 */ |
|
251 contributionAmount: null, |
|
252 |
|
253 /** |
|
254 * The URL to visit in order to purchase the add-on |
|
255 */ |
|
256 purchaseURL: null, |
|
257 |
|
258 /** |
|
259 * The numerical cost of the add-on in some currency, for sorting purposes |
|
260 * only |
|
261 */ |
|
262 purchaseAmount: null, |
|
263 |
|
264 /** |
|
265 * The display cost of the add-on, for display purposes only |
|
266 */ |
|
267 purchaseDisplayAmount: null, |
|
268 |
|
269 /** |
|
270 * The rating of the add-on, 0-5 |
|
271 */ |
|
272 averageRating: null, |
|
273 |
|
274 /** |
|
275 * The number of reviews for this add-on |
|
276 */ |
|
277 reviewCount: null, |
|
278 |
|
279 /** |
|
280 * The URL to the list of reviews for this add-on |
|
281 */ |
|
282 reviewURL: null, |
|
283 |
|
284 /** |
|
285 * The total number of times the add-on was downloaded |
|
286 */ |
|
287 totalDownloads: null, |
|
288 |
|
289 /** |
|
290 * The number of times the add-on was downloaded the current week |
|
291 */ |
|
292 weeklyDownloads: null, |
|
293 |
|
294 /** |
|
295 * The number of daily users for the add-on |
|
296 */ |
|
297 dailyUsers: null, |
|
298 |
|
299 /** |
|
300 * AddonInstall object generated from the add-on XPI url |
|
301 */ |
|
302 install: null, |
|
303 |
|
304 /** |
|
305 * nsIURI storing where this add-on was installed from |
|
306 */ |
|
307 sourceURI: null, |
|
308 |
|
309 /** |
|
310 * The status of the add-on in the repository (e.g. 4 = "Public") |
|
311 */ |
|
312 repositoryStatus: null, |
|
313 |
|
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, |
|
319 |
|
320 /** |
|
321 * The Date that the add-on was most recently updated |
|
322 */ |
|
323 updateDate: null, |
|
324 |
|
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, |
|
330 |
|
331 /** |
|
332 * True or false depending on whether the add-on is compatible with the |
|
333 * current platform |
|
334 */ |
|
335 isPlatformCompatible: true, |
|
336 |
|
337 /** |
|
338 * Array of AddonCompatibilityOverride objects, that describe overrides for |
|
339 * compatibility with an application versions. |
|
340 **/ |
|
341 compatibilityOverrides: null, |
|
342 |
|
343 /** |
|
344 * True if the add-on has a secure means of updating |
|
345 */ |
|
346 providesUpdatesSecurely: true, |
|
347 |
|
348 /** |
|
349 * The current blocklist state of the add-on |
|
350 */ |
|
351 blocklistState: Ci.nsIBlocklistService.STATE_NOT_BLOCKED, |
|
352 |
|
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, |
|
358 |
|
359 /** |
|
360 * True if the user wants this add-on to be disabled |
|
361 */ |
|
362 userDisabled: false, |
|
363 |
|
364 /** |
|
365 * Indicates what scope the add-on is installed in, per profile, user, |
|
366 * system or application |
|
367 */ |
|
368 scope: AddonManager.SCOPE_PROFILE, |
|
369 |
|
370 /** |
|
371 * True if the add-on is currently functional |
|
372 */ |
|
373 isActive: true, |
|
374 |
|
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, |
|
380 |
|
381 /** |
|
382 * A bitfield holding all the the operations that can be performed on |
|
383 * this add-on |
|
384 */ |
|
385 permissions: 0, |
|
386 |
|
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 }, |
|
400 |
|
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 }, |
|
422 |
|
423 toJSON: function() { |
|
424 let json = {}; |
|
425 |
|
426 for (let [property, value] of Iterator(this)) { |
|
427 if (property.startsWith("_") || |
|
428 typeof(value) === "function") |
|
429 continue; |
|
430 |
|
431 try { |
|
432 switch (property) { |
|
433 case "sourceURI": |
|
434 json.sourceURI = value ? value.spec : ""; |
|
435 break; |
|
436 |
|
437 case "updateDate": |
|
438 json.updateDate = value ? value.getTime() : ""; |
|
439 break; |
|
440 |
|
441 default: |
|
442 json[property] = value; |
|
443 } |
|
444 } catch (ex) { |
|
445 logger.warn("Error writing property value for " + property); |
|
446 } |
|
447 } |
|
448 |
|
449 for (let [property, value] of Iterator(this._unsupportedProperties)) { |
|
450 if (!property.startsWith("_")) |
|
451 json[property] = value; |
|
452 } |
|
453 |
|
454 return json; |
|
455 } |
|
456 } |
|
457 |
|
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 } |
|
480 |
|
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 } |
|
488 |
|
489 return enabled; |
|
490 }, |
|
491 |
|
492 // A cache of the add-ons stored in the database |
|
493 _addons: null, |
|
494 |
|
495 // An array of callbacks pending the retrieval of add-ons from AddonDatabase |
|
496 _pendingCallbacks: null, |
|
497 |
|
498 // Whether a migration in currently in progress |
|
499 _migrationInProgress: false, |
|
500 |
|
501 // A callback to be called when migration finishes |
|
502 _postMigrationCallback: null, |
|
503 |
|
504 // Whether a search is currently in progress |
|
505 _searching: false, |
|
506 |
|
507 // XHR associated with the current request |
|
508 _request: null, |
|
509 |
|
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, |
|
528 |
|
529 // Maximum number of results to return |
|
530 _maxResults: null, |
|
531 |
|
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(); |
|
539 |
|
540 this._addons = null; |
|
541 this._pendingCallbacks = null; |
|
542 return AddonDatabase.shutdown(false); |
|
543 }, |
|
544 |
|
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 } |
|
560 |
|
561 let self = this; |
|
562 function getAddon(aAddons) { |
|
563 aCallback((aId in aAddons) ? aAddons[aId] : null); |
|
564 } |
|
565 |
|
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; |
|
573 |
|
574 // Check if cache was shutdown or deleted before callback was called |
|
575 if (pendingCallbacks == null) |
|
576 return; |
|
577 |
|
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; |
|
582 |
|
583 pendingCallbacks.forEach(function(aCallback) aCallback(aAddons)); |
|
584 }); |
|
585 |
|
586 return; |
|
587 } |
|
588 |
|
589 // Data is being retrieved from the database, so wait |
|
590 this._pendingCallbacks.push(getAddon); |
|
591 return; |
|
592 } |
|
593 |
|
594 // Data has been retrieved, so immediately return result |
|
595 getAddon(this._addons); |
|
596 }, |
|
597 |
|
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 }, |
|
614 |
|
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 } |
|
620 |
|
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 } |
|
630 |
|
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 } |
|
641 |
|
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 }, |
|
655 |
|
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 } |
|
673 |
|
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 } |
|
682 |
|
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 }, |
|
696 |
|
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 }, |
|
705 |
|
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 }, |
|
713 |
|
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 }, |
|
722 |
|
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 }, |
|
737 |
|
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 }, |
|
750 |
|
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 }, |
|
763 |
|
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); |
|
780 |
|
781 let params = { |
|
782 API_VERSION : API_VERSION, |
|
783 IDS : ids.map(encodeURIComponent).join(',') |
|
784 }; |
|
785 |
|
786 let pref = PREF_GETADDONS_BYIDS; |
|
787 |
|
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; |
|
792 |
|
793 let startupInfo = Cc["@mozilla.org/toolkit/app-startup;1"]. |
|
794 getService(Ci.nsIAppStartup). |
|
795 getStartupInfo(); |
|
796 |
|
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 } |
|
815 |
|
816 let url = this._formatURLPref(pref, params); |
|
817 |
|
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; |
|
827 |
|
828 // Ignore add-on if it wasn't actually requested |
|
829 let idIndex = ids.indexOf(result.addon.id); |
|
830 if (idIndex == -1) |
|
831 continue; |
|
832 |
|
833 results.push(result); |
|
834 // Ignore this add-on from now on |
|
835 ids.splice(idIndex, 1); |
|
836 } |
|
837 |
|
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; |
|
843 |
|
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 } |
|
855 |
|
856 // aTotalResults irrelevant |
|
857 self._reportSuccess(results, -1); |
|
858 } |
|
859 |
|
860 this._beginSearch(url, ids.length, aCallback, handleResults, aTimeout); |
|
861 }, |
|
862 |
|
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 }, |
|
879 |
|
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, |
|
892 |
|
893 // Get twice as many results to account for potential filtering |
|
894 MAX_RESULTS : 2 * aMaxResults |
|
895 }); |
|
896 |
|
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 } |
|
904 |
|
905 this._beginSearch(url, aMaxResults, aCallback, handleResults); |
|
906 }, |
|
907 |
|
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"; |
|
925 |
|
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 }; |
|
933 |
|
934 let url = this._formatURLPref(PREF_GETADDONS_GETSEARCHRESULTS, substitutions); |
|
935 |
|
936 let self = this; |
|
937 function handleResults(aElements, aTotalResults) { |
|
938 self._getLocalAddonIds(function searchAddons_getLocalAddonIds(aLocalAddonIds) { |
|
939 self._parseAddons(aElements, aTotalResults, aLocalAddonIds); |
|
940 }); |
|
941 } |
|
942 |
|
943 this._beginSearch(url, aMaxResults, aCallback, handleResults); |
|
944 }, |
|
945 |
|
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 }, |
|
956 |
|
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 }, |
|
966 |
|
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 }, |
|
972 |
|
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 }, |
|
980 |
|
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 }, |
|
986 |
|
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 }, |
|
993 |
|
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 }, |
|
1001 |
|
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 : []; |
|
1018 |
|
1019 let guid = this._getDescendantTextContent(aElement, "guid"); |
|
1020 if (guid == null || skipIDs.indexOf(guid) != -1) |
|
1021 return null; |
|
1022 |
|
1023 let addon = new AddonSearchResult(guid); |
|
1024 let result = { |
|
1025 addon: addon, |
|
1026 xpiURL: null, |
|
1027 xpiHash: null |
|
1028 }; |
|
1029 |
|
1030 if (aCompatData && guid in aCompatData) |
|
1031 addon.compatibilityOverrides = aCompatData[guid].compatRanges; |
|
1032 |
|
1033 let self = this; |
|
1034 for (let node = aElement.firstChild; node; node = node.nextSibling) { |
|
1035 if (!(node instanceof Ci.nsIDOMElement)) |
|
1036 continue; |
|
1037 |
|
1038 let localName = node.localName; |
|
1039 |
|
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 } |
|
1046 |
|
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 } |
|
1052 |
|
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 } |
|
1060 |
|
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; |
|
1087 |
|
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 = []; |
|
1094 |
|
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; |
|
1105 |
|
1106 let fullURL = self._getTextContent(full); |
|
1107 let fullWidth = full.getAttribute("width"); |
|
1108 let fullHeight = full.getAttribute("height"); |
|
1109 |
|
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); |
|
1121 |
|
1122 if (addon.screenshots == null) |
|
1123 addon.screenshots = []; |
|
1124 |
|
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 } |
|
1187 |
|
1188 let xpiURL = this._getTextContent(node); |
|
1189 if (xpiURL == null) |
|
1190 break; |
|
1191 |
|
1192 if (skipSourceURIs.indexOf(xpiURL) != -1) |
|
1193 return null; |
|
1194 |
|
1195 result.xpiURL = xpiURL; |
|
1196 addon.sourceURI = NetUtil.newURI(xpiURL); |
|
1197 |
|
1198 let size = parseInt(node.getAttribute("size")); |
|
1199 addon.size = (size >= 0) ? size : null; |
|
1200 |
|
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 } |
|
1216 |
|
1217 return result; |
|
1218 }, |
|
1219 |
|
1220 _parseAddons: function AddonRepo_parseAddons(aElements, aTotalResults, aSkip) { |
|
1221 let self = this; |
|
1222 let results = []; |
|
1223 |
|
1224 function isSameApplication(aAppNode) { |
|
1225 return self._getTextContent(aAppNode) == Services.appinfo.ID; |
|
1226 } |
|
1227 |
|
1228 for (let i = 0; i < aElements.length && results.length < this._maxResults; i++) { |
|
1229 let element = aElements[i]; |
|
1230 |
|
1231 let tags = this._getUniqueDescendant(element, "compatible_applications"); |
|
1232 if (tags == null) |
|
1233 continue; |
|
1234 |
|
1235 let applications = tags.getElementsByTagName("appID"); |
|
1236 let compatible = Array.some(applications, function parseAddons_applicationsCompatFilter(aAppNode) { |
|
1237 if (!isSameApplication(aAppNode)) |
|
1238 return false; |
|
1239 |
|
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; |
|
1245 |
|
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 }); |
|
1251 |
|
1252 // Ignore add-ons not compatible with this Application |
|
1253 if (!compatible) { |
|
1254 if (AddonManager.checkCompatibility) |
|
1255 continue; |
|
1256 |
|
1257 if (!Array.some(applications, isSameApplication)) |
|
1258 continue; |
|
1259 } |
|
1260 |
|
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; |
|
1267 |
|
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; |
|
1272 |
|
1273 // Add only if the add-on is compatible with the platform |
|
1274 if (!result.addon.isPlatformCompatible) |
|
1275 continue; |
|
1276 |
|
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; |
|
1281 |
|
1282 result.addon.isCompatible = compatible; |
|
1283 |
|
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 } |
|
1288 |
|
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 } |
|
1295 |
|
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 } |
|
1306 |
|
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 }, |
|
1317 |
|
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 } |
|
1325 |
|
1326 let compat = {id: guid}; |
|
1327 compat.hosted = aElement.getAttribute("hosted") != "false"; |
|
1328 |
|
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; |
|
1335 |
|
1336 let minVersion = this._getDescendantTextContent(node, "min_version"); |
|
1337 let maxVersion = this._getDescendantTextContent(node, "max_version"); |
|
1338 if (minVersion == null || maxVersion == null) |
|
1339 continue; |
|
1340 |
|
1341 let appRange = { appID: appID, |
|
1342 appMinVersion: minVersion, |
|
1343 appMaxVersion: maxVersion }; |
|
1344 |
|
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 } |
|
1353 |
|
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 } |
|
1361 |
|
1362 let override = new AddonManagerPrivate.AddonCompatibilityOverride(type); |
|
1363 |
|
1364 override.minVersion = this._getDirectDescendantTextContent(aNode, "min_version"); |
|
1365 override.maxVersion = this._getDirectDescendantTextContent(aNode, "max_version"); |
|
1366 |
|
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 } |
|
1375 |
|
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 } |
|
1382 |
|
1383 override.appID = appRange.appID; |
|
1384 override.appMinVersion = appRange.appMinVersion; |
|
1385 override.appMaxVersion = appRange.appMaxVersion; |
|
1386 |
|
1387 return override; |
|
1388 } |
|
1389 |
|
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; |
|
1395 |
|
1396 aResultObj[compat.id] = compat; |
|
1397 }, |
|
1398 |
|
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 }, |
|
1405 |
|
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 } |
|
1414 |
|
1415 this._searching = true; |
|
1416 this._callback = aCallback; |
|
1417 this._maxResults = aMaxResults; |
|
1418 |
|
1419 logger.debug("Requesting " + aURI); |
|
1420 |
|
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 } |
|
1428 |
|
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; |
|
1435 |
|
1436 if (!responseXML || responseXML.documentElement.namespaceURI == XMLURI_PARSE_ERROR || |
|
1437 (request.status != 200 && request.status != 0)) { |
|
1438 this._reportFailure(); |
|
1439 return; |
|
1440 } |
|
1441 |
|
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; |
|
1449 |
|
1450 let compatElements = documentElement.getElementsByTagName("addon_compatibility"); |
|
1451 let compatData = this._parseAddonCompatData(compatElements); |
|
1452 |
|
1453 aHandleResults(elements, totalResults, compatData); |
|
1454 }, false); |
|
1455 this._request.send(null); |
|
1456 }, |
|
1457 |
|
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}; |
|
1463 |
|
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 }); |
|
1469 |
|
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 }); |
|
1476 |
|
1477 if (localAddonIds.ids) |
|
1478 aCallback(localAddonIds); |
|
1479 }); |
|
1480 }, |
|
1481 |
|
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 } |
|
1491 |
|
1492 url = url.replace(/%([A-Z_]+)%/g, function urlSubstitution(aMatch, aKey) { |
|
1493 return (aKey in aSubstitutions) ? aSubstitutions[aKey] : aMatch; |
|
1494 }); |
|
1495 |
|
1496 return Services.urlFormatter.formatURL(url); |
|
1497 }, |
|
1498 |
|
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) { |
|
1506 |
|
1507 let appVersion = null; |
|
1508 if (override.appID == TOOLKIT_ID) |
|
1509 appVersion = aPlatformVersion || Services.appinfo.platformVersion; |
|
1510 else |
|
1511 appVersion = aAppVersion || Services.appinfo.version; |
|
1512 |
|
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 }, |
|
1522 |
|
1523 flush: function() { |
|
1524 return AddonDatabase.flush(); |
|
1525 } |
|
1526 }; |
|
1527 |
|
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, |
|
1533 |
|
1534 // the in-memory database |
|
1535 DB: BLANK_DB(), |
|
1536 |
|
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 }, |
|
1544 |
|
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; |
|
1552 |
|
1553 let inputDB, fstream, cstream, schema; |
|
1554 |
|
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); |
|
1561 |
|
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 } |
|
1571 |
|
1572 inputDB = JSON.parse(data); |
|
1573 |
|
1574 if (!inputDB.hasOwnProperty("addons") || |
|
1575 !Array.isArray(inputDB.addons)) { |
|
1576 throw new Error("No addons array."); |
|
1577 } |
|
1578 |
|
1579 if (!inputDB.hasOwnProperty("schema")) { |
|
1580 throw new Error("No schema specified."); |
|
1581 } |
|
1582 |
|
1583 schema = parseInt(inputDB.schema, 10); |
|
1584 |
|
1585 if (!Number.isInteger(schema) || |
|
1586 schema < DB_MIN_JSON_SCHEMA) { |
|
1587 throw new Error("Invalid schema value."); |
|
1588 } |
|
1589 |
|
1590 } catch (e if e.result == Cr.NS_ERROR_FILE_NOT_FOUND) { |
|
1591 logger.debug("No " + FILE_DATABASE + " found."); |
|
1592 |
|
1593 // Create a blank addons.json file |
|
1594 this._saveDBToDisk(); |
|
1595 |
|
1596 let dbSchema = 0; |
|
1597 try { |
|
1598 dbSchema = Services.prefs.getIntPref(PREF_GETADDONS_DB_SCHEMA); |
|
1599 } catch (e) {} |
|
1600 |
|
1601 if (dbSchema < DB_MIN_JSON_SCHEMA) { |
|
1602 this._migrationInProgress = AddonRepository_SQLiteMigrator.migrate((results) => { |
|
1603 if (results.length) |
|
1604 this.insertAddons(results); |
|
1605 |
|
1606 if (this._postMigrationCallback) { |
|
1607 this._postMigrationCallback(); |
|
1608 this._postMigrationCallback = null; |
|
1609 } |
|
1610 |
|
1611 this._migrationInProgress = false; |
|
1612 }); |
|
1613 |
|
1614 Services.prefs.setIntPref(PREF_GETADDONS_DB_SCHEMA, DB_SCHEMA); |
|
1615 } |
|
1616 |
|
1617 return; |
|
1618 |
|
1619 } catch (e) { |
|
1620 logger.error("Malformed " + FILE_DATABASE + ": " + e); |
|
1621 this.databaseOk = false; |
|
1622 return; |
|
1623 |
|
1624 } finally { |
|
1625 cstream.close(); |
|
1626 fstream.close(); |
|
1627 } |
|
1628 |
|
1629 Services.prefs.setIntPref(PREF_GETADDONS_DB_SCHEMA, DB_SCHEMA); |
|
1630 |
|
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 }, |
|
1638 |
|
1639 /** |
|
1640 * A lazy getter for the database connection. |
|
1641 */ |
|
1642 get connection() { |
|
1643 return this.openConnection(); |
|
1644 }, |
|
1645 |
|
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; |
|
1658 |
|
1659 if (!this.initialized) { |
|
1660 return Promise.resolve(0); |
|
1661 } |
|
1662 |
|
1663 this.initialized = false; |
|
1664 |
|
1665 this.__defineGetter__("connection", function shutdown_connectionGetter() { |
|
1666 return this.openConnection(); |
|
1667 }); |
|
1668 |
|
1669 if (aSkipFlush) { |
|
1670 return Promise.resolve(0); |
|
1671 } else { |
|
1672 return this.Writer.flush(); |
|
1673 } |
|
1674 }, |
|
1675 |
|
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(); |
|
1685 |
|
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 }, |
|
1696 |
|
1697 toJSON: function AD_toJSON() { |
|
1698 let json = { |
|
1699 schema: this.DB.schema, |
|
1700 addons: [] |
|
1701 } |
|
1702 |
|
1703 for (let [, value] of this.DB.addons) |
|
1704 json.addons.push(value); |
|
1705 |
|
1706 return json; |
|
1707 }, |
|
1708 |
|
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 }, |
|
1723 |
|
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 }, |
|
1736 |
|
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(); |
|
1747 |
|
1748 let gatherResults = () => { |
|
1749 let result = {}; |
|
1750 for (let [key, value] of this.DB.addons) |
|
1751 result[key] = value; |
|
1752 |
|
1753 executeSoon(function() aCallback(result)); |
|
1754 }; |
|
1755 |
|
1756 if (this._migrationInProgress) |
|
1757 this._postMigrationCallback = gatherResults; |
|
1758 else |
|
1759 gatherResults(); |
|
1760 }, |
|
1761 |
|
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 }, |
|
1775 |
|
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(); |
|
1787 |
|
1788 for (let addon of aAddons) { |
|
1789 this._insertAddon(addon); |
|
1790 } |
|
1791 |
|
1792 this._saveDBToDisk(); |
|
1793 |
|
1794 if (aCallback) |
|
1795 executeSoon(aCallback); |
|
1796 }, |
|
1797 |
|
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; |
|
1814 |
|
1815 this.DB.addons.set(newAddon.id, newAddon); |
|
1816 }, |
|
1817 |
|
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; |
|
1829 |
|
1830 let id = aObj.id; |
|
1831 if (!aObj.id) |
|
1832 return null; |
|
1833 |
|
1834 let addon = new AddonSearchResult(id); |
|
1835 |
|
1836 for (let [expectedProperty,] of Iterator(AddonSearchResult.prototype)) { |
|
1837 if (!(expectedProperty in aObj) || |
|
1838 typeof(aObj[expectedProperty]) === "function") |
|
1839 continue; |
|
1840 |
|
1841 let value = aObj[expectedProperty]; |
|
1842 |
|
1843 try { |
|
1844 switch (expectedProperty) { |
|
1845 case "sourceURI": |
|
1846 addon.sourceURI = value ? NetUtil.newURI(value) : null; |
|
1847 break; |
|
1848 |
|
1849 case "creator": |
|
1850 addon.creator = value |
|
1851 ? this._makeDeveloper(value) |
|
1852 : null; |
|
1853 break; |
|
1854 |
|
1855 case "updateDate": |
|
1856 addon.updateDate = value ? new Date(value) : null; |
|
1857 break; |
|
1858 |
|
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; |
|
1865 |
|
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; |
|
1872 |
|
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; |
|
1881 |
|
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; |
|
1888 |
|
1889 case "iconURL": |
|
1890 break; |
|
1891 |
|
1892 default: |
|
1893 addon[expectedProperty] = value; |
|
1894 } |
|
1895 } catch (ex) { |
|
1896 logger.warn("Error in parsing property value for " + expectedProperty + " | " + ex); |
|
1897 } |
|
1898 |
|
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 } |
|
1905 |
|
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 } |
|
1921 |
|
1922 if (!remainingProperty.startsWith("_")) |
|
1923 addon._unsupportedProperties[remainingProperty] = |
|
1924 aObj[remainingProperty]; |
|
1925 } |
|
1926 |
|
1927 return addon; |
|
1928 }, |
|
1929 |
|
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 }, |
|
1942 |
|
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 }, |
|
1956 |
|
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 }, |
|
1976 |
|
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 }; |
|
2000 |
|
2001 function executeSoon(aCallback) { |
|
2002 Services.tm.mainThread.dispatch(aCallback, Ci.nsIThread.DISPATCH_NORMAL); |
|
2003 } |