1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/mozapps/extensions/internal/AddonRepository.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,2003 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +const Cc = Components.classes; 1.11 +const Ci = Components.interfaces; 1.12 +const Cu = Components.utils; 1.13 +const Cr = Components.results; 1.14 + 1.15 +Components.utils.import("resource://gre/modules/Services.jsm"); 1.16 +Components.utils.import("resource://gre/modules/AddonManager.jsm"); 1.17 +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); 1.18 + 1.19 +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", 1.20 + "resource://gre/modules/FileUtils.jsm"); 1.21 +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", 1.22 + "resource://gre/modules/NetUtil.jsm"); 1.23 +XPCOMUtils.defineLazyModuleGetter(this, "OS", 1.24 + "resource://gre/modules/osfile.jsm"); 1.25 +XPCOMUtils.defineLazyModuleGetter(this, "DeferredSave", 1.26 + "resource://gre/modules/DeferredSave.jsm"); 1.27 +XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository_SQLiteMigrator", 1.28 + "resource://gre/modules/addons/AddonRepository_SQLiteMigrator.jsm"); 1.29 +XPCOMUtils.defineLazyModuleGetter(this, "Promise", 1.30 + "resource://gre/modules/Promise.jsm"); 1.31 + 1.32 +this.EXPORTED_SYMBOLS = [ "AddonRepository" ]; 1.33 + 1.34 +const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled"; 1.35 +const PREF_GETADDONS_CACHE_TYPES = "extensions.getAddons.cache.types"; 1.36 +const PREF_GETADDONS_CACHE_ID_ENABLED = "extensions.%ID%.getAddons.cache.enabled" 1.37 +const PREF_GETADDONS_BROWSEADDONS = "extensions.getAddons.browseAddons"; 1.38 +const PREF_GETADDONS_BYIDS = "extensions.getAddons.get.url"; 1.39 +const PREF_GETADDONS_BYIDS_PERFORMANCE = "extensions.getAddons.getWithPerformance.url"; 1.40 +const PREF_GETADDONS_BROWSERECOMMENDED = "extensions.getAddons.recommended.browseURL"; 1.41 +const PREF_GETADDONS_GETRECOMMENDED = "extensions.getAddons.recommended.url"; 1.42 +const PREF_GETADDONS_BROWSESEARCHRESULTS = "extensions.getAddons.search.browseURL"; 1.43 +const PREF_GETADDONS_GETSEARCHRESULTS = "extensions.getAddons.search.url"; 1.44 +const PREF_GETADDONS_DB_SCHEMA = "extensions.getAddons.databaseSchema" 1.45 + 1.46 +const XMLURI_PARSE_ERROR = "http://www.mozilla.org/newlayout/xml/parsererror.xml"; 1.47 + 1.48 +const API_VERSION = "1.5"; 1.49 +const DEFAULT_CACHE_TYPES = "extension,theme,locale,dictionary"; 1.50 + 1.51 +const KEY_PROFILEDIR = "ProfD"; 1.52 +const FILE_DATABASE = "addons.json"; 1.53 +const DB_SCHEMA = 5; 1.54 +const DB_MIN_JSON_SCHEMA = 5; 1.55 +const DB_BATCH_TIMEOUT_MS = 50; 1.56 + 1.57 +const BLANK_DB = function() { 1.58 + return { 1.59 + addons: new Map(), 1.60 + schema: DB_SCHEMA 1.61 + }; 1.62 +} 1.63 + 1.64 +const TOOLKIT_ID = "toolkit@mozilla.org"; 1.65 + 1.66 +Cu.import("resource://gre/modules/Log.jsm"); 1.67 +const LOGGER_ID = "addons.repository"; 1.68 + 1.69 +// Create a new logger for use by the Addons Repository 1.70 +// (Requires AddonManager.jsm) 1.71 +let logger = Log.repository.getLogger(LOGGER_ID); 1.72 + 1.73 +// A map between XML keys to AddonSearchResult keys for string values 1.74 +// that require no extra parsing from XML 1.75 +const STRING_KEY_MAP = { 1.76 + name: "name", 1.77 + version: "version", 1.78 + homepage: "homepageURL", 1.79 + support: "supportURL" 1.80 +}; 1.81 + 1.82 +// A map between XML keys to AddonSearchResult keys for string values 1.83 +// that require parsing from HTML 1.84 +const HTML_KEY_MAP = { 1.85 + summary: "description", 1.86 + description: "fullDescription", 1.87 + developer_comments: "developerComments", 1.88 + eula: "eula" 1.89 +}; 1.90 + 1.91 +// A map between XML keys to AddonSearchResult keys for integer values 1.92 +// that require no extra parsing from XML 1.93 +const INTEGER_KEY_MAP = { 1.94 + total_downloads: "totalDownloads", 1.95 + weekly_downloads: "weeklyDownloads", 1.96 + daily_users: "dailyUsers" 1.97 +}; 1.98 + 1.99 +// Wrap the XHR factory so that tests can override with a mock 1.100 +let XHRequest = Components.Constructor("@mozilla.org/xmlextras/xmlhttprequest;1", 1.101 + "nsIXMLHttpRequest"); 1.102 + 1.103 +function convertHTMLToPlainText(html) { 1.104 + if (!html) 1.105 + return html; 1.106 + var converter = Cc["@mozilla.org/widget/htmlformatconverter;1"]. 1.107 + createInstance(Ci.nsIFormatConverter); 1.108 + 1.109 + var input = Cc["@mozilla.org/supports-string;1"]. 1.110 + createInstance(Ci.nsISupportsString); 1.111 + input.data = html.replace(/\n/g, "<br>"); 1.112 + 1.113 + var output = {}; 1.114 + converter.convert("text/html", input, input.data.length, "text/unicode", 1.115 + output, {}); 1.116 + 1.117 + if (output.value instanceof Ci.nsISupportsString) 1.118 + return output.value.data.replace(/\r\n/g, "\n"); 1.119 + return html; 1.120 +} 1.121 + 1.122 +function getAddonsToCache(aIds, aCallback) { 1.123 + try { 1.124 + var types = Services.prefs.getCharPref(PREF_GETADDONS_CACHE_TYPES); 1.125 + } 1.126 + catch (e) { } 1.127 + if (!types) 1.128 + types = DEFAULT_CACHE_TYPES; 1.129 + 1.130 + types = types.split(","); 1.131 + 1.132 + AddonManager.getAddonsByIDs(aIds, function getAddonsToCache_getAddonsByIDs(aAddons) { 1.133 + let enabledIds = []; 1.134 + for (var i = 0; i < aIds.length; i++) { 1.135 + var preference = PREF_GETADDONS_CACHE_ID_ENABLED.replace("%ID%", aIds[i]); 1.136 + try { 1.137 + if (!Services.prefs.getBoolPref(preference)) 1.138 + continue; 1.139 + } catch(e) { 1.140 + // If the preference doesn't exist caching is enabled by default 1.141 + } 1.142 + 1.143 + // The add-ons manager may not know about this ID yet if it is a pending 1.144 + // install. In that case we'll just cache it regardless 1.145 + if (aAddons[i] && (types.indexOf(aAddons[i].type) == -1)) 1.146 + continue; 1.147 + 1.148 + enabledIds.push(aIds[i]); 1.149 + } 1.150 + 1.151 + aCallback(enabledIds); 1.152 + }); 1.153 +} 1.154 + 1.155 +function AddonSearchResult(aId) { 1.156 + this.id = aId; 1.157 + this.icons = {}; 1.158 + this._unsupportedProperties = {}; 1.159 +} 1.160 + 1.161 +AddonSearchResult.prototype = { 1.162 + /** 1.163 + * The ID of the add-on 1.164 + */ 1.165 + id: null, 1.166 + 1.167 + /** 1.168 + * The add-on type (e.g. "extension" or "theme") 1.169 + */ 1.170 + type: null, 1.171 + 1.172 + /** 1.173 + * The name of the add-on 1.174 + */ 1.175 + name: null, 1.176 + 1.177 + /** 1.178 + * The version of the add-on 1.179 + */ 1.180 + version: null, 1.181 + 1.182 + /** 1.183 + * The creator of the add-on 1.184 + */ 1.185 + creator: null, 1.186 + 1.187 + /** 1.188 + * The developers of the add-on 1.189 + */ 1.190 + developers: null, 1.191 + 1.192 + /** 1.193 + * A short description of the add-on 1.194 + */ 1.195 + description: null, 1.196 + 1.197 + /** 1.198 + * The full description of the add-on 1.199 + */ 1.200 + fullDescription: null, 1.201 + 1.202 + /** 1.203 + * The developer comments for the add-on. This includes any information 1.204 + * that may be helpful to end users that isn't necessarily applicable to 1.205 + * the add-on description (e.g. known major bugs) 1.206 + */ 1.207 + developerComments: null, 1.208 + 1.209 + /** 1.210 + * The end-user licensing agreement (EULA) of the add-on 1.211 + */ 1.212 + eula: null, 1.213 + 1.214 + /** 1.215 + * The url of the add-on's icon 1.216 + */ 1.217 + get iconURL() { 1.218 + return this.icons && this.icons[32]; 1.219 + }, 1.220 + 1.221 + /** 1.222 + * The URLs of the add-on's icons, as an object with icon size as key 1.223 + */ 1.224 + icons: null, 1.225 + 1.226 + /** 1.227 + * An array of screenshot urls for the add-on 1.228 + */ 1.229 + screenshots: null, 1.230 + 1.231 + /** 1.232 + * The homepage for the add-on 1.233 + */ 1.234 + homepageURL: null, 1.235 + 1.236 + /** 1.237 + * The homepage for the add-on 1.238 + */ 1.239 + learnmoreURL: null, 1.240 + 1.241 + /** 1.242 + * The support URL for the add-on 1.243 + */ 1.244 + supportURL: null, 1.245 + 1.246 + /** 1.247 + * The contribution url of the add-on 1.248 + */ 1.249 + contributionURL: null, 1.250 + 1.251 + /** 1.252 + * The suggested contribution amount 1.253 + */ 1.254 + contributionAmount: null, 1.255 + 1.256 + /** 1.257 + * The URL to visit in order to purchase the add-on 1.258 + */ 1.259 + purchaseURL: null, 1.260 + 1.261 + /** 1.262 + * The numerical cost of the add-on in some currency, for sorting purposes 1.263 + * only 1.264 + */ 1.265 + purchaseAmount: null, 1.266 + 1.267 + /** 1.268 + * The display cost of the add-on, for display purposes only 1.269 + */ 1.270 + purchaseDisplayAmount: null, 1.271 + 1.272 + /** 1.273 + * The rating of the add-on, 0-5 1.274 + */ 1.275 + averageRating: null, 1.276 + 1.277 + /** 1.278 + * The number of reviews for this add-on 1.279 + */ 1.280 + reviewCount: null, 1.281 + 1.282 + /** 1.283 + * The URL to the list of reviews for this add-on 1.284 + */ 1.285 + reviewURL: null, 1.286 + 1.287 + /** 1.288 + * The total number of times the add-on was downloaded 1.289 + */ 1.290 + totalDownloads: null, 1.291 + 1.292 + /** 1.293 + * The number of times the add-on was downloaded the current week 1.294 + */ 1.295 + weeklyDownloads: null, 1.296 + 1.297 + /** 1.298 + * The number of daily users for the add-on 1.299 + */ 1.300 + dailyUsers: null, 1.301 + 1.302 + /** 1.303 + * AddonInstall object generated from the add-on XPI url 1.304 + */ 1.305 + install: null, 1.306 + 1.307 + /** 1.308 + * nsIURI storing where this add-on was installed from 1.309 + */ 1.310 + sourceURI: null, 1.311 + 1.312 + /** 1.313 + * The status of the add-on in the repository (e.g. 4 = "Public") 1.314 + */ 1.315 + repositoryStatus: null, 1.316 + 1.317 + /** 1.318 + * The size of the add-on's files in bytes. For an add-on that have not yet 1.319 + * been downloaded this may be an estimated value. 1.320 + */ 1.321 + size: null, 1.322 + 1.323 + /** 1.324 + * The Date that the add-on was most recently updated 1.325 + */ 1.326 + updateDate: null, 1.327 + 1.328 + /** 1.329 + * True or false depending on whether the add-on is compatible with the 1.330 + * current version of the application 1.331 + */ 1.332 + isCompatible: true, 1.333 + 1.334 + /** 1.335 + * True or false depending on whether the add-on is compatible with the 1.336 + * current platform 1.337 + */ 1.338 + isPlatformCompatible: true, 1.339 + 1.340 + /** 1.341 + * Array of AddonCompatibilityOverride objects, that describe overrides for 1.342 + * compatibility with an application versions. 1.343 + **/ 1.344 + compatibilityOverrides: null, 1.345 + 1.346 + /** 1.347 + * True if the add-on has a secure means of updating 1.348 + */ 1.349 + providesUpdatesSecurely: true, 1.350 + 1.351 + /** 1.352 + * The current blocklist state of the add-on 1.353 + */ 1.354 + blocklistState: Ci.nsIBlocklistService.STATE_NOT_BLOCKED, 1.355 + 1.356 + /** 1.357 + * True if this add-on cannot be used in the application based on version 1.358 + * compatibility, dependencies and blocklisting 1.359 + */ 1.360 + appDisabled: false, 1.361 + 1.362 + /** 1.363 + * True if the user wants this add-on to be disabled 1.364 + */ 1.365 + userDisabled: false, 1.366 + 1.367 + /** 1.368 + * Indicates what scope the add-on is installed in, per profile, user, 1.369 + * system or application 1.370 + */ 1.371 + scope: AddonManager.SCOPE_PROFILE, 1.372 + 1.373 + /** 1.374 + * True if the add-on is currently functional 1.375 + */ 1.376 + isActive: true, 1.377 + 1.378 + /** 1.379 + * A bitfield holding all of the current operations that are waiting to be 1.380 + * performed for this add-on 1.381 + */ 1.382 + pendingOperations: AddonManager.PENDING_NONE, 1.383 + 1.384 + /** 1.385 + * A bitfield holding all the the operations that can be performed on 1.386 + * this add-on 1.387 + */ 1.388 + permissions: 0, 1.389 + 1.390 + /** 1.391 + * Tests whether this add-on is known to be compatible with a 1.392 + * particular application and platform version. 1.393 + * 1.394 + * @param appVersion 1.395 + * An application version to test against 1.396 + * @param platformVersion 1.397 + * A platform version to test against 1.398 + * @return Boolean representing if the add-on is compatible 1.399 + */ 1.400 + isCompatibleWith: function ASR_isCompatibleWith(aAppVerison, aPlatformVersion) { 1.401 + return true; 1.402 + }, 1.403 + 1.404 + /** 1.405 + * Starts an update check for this add-on. This will perform 1.406 + * asynchronously and deliver results to the given listener. 1.407 + * 1.408 + * @param aListener 1.409 + * An UpdateListener for the update process 1.410 + * @param aReason 1.411 + * A reason code for performing the update 1.412 + * @param aAppVersion 1.413 + * An application version to check for updates for 1.414 + * @param aPlatformVersion 1.415 + * A platform version to check for updates for 1.416 + */ 1.417 + findUpdates: function ASR_findUpdates(aListener, aReason, aAppVersion, aPlatformVersion) { 1.418 + if ("onNoCompatibilityUpdateAvailable" in aListener) 1.419 + aListener.onNoCompatibilityUpdateAvailable(this); 1.420 + if ("onNoUpdateAvailable" in aListener) 1.421 + aListener.onNoUpdateAvailable(this); 1.422 + if ("onUpdateFinished" in aListener) 1.423 + aListener.onUpdateFinished(this); 1.424 + }, 1.425 + 1.426 + toJSON: function() { 1.427 + let json = {}; 1.428 + 1.429 + for (let [property, value] of Iterator(this)) { 1.430 + if (property.startsWith("_") || 1.431 + typeof(value) === "function") 1.432 + continue; 1.433 + 1.434 + try { 1.435 + switch (property) { 1.436 + case "sourceURI": 1.437 + json.sourceURI = value ? value.spec : ""; 1.438 + break; 1.439 + 1.440 + case "updateDate": 1.441 + json.updateDate = value ? value.getTime() : ""; 1.442 + break; 1.443 + 1.444 + default: 1.445 + json[property] = value; 1.446 + } 1.447 + } catch (ex) { 1.448 + logger.warn("Error writing property value for " + property); 1.449 + } 1.450 + } 1.451 + 1.452 + for (let [property, value] of Iterator(this._unsupportedProperties)) { 1.453 + if (!property.startsWith("_")) 1.454 + json[property] = value; 1.455 + } 1.456 + 1.457 + return json; 1.458 + } 1.459 +} 1.460 + 1.461 +/** 1.462 + * The add-on repository is a source of add-ons that can be installed. It can 1.463 + * be searched in three ways. The first takes a list of IDs and returns a 1.464 + * list of the corresponding add-ons. The second returns a list of add-ons that 1.465 + * come highly recommended. This list should change frequently. The third is to 1.466 + * search for specific search terms entered by the user. Searches are 1.467 + * asynchronous and results should be passed to the provided callback object 1.468 + * when complete. The results passed to the callback should only include add-ons 1.469 + * that are compatible with the current application and are not already 1.470 + * installed. 1.471 + */ 1.472 +this.AddonRepository = { 1.473 + /** 1.474 + * Whether caching is currently enabled 1.475 + */ 1.476 + get cacheEnabled() { 1.477 + // Act as though caching is disabled if there was an unrecoverable error 1.478 + // openning the database. 1.479 + if (!AddonDatabase.databaseOk) { 1.480 + logger.warn("Cache is disabled because database is not OK"); 1.481 + return false; 1.482 + } 1.483 + 1.484 + let preference = PREF_GETADDONS_CACHE_ENABLED; 1.485 + let enabled = false; 1.486 + try { 1.487 + enabled = Services.prefs.getBoolPref(preference); 1.488 + } catch(e) { 1.489 + logger.warn("cacheEnabled: Couldn't get pref: " + preference); 1.490 + } 1.491 + 1.492 + return enabled; 1.493 + }, 1.494 + 1.495 + // A cache of the add-ons stored in the database 1.496 + _addons: null, 1.497 + 1.498 + // An array of callbacks pending the retrieval of add-ons from AddonDatabase 1.499 + _pendingCallbacks: null, 1.500 + 1.501 + // Whether a migration in currently in progress 1.502 + _migrationInProgress: false, 1.503 + 1.504 + // A callback to be called when migration finishes 1.505 + _postMigrationCallback: null, 1.506 + 1.507 + // Whether a search is currently in progress 1.508 + _searching: false, 1.509 + 1.510 + // XHR associated with the current request 1.511 + _request: null, 1.512 + 1.513 + /* 1.514 + * Addon search results callback object that contains two functions 1.515 + * 1.516 + * searchSucceeded - Called when a search has suceeded. 1.517 + * 1.518 + * @param aAddons 1.519 + * An array of the add-on results. In the case of searching for 1.520 + * specific terms the ordering of results may be determined by 1.521 + * the search provider. 1.522 + * @param aAddonCount 1.523 + * The length of aAddons 1.524 + * @param aTotalResults 1.525 + * The total results actually available in the repository 1.526 + * 1.527 + * 1.528 + * searchFailed - Called when an error occurred when performing a search. 1.529 + */ 1.530 + _callback: null, 1.531 + 1.532 + // Maximum number of results to return 1.533 + _maxResults: null, 1.534 + 1.535 + /** 1.536 + * Shut down AddonRepository 1.537 + * return: promise{integer} resolves with the result of flushing 1.538 + * the AddonRepository database 1.539 + */ 1.540 + shutdown: function AddonRepo_shutdown() { 1.541 + this.cancelSearch(); 1.542 + 1.543 + this._addons = null; 1.544 + this._pendingCallbacks = null; 1.545 + return AddonDatabase.shutdown(false); 1.546 + }, 1.547 + 1.548 + /** 1.549 + * Asynchronously get a cached add-on by id. The add-on (or null if the 1.550 + * add-on is not found) is passed to the specified callback. If caching is 1.551 + * disabled, null is passed to the specified callback. 1.552 + * 1.553 + * @param aId 1.554 + * The id of the add-on to get 1.555 + * @param aCallback 1.556 + * The callback to pass the result back to 1.557 + */ 1.558 + getCachedAddonByID: function AddonRepo_getCachedAddonByID(aId, aCallback) { 1.559 + if (!aId || !this.cacheEnabled) { 1.560 + aCallback(null); 1.561 + return; 1.562 + } 1.563 + 1.564 + let self = this; 1.565 + function getAddon(aAddons) { 1.566 + aCallback((aId in aAddons) ? aAddons[aId] : null); 1.567 + } 1.568 + 1.569 + if (this._addons == null) { 1.570 + if (this._pendingCallbacks == null) { 1.571 + // Data has not been retrieved from the database, so retrieve it 1.572 + this._pendingCallbacks = []; 1.573 + this._pendingCallbacks.push(getAddon); 1.574 + AddonDatabase.retrieveStoredData(function getCachedAddonByID_retrieveData(aAddons) { 1.575 + let pendingCallbacks = self._pendingCallbacks; 1.576 + 1.577 + // Check if cache was shutdown or deleted before callback was called 1.578 + if (pendingCallbacks == null) 1.579 + return; 1.580 + 1.581 + // Callbacks may want to trigger a other caching operations that may 1.582 + // affect _addons and _pendingCallbacks, so set to final values early 1.583 + self._pendingCallbacks = null; 1.584 + self._addons = aAddons; 1.585 + 1.586 + pendingCallbacks.forEach(function(aCallback) aCallback(aAddons)); 1.587 + }); 1.588 + 1.589 + return; 1.590 + } 1.591 + 1.592 + // Data is being retrieved from the database, so wait 1.593 + this._pendingCallbacks.push(getAddon); 1.594 + return; 1.595 + } 1.596 + 1.597 + // Data has been retrieved, so immediately return result 1.598 + getAddon(this._addons); 1.599 + }, 1.600 + 1.601 + /** 1.602 + * Asynchronously repopulate cache so it only contains the add-ons 1.603 + * corresponding to the specified ids. If caching is disabled, 1.604 + * the cache is completely removed. 1.605 + * 1.606 + * @param aIds 1.607 + * The array of add-on ids to repopulate the cache with 1.608 + * @param aCallback 1.609 + * The optional callback to call once complete 1.610 + * @param aTimeout 1.611 + * (Optional) timeout in milliseconds to abandon the XHR request 1.612 + * if we have not received a response from the server. 1.613 + */ 1.614 + repopulateCache: function(aIds, aCallback, aTimeout) { 1.615 + this._repopulateCacheInternal(aIds, aCallback, false, aTimeout); 1.616 + }, 1.617 + 1.618 + _repopulateCacheInternal: function (aIds, aCallback, aSendPerformance, aTimeout) { 1.619 + // Always call AddonManager updateAddonRepositoryData after we refill the cache 1.620 + function repopulateAddonManager() { 1.621 + AddonManagerPrivate.updateAddonRepositoryData(aCallback); 1.622 + } 1.623 + 1.624 + logger.debug("Repopulate add-on cache with " + aIds.toSource()); 1.625 + // Completely remove cache if caching is not enabled 1.626 + if (!this.cacheEnabled) { 1.627 + logger.debug("Clearing cache because it is disabled"); 1.628 + this._addons = null; 1.629 + this._pendingCallbacks = null; 1.630 + AddonDatabase.delete(repopulateAddonManager); 1.631 + return; 1.632 + } 1.633 + 1.634 + let self = this; 1.635 + getAddonsToCache(aIds, function repopulateCache_getAddonsToCache(aAddons) { 1.636 + // Completely remove cache if there are no add-ons to cache 1.637 + if (aAddons.length == 0) { 1.638 + logger.debug("Clearing cache because 0 add-ons were requested"); 1.639 + self._addons = null; 1.640 + self._pendingCallbacks = null; 1.641 + AddonDatabase.delete(repopulateAddonManager); 1.642 + return; 1.643 + } 1.644 + 1.645 + self._beginGetAddons(aAddons, { 1.646 + searchSucceeded: function repopulateCacheInternal_searchSucceeded(aAddons) { 1.647 + self._addons = {}; 1.648 + aAddons.forEach(function(aAddon) { self._addons[aAddon.id] = aAddon; }); 1.649 + AddonDatabase.repopulate(aAddons, repopulateAddonManager); 1.650 + }, 1.651 + searchFailed: function repopulateCacheInternal_searchFailed() { 1.652 + logger.warn("Search failed when repopulating cache"); 1.653 + repopulateAddonManager(); 1.654 + } 1.655 + }, aSendPerformance, aTimeout); 1.656 + }); 1.657 + }, 1.658 + 1.659 + /** 1.660 + * Asynchronously add add-ons to the cache corresponding to the specified 1.661 + * ids. If caching is disabled, the cache is unchanged and the callback is 1.662 + * immediately called if it is defined. 1.663 + * 1.664 + * @param aIds 1.665 + * The array of add-on ids to add to the cache 1.666 + * @param aCallback 1.667 + * The optional callback to call once complete 1.668 + */ 1.669 + cacheAddons: function AddonRepo_cacheAddons(aIds, aCallback) { 1.670 + logger.debug("cacheAddons: enabled " + this.cacheEnabled + " IDs " + aIds.toSource()); 1.671 + if (!this.cacheEnabled) { 1.672 + if (aCallback) 1.673 + aCallback(); 1.674 + return; 1.675 + } 1.676 + 1.677 + let self = this; 1.678 + getAddonsToCache(aIds, function cacheAddons_getAddonsToCache(aAddons) { 1.679 + // If there are no add-ons to cache, act as if caching is disabled 1.680 + if (aAddons.length == 0) { 1.681 + if (aCallback) 1.682 + aCallback(); 1.683 + return; 1.684 + } 1.685 + 1.686 + self.getAddonsByIDs(aAddons, { 1.687 + searchSucceeded: function cacheAddons_searchSucceeded(aAddons) { 1.688 + aAddons.forEach(function(aAddon) { self._addons[aAddon.id] = aAddon; }); 1.689 + AddonDatabase.insertAddons(aAddons, aCallback); 1.690 + }, 1.691 + searchFailed: function cacheAddons_searchFailed() { 1.692 + logger.warn("Search failed when adding add-ons to cache"); 1.693 + if (aCallback) 1.694 + aCallback(); 1.695 + } 1.696 + }); 1.697 + }); 1.698 + }, 1.699 + 1.700 + /** 1.701 + * The homepage for visiting this repository. If the corresponding preference 1.702 + * is not defined, defaults to about:blank. 1.703 + */ 1.704 + get homepageURL() { 1.705 + let url = this._formatURLPref(PREF_GETADDONS_BROWSEADDONS, {}); 1.706 + return (url != null) ? url : "about:blank"; 1.707 + }, 1.708 + 1.709 + /** 1.710 + * Returns whether this instance is currently performing a search. New 1.711 + * searches will not be performed while this is the case. 1.712 + */ 1.713 + get isSearching() { 1.714 + return this._searching; 1.715 + }, 1.716 + 1.717 + /** 1.718 + * The url that can be visited to see recommended add-ons in this repository. 1.719 + * If the corresponding preference is not defined, defaults to about:blank. 1.720 + */ 1.721 + getRecommendedURL: function AddonRepo_getRecommendedURL() { 1.722 + let url = this._formatURLPref(PREF_GETADDONS_BROWSERECOMMENDED, {}); 1.723 + return (url != null) ? url : "about:blank"; 1.724 + }, 1.725 + 1.726 + /** 1.727 + * Retrieves the url that can be visited to see search results for the given 1.728 + * terms. If the corresponding preference is not defined, defaults to 1.729 + * about:blank. 1.730 + * 1.731 + * @param aSearchTerms 1.732 + * Search terms used to search the repository 1.733 + */ 1.734 + getSearchURL: function AddonRepo_getSearchURL(aSearchTerms) { 1.735 + let url = this._formatURLPref(PREF_GETADDONS_BROWSESEARCHRESULTS, { 1.736 + TERMS : encodeURIComponent(aSearchTerms) 1.737 + }); 1.738 + return (url != null) ? url : "about:blank"; 1.739 + }, 1.740 + 1.741 + /** 1.742 + * Cancels the search in progress. If there is no search in progress this 1.743 + * does nothing. 1.744 + */ 1.745 + cancelSearch: function AddonRepo_cancelSearch() { 1.746 + this._searching = false; 1.747 + if (this._request) { 1.748 + this._request.abort(); 1.749 + this._request = null; 1.750 + } 1.751 + this._callback = null; 1.752 + }, 1.753 + 1.754 + /** 1.755 + * Begins a search for add-ons in this repository by ID. Results will be 1.756 + * passed to the given callback. 1.757 + * 1.758 + * @param aIDs 1.759 + * The array of ids to search for 1.760 + * @param aCallback 1.761 + * The callback to pass results to 1.762 + */ 1.763 + getAddonsByIDs: function AddonRepo_getAddonsByIDs(aIDs, aCallback) { 1.764 + return this._beginGetAddons(aIDs, aCallback, false); 1.765 + }, 1.766 + 1.767 + /** 1.768 + * Begins a search of add-ons, potentially sending performance data. 1.769 + * 1.770 + * @param aIDs 1.771 + * Array of ids to search for. 1.772 + * @param aCallback 1.773 + * Function to pass results to. 1.774 + * @param aSendPerformance 1.775 + * Boolean indicating whether to send performance data with the 1.776 + * request. 1.777 + * @param aTimeout 1.778 + * (Optional) timeout in milliseconds to abandon the XHR request 1.779 + * if we have not received a response from the server. 1.780 + */ 1.781 + _beginGetAddons: function(aIDs, aCallback, aSendPerformance, aTimeout) { 1.782 + let ids = aIDs.slice(0); 1.783 + 1.784 + let params = { 1.785 + API_VERSION : API_VERSION, 1.786 + IDS : ids.map(encodeURIComponent).join(',') 1.787 + }; 1.788 + 1.789 + let pref = PREF_GETADDONS_BYIDS; 1.790 + 1.791 + if (aSendPerformance) { 1.792 + let type = Services.prefs.getPrefType(PREF_GETADDONS_BYIDS_PERFORMANCE); 1.793 + if (type == Services.prefs.PREF_STRING) { 1.794 + pref = PREF_GETADDONS_BYIDS_PERFORMANCE; 1.795 + 1.796 + let startupInfo = Cc["@mozilla.org/toolkit/app-startup;1"]. 1.797 + getService(Ci.nsIAppStartup). 1.798 + getStartupInfo(); 1.799 + 1.800 + params.TIME_MAIN = ""; 1.801 + params.TIME_FIRST_PAINT = ""; 1.802 + params.TIME_SESSION_RESTORED = ""; 1.803 + if (startupInfo.process) { 1.804 + if (startupInfo.main) { 1.805 + params.TIME_MAIN = startupInfo.main - startupInfo.process; 1.806 + } 1.807 + if (startupInfo.firstPaint) { 1.808 + params.TIME_FIRST_PAINT = startupInfo.firstPaint - 1.809 + startupInfo.process; 1.810 + } 1.811 + if (startupInfo.sessionRestored) { 1.812 + params.TIME_SESSION_RESTORED = startupInfo.sessionRestored - 1.813 + startupInfo.process; 1.814 + } 1.815 + } 1.816 + } 1.817 + } 1.818 + 1.819 + let url = this._formatURLPref(pref, params); 1.820 + 1.821 + let self = this; 1.822 + function handleResults(aElements, aTotalResults, aCompatData) { 1.823 + // Don't use this._parseAddons() so that, for example, 1.824 + // incompatible add-ons are not filtered out 1.825 + let results = []; 1.826 + for (let i = 0; i < aElements.length && results.length < self._maxResults; i++) { 1.827 + let result = self._parseAddon(aElements[i], null, aCompatData); 1.828 + if (result == null) 1.829 + continue; 1.830 + 1.831 + // Ignore add-on if it wasn't actually requested 1.832 + let idIndex = ids.indexOf(result.addon.id); 1.833 + if (idIndex == -1) 1.834 + continue; 1.835 + 1.836 + results.push(result); 1.837 + // Ignore this add-on from now on 1.838 + ids.splice(idIndex, 1); 1.839 + } 1.840 + 1.841 + // Include any compatibility overrides for addons not hosted by the 1.842 + // remote repository. 1.843 + for each (let addonCompat in aCompatData) { 1.844 + if (addonCompat.hosted) 1.845 + continue; 1.846 + 1.847 + let addon = new AddonSearchResult(addonCompat.id); 1.848 + // Compatibility overrides can only be for extensions. 1.849 + addon.type = "extension"; 1.850 + addon.compatibilityOverrides = addonCompat.compatRanges; 1.851 + let result = { 1.852 + addon: addon, 1.853 + xpiURL: null, 1.854 + xpiHash: null 1.855 + }; 1.856 + results.push(result); 1.857 + } 1.858 + 1.859 + // aTotalResults irrelevant 1.860 + self._reportSuccess(results, -1); 1.861 + } 1.862 + 1.863 + this._beginSearch(url, ids.length, aCallback, handleResults, aTimeout); 1.864 + }, 1.865 + 1.866 + /** 1.867 + * Performs the daily background update check. 1.868 + * 1.869 + * This API both searches for the add-on IDs specified and sends performance 1.870 + * data. It is meant to be called as part of the daily update ping. It should 1.871 + * not be used for any other purpose. Use repopulateCache instead. 1.872 + * 1.873 + * @param aIDs 1.874 + * Array of add-on IDs to repopulate the cache with. 1.875 + * @param aCallback 1.876 + * Function to call when data is received. Function must be an object 1.877 + * with the keys searchSucceeded and searchFailed. 1.878 + */ 1.879 + backgroundUpdateCheck: function AddonRepo_backgroundUpdateCheck(aIDs, aCallback) { 1.880 + this._repopulateCacheInternal(aIDs, aCallback, true); 1.881 + }, 1.882 + 1.883 + /** 1.884 + * Begins a search for recommended add-ons in this repository. Results will 1.885 + * be passed to the given callback. 1.886 + * 1.887 + * @param aMaxResults 1.888 + * The maximum number of results to return 1.889 + * @param aCallback 1.890 + * The callback to pass results to 1.891 + */ 1.892 + retrieveRecommendedAddons: function AddonRepo_retrieveRecommendedAddons(aMaxResults, aCallback) { 1.893 + let url = this._formatURLPref(PREF_GETADDONS_GETRECOMMENDED, { 1.894 + API_VERSION : API_VERSION, 1.895 + 1.896 + // Get twice as many results to account for potential filtering 1.897 + MAX_RESULTS : 2 * aMaxResults 1.898 + }); 1.899 + 1.900 + let self = this; 1.901 + function handleResults(aElements, aTotalResults) { 1.902 + self._getLocalAddonIds(function retrieveRecommendedAddons_getLocalAddonIds(aLocalAddonIds) { 1.903 + // aTotalResults irrelevant 1.904 + self._parseAddons(aElements, -1, aLocalAddonIds); 1.905 + }); 1.906 + } 1.907 + 1.908 + this._beginSearch(url, aMaxResults, aCallback, handleResults); 1.909 + }, 1.910 + 1.911 + /** 1.912 + * Begins a search for add-ons in this repository. Results will be passed to 1.913 + * the given callback. 1.914 + * 1.915 + * @param aSearchTerms 1.916 + * The terms to search for 1.917 + * @param aMaxResults 1.918 + * The maximum number of results to return 1.919 + * @param aCallback 1.920 + * The callback to pass results to 1.921 + */ 1.922 + searchAddons: function AddonRepo_searchAddons(aSearchTerms, aMaxResults, aCallback) { 1.923 + let compatMode = "normal"; 1.924 + if (!AddonManager.checkCompatibility) 1.925 + compatMode = "ignore"; 1.926 + else if (AddonManager.strictCompatibility) 1.927 + compatMode = "strict"; 1.928 + 1.929 + let substitutions = { 1.930 + API_VERSION : API_VERSION, 1.931 + TERMS : encodeURIComponent(aSearchTerms), 1.932 + // Get twice as many results to account for potential filtering 1.933 + MAX_RESULTS : 2 * aMaxResults, 1.934 + COMPATIBILITY_MODE : compatMode, 1.935 + }; 1.936 + 1.937 + let url = this._formatURLPref(PREF_GETADDONS_GETSEARCHRESULTS, substitutions); 1.938 + 1.939 + let self = this; 1.940 + function handleResults(aElements, aTotalResults) { 1.941 + self._getLocalAddonIds(function searchAddons_getLocalAddonIds(aLocalAddonIds) { 1.942 + self._parseAddons(aElements, aTotalResults, aLocalAddonIds); 1.943 + }); 1.944 + } 1.945 + 1.946 + this._beginSearch(url, aMaxResults, aCallback, handleResults); 1.947 + }, 1.948 + 1.949 + // Posts results to the callback 1.950 + _reportSuccess: function AddonRepo_reportSuccess(aResults, aTotalResults) { 1.951 + this._searching = false; 1.952 + this._request = null; 1.953 + // The callback may want to trigger a new search so clear references early 1.954 + let addons = [result.addon for each(result in aResults)]; 1.955 + let callback = this._callback; 1.956 + this._callback = null; 1.957 + callback.searchSucceeded(addons, addons.length, aTotalResults); 1.958 + }, 1.959 + 1.960 + // Notifies the callback of a failure 1.961 + _reportFailure: function AddonRepo_reportFailure() { 1.962 + this._searching = false; 1.963 + this._request = null; 1.964 + // The callback may want to trigger a new search so clear references early 1.965 + let callback = this._callback; 1.966 + this._callback = null; 1.967 + callback.searchFailed(); 1.968 + }, 1.969 + 1.970 + // Get descendant by unique tag name. Returns null if not unique tag name. 1.971 + _getUniqueDescendant: function AddonRepo_getUniqueDescendant(aElement, aTagName) { 1.972 + let elementsList = aElement.getElementsByTagName(aTagName); 1.973 + return (elementsList.length == 1) ? elementsList[0] : null; 1.974 + }, 1.975 + 1.976 + // Get direct descendant by unique tag name. 1.977 + // Returns null if not unique tag name. 1.978 + _getUniqueDirectDescendant: function AddonRepo_getUniqueDirectDescendant(aElement, aTagName) { 1.979 + let elementsList = Array.filter(aElement.children, 1.980 + function arrayFiltering(aChild) aChild.tagName == aTagName); 1.981 + return (elementsList.length == 1) ? elementsList[0] : null; 1.982 + }, 1.983 + 1.984 + // Parse out trimmed text content. Returns null if text content empty. 1.985 + _getTextContent: function AddonRepo_getTextContent(aElement) { 1.986 + let textContent = aElement.textContent.trim(); 1.987 + return (textContent.length > 0) ? textContent : null; 1.988 + }, 1.989 + 1.990 + // Parse out trimmed text content of a descendant with the specified tag name 1.991 + // Returns null if the parsing unsuccessful. 1.992 + _getDescendantTextContent: function AddonRepo_getDescendantTextContent(aElement, aTagName) { 1.993 + let descendant = this._getUniqueDescendant(aElement, aTagName); 1.994 + return (descendant != null) ? this._getTextContent(descendant) : null; 1.995 + }, 1.996 + 1.997 + // Parse out trimmed text content of a direct descendant with the specified 1.998 + // tag name. 1.999 + // Returns null if the parsing unsuccessful. 1.1000 + _getDirectDescendantTextContent: function AddonRepo_getDirectDescendantTextContent(aElement, aTagName) { 1.1001 + let descendant = this._getUniqueDirectDescendant(aElement, aTagName); 1.1002 + return (descendant != null) ? this._getTextContent(descendant) : null; 1.1003 + }, 1.1004 + 1.1005 + /* 1.1006 + * Creates an AddonSearchResult by parsing an <addon> element 1.1007 + * 1.1008 + * @param aElement 1.1009 + * The <addon> element to parse 1.1010 + * @param aSkip 1.1011 + * Object containing ids and sourceURIs of add-ons to skip. 1.1012 + * @param aCompatData 1.1013 + * Array of parsed addon_compatibility elements to accosiate with the 1.1014 + * resulting AddonSearchResult. Optional. 1.1015 + * @return Result object containing the parsed AddonSearchResult, xpiURL and 1.1016 + * xpiHash if the parsing was successful. Otherwise returns null. 1.1017 + */ 1.1018 + _parseAddon: function AddonRepo_parseAddon(aElement, aSkip, aCompatData) { 1.1019 + let skipIDs = (aSkip && aSkip.ids) ? aSkip.ids : []; 1.1020 + let skipSourceURIs = (aSkip && aSkip.sourceURIs) ? aSkip.sourceURIs : []; 1.1021 + 1.1022 + let guid = this._getDescendantTextContent(aElement, "guid"); 1.1023 + if (guid == null || skipIDs.indexOf(guid) != -1) 1.1024 + return null; 1.1025 + 1.1026 + let addon = new AddonSearchResult(guid); 1.1027 + let result = { 1.1028 + addon: addon, 1.1029 + xpiURL: null, 1.1030 + xpiHash: null 1.1031 + }; 1.1032 + 1.1033 + if (aCompatData && guid in aCompatData) 1.1034 + addon.compatibilityOverrides = aCompatData[guid].compatRanges; 1.1035 + 1.1036 + let self = this; 1.1037 + for (let node = aElement.firstChild; node; node = node.nextSibling) { 1.1038 + if (!(node instanceof Ci.nsIDOMElement)) 1.1039 + continue; 1.1040 + 1.1041 + let localName = node.localName; 1.1042 + 1.1043 + // Handle case where the wanted string value is located in text content 1.1044 + // but only if the content is not empty 1.1045 + if (localName in STRING_KEY_MAP) { 1.1046 + addon[STRING_KEY_MAP[localName]] = this._getTextContent(node) || addon[STRING_KEY_MAP[localName]]; 1.1047 + continue; 1.1048 + } 1.1049 + 1.1050 + // Handle case where the wanted string value is html located in text content 1.1051 + if (localName in HTML_KEY_MAP) { 1.1052 + addon[HTML_KEY_MAP[localName]] = convertHTMLToPlainText(this._getTextContent(node)); 1.1053 + continue; 1.1054 + } 1.1055 + 1.1056 + // Handle case where the wanted integer value is located in text content 1.1057 + if (localName in INTEGER_KEY_MAP) { 1.1058 + let value = parseInt(this._getTextContent(node)); 1.1059 + if (value >= 0) 1.1060 + addon[INTEGER_KEY_MAP[localName]] = value; 1.1061 + continue; 1.1062 + } 1.1063 + 1.1064 + // Handle cases that aren't as simple as grabbing the text content 1.1065 + switch (localName) { 1.1066 + case "type": 1.1067 + // Map AMO's type id to corresponding string 1.1068 + let id = parseInt(node.getAttribute("id")); 1.1069 + switch (id) { 1.1070 + case 1: 1.1071 + addon.type = "extension"; 1.1072 + break; 1.1073 + case 2: 1.1074 + addon.type = "theme"; 1.1075 + break; 1.1076 + case 3: 1.1077 + addon.type = "dictionary"; 1.1078 + break; 1.1079 + default: 1.1080 + logger.warn("Unknown type id when parsing addon: " + id); 1.1081 + } 1.1082 + break; 1.1083 + case "authors": 1.1084 + let authorNodes = node.getElementsByTagName("author"); 1.1085 + for (let authorNode of authorNodes) { 1.1086 + let name = self._getDescendantTextContent(authorNode, "name"); 1.1087 + let link = self._getDescendantTextContent(authorNode, "link"); 1.1088 + if (name == null || link == null) 1.1089 + continue; 1.1090 + 1.1091 + let author = new AddonManagerPrivate.AddonAuthor(name, link); 1.1092 + if (addon.creator == null) 1.1093 + addon.creator = author; 1.1094 + else { 1.1095 + if (addon.developers == null) 1.1096 + addon.developers = []; 1.1097 + 1.1098 + addon.developers.push(author); 1.1099 + } 1.1100 + } 1.1101 + break; 1.1102 + case "previews": 1.1103 + let previewNodes = node.getElementsByTagName("preview"); 1.1104 + for (let previewNode of previewNodes) { 1.1105 + let full = self._getUniqueDescendant(previewNode, "full"); 1.1106 + if (full == null) 1.1107 + continue; 1.1108 + 1.1109 + let fullURL = self._getTextContent(full); 1.1110 + let fullWidth = full.getAttribute("width"); 1.1111 + let fullHeight = full.getAttribute("height"); 1.1112 + 1.1113 + let thumbnailURL, thumbnailWidth, thumbnailHeight; 1.1114 + let thumbnail = self._getUniqueDescendant(previewNode, "thumbnail"); 1.1115 + if (thumbnail) { 1.1116 + thumbnailURL = self._getTextContent(thumbnail); 1.1117 + thumbnailWidth = thumbnail.getAttribute("width"); 1.1118 + thumbnailHeight = thumbnail.getAttribute("height"); 1.1119 + } 1.1120 + let caption = self._getDescendantTextContent(previewNode, "caption"); 1.1121 + let screenshot = new AddonManagerPrivate.AddonScreenshot(fullURL, fullWidth, fullHeight, 1.1122 + thumbnailURL, thumbnailWidth, 1.1123 + thumbnailHeight, caption); 1.1124 + 1.1125 + if (addon.screenshots == null) 1.1126 + addon.screenshots = []; 1.1127 + 1.1128 + if (previewNode.getAttribute("primary") == 1) 1.1129 + addon.screenshots.unshift(screenshot); 1.1130 + else 1.1131 + addon.screenshots.push(screenshot); 1.1132 + } 1.1133 + break; 1.1134 + case "learnmore": 1.1135 + addon.learnmoreURL = this._getTextContent(node); 1.1136 + addon.homepageURL = addon.homepageURL || addon.learnmoreURL; 1.1137 + break; 1.1138 + case "contribution_data": 1.1139 + let meetDevelopers = this._getDescendantTextContent(node, "meet_developers"); 1.1140 + let suggestedAmount = this._getDescendantTextContent(node, "suggested_amount"); 1.1141 + if (meetDevelopers != null) { 1.1142 + addon.contributionURL = meetDevelopers; 1.1143 + addon.contributionAmount = suggestedAmount; 1.1144 + } 1.1145 + break 1.1146 + case "payment_data": 1.1147 + let link = this._getDescendantTextContent(node, "link"); 1.1148 + let amountTag = this._getUniqueDescendant(node, "amount"); 1.1149 + let amount = parseFloat(amountTag.getAttribute("amount")); 1.1150 + let displayAmount = this._getTextContent(amountTag); 1.1151 + if (link != null && amount != null && displayAmount != null) { 1.1152 + addon.purchaseURL = link; 1.1153 + addon.purchaseAmount = amount; 1.1154 + addon.purchaseDisplayAmount = displayAmount; 1.1155 + } 1.1156 + break 1.1157 + case "rating": 1.1158 + let averageRating = parseInt(this._getTextContent(node)); 1.1159 + if (averageRating >= 0) 1.1160 + addon.averageRating = Math.min(5, averageRating); 1.1161 + break; 1.1162 + case "reviews": 1.1163 + let url = this._getTextContent(node); 1.1164 + let num = parseInt(node.getAttribute("num")); 1.1165 + if (url != null && num >= 0) { 1.1166 + addon.reviewURL = url; 1.1167 + addon.reviewCount = num; 1.1168 + } 1.1169 + break; 1.1170 + case "status": 1.1171 + let repositoryStatus = parseInt(node.getAttribute("id")); 1.1172 + if (!isNaN(repositoryStatus)) 1.1173 + addon.repositoryStatus = repositoryStatus; 1.1174 + break; 1.1175 + case "all_compatible_os": 1.1176 + let nodes = node.getElementsByTagName("os"); 1.1177 + addon.isPlatformCompatible = Array.some(nodes, function parseAddon_platformCompatFilter(aNode) { 1.1178 + let text = aNode.textContent.toLowerCase().trim(); 1.1179 + return text == "all" || text == Services.appinfo.OS.toLowerCase(); 1.1180 + }); 1.1181 + break; 1.1182 + case "install": 1.1183 + // No os attribute means the xpi is compatible with any os 1.1184 + if (node.hasAttribute("os")) { 1.1185 + let os = node.getAttribute("os").trim().toLowerCase(); 1.1186 + // If the os is not ALL and not the current OS then ignore this xpi 1.1187 + if (os != "all" && os != Services.appinfo.OS.toLowerCase()) 1.1188 + break; 1.1189 + } 1.1190 + 1.1191 + let xpiURL = this._getTextContent(node); 1.1192 + if (xpiURL == null) 1.1193 + break; 1.1194 + 1.1195 + if (skipSourceURIs.indexOf(xpiURL) != -1) 1.1196 + return null; 1.1197 + 1.1198 + result.xpiURL = xpiURL; 1.1199 + addon.sourceURI = NetUtil.newURI(xpiURL); 1.1200 + 1.1201 + let size = parseInt(node.getAttribute("size")); 1.1202 + addon.size = (size >= 0) ? size : null; 1.1203 + 1.1204 + let xpiHash = node.getAttribute("hash"); 1.1205 + if (xpiHash != null) 1.1206 + xpiHash = xpiHash.trim(); 1.1207 + result.xpiHash = xpiHash ? xpiHash : null; 1.1208 + break; 1.1209 + case "last_updated": 1.1210 + let epoch = parseInt(node.getAttribute("epoch")); 1.1211 + if (!isNaN(epoch)) 1.1212 + addon.updateDate = new Date(1000 * epoch); 1.1213 + break; 1.1214 + case "icon": 1.1215 + addon.icons[node.getAttribute("size")] = this._getTextContent(node); 1.1216 + break; 1.1217 + } 1.1218 + } 1.1219 + 1.1220 + return result; 1.1221 + }, 1.1222 + 1.1223 + _parseAddons: function AddonRepo_parseAddons(aElements, aTotalResults, aSkip) { 1.1224 + let self = this; 1.1225 + let results = []; 1.1226 + 1.1227 + function isSameApplication(aAppNode) { 1.1228 + return self._getTextContent(aAppNode) == Services.appinfo.ID; 1.1229 + } 1.1230 + 1.1231 + for (let i = 0; i < aElements.length && results.length < this._maxResults; i++) { 1.1232 + let element = aElements[i]; 1.1233 + 1.1234 + let tags = this._getUniqueDescendant(element, "compatible_applications"); 1.1235 + if (tags == null) 1.1236 + continue; 1.1237 + 1.1238 + let applications = tags.getElementsByTagName("appID"); 1.1239 + let compatible = Array.some(applications, function parseAddons_applicationsCompatFilter(aAppNode) { 1.1240 + if (!isSameApplication(aAppNode)) 1.1241 + return false; 1.1242 + 1.1243 + let parent = aAppNode.parentNode; 1.1244 + let minVersion = self._getDescendantTextContent(parent, "min_version"); 1.1245 + let maxVersion = self._getDescendantTextContent(parent, "max_version"); 1.1246 + if (minVersion == null || maxVersion == null) 1.1247 + return false; 1.1248 + 1.1249 + let currentVersion = Services.appinfo.version; 1.1250 + return (Services.vc.compare(minVersion, currentVersion) <= 0 && 1.1251 + ((!AddonManager.strictCompatibility) || 1.1252 + Services.vc.compare(currentVersion, maxVersion) <= 0)); 1.1253 + }); 1.1254 + 1.1255 + // Ignore add-ons not compatible with this Application 1.1256 + if (!compatible) { 1.1257 + if (AddonManager.checkCompatibility) 1.1258 + continue; 1.1259 + 1.1260 + if (!Array.some(applications, isSameApplication)) 1.1261 + continue; 1.1262 + } 1.1263 + 1.1264 + // Add-on meets all requirements, so parse out data. 1.1265 + // Don't pass in compatiblity override data, because that's only returned 1.1266 + // in GUID searches, which don't use _parseAddons(). 1.1267 + let result = this._parseAddon(element, aSkip); 1.1268 + if (result == null) 1.1269 + continue; 1.1270 + 1.1271 + // Ignore add-on missing a required attribute 1.1272 + let requiredAttributes = ["id", "name", "version", "type", "creator"]; 1.1273 + if (requiredAttributes.some(function parseAddons_attributeFilter(aAttribute) !result.addon[aAttribute])) 1.1274 + continue; 1.1275 + 1.1276 + // Add only if the add-on is compatible with the platform 1.1277 + if (!result.addon.isPlatformCompatible) 1.1278 + continue; 1.1279 + 1.1280 + // Add only if there was an xpi compatible with this OS or there was a 1.1281 + // way to purchase the add-on 1.1282 + if (!result.xpiURL && !result.addon.purchaseURL) 1.1283 + continue; 1.1284 + 1.1285 + result.addon.isCompatible = compatible; 1.1286 + 1.1287 + results.push(result); 1.1288 + // Ignore this add-on from now on by adding it to the skip array 1.1289 + aSkip.ids.push(result.addon.id); 1.1290 + } 1.1291 + 1.1292 + // Immediately report success if no AddonInstall instances to create 1.1293 + let pendingResults = results.length; 1.1294 + if (pendingResults == 0) { 1.1295 + this._reportSuccess(results, aTotalResults); 1.1296 + return; 1.1297 + } 1.1298 + 1.1299 + // Create an AddonInstall for each result 1.1300 + let self = this; 1.1301 + results.forEach(function(aResult) { 1.1302 + let addon = aResult.addon; 1.1303 + let callback = function addonInstallCallback(aInstall) { 1.1304 + addon.install = aInstall; 1.1305 + pendingResults--; 1.1306 + if (pendingResults == 0) 1.1307 + self._reportSuccess(results, aTotalResults); 1.1308 + } 1.1309 + 1.1310 + if (aResult.xpiURL) { 1.1311 + AddonManager.getInstallForURL(aResult.xpiURL, callback, 1.1312 + "application/x-xpinstall", aResult.xpiHash, 1.1313 + addon.name, addon.icons, addon.version); 1.1314 + } 1.1315 + else { 1.1316 + callback(null); 1.1317 + } 1.1318 + }); 1.1319 + }, 1.1320 + 1.1321 + // Parses addon_compatibility nodes, that describe compatibility overrides. 1.1322 + _parseAddonCompatElement: function AddonRepo_parseAddonCompatElement(aResultObj, aElement) { 1.1323 + let guid = this._getDescendantTextContent(aElement, "guid"); 1.1324 + if (!guid) { 1.1325 + logger.debug("Compatibility override is missing guid."); 1.1326 + return; 1.1327 + } 1.1328 + 1.1329 + let compat = {id: guid}; 1.1330 + compat.hosted = aElement.getAttribute("hosted") != "false"; 1.1331 + 1.1332 + function findMatchingAppRange(aNodes) { 1.1333 + let toolkitAppRange = null; 1.1334 + for (let node of aNodes) { 1.1335 + let appID = this._getDescendantTextContent(node, "appID"); 1.1336 + if (appID != Services.appinfo.ID && appID != TOOLKIT_ID) 1.1337 + continue; 1.1338 + 1.1339 + let minVersion = this._getDescendantTextContent(node, "min_version"); 1.1340 + let maxVersion = this._getDescendantTextContent(node, "max_version"); 1.1341 + if (minVersion == null || maxVersion == null) 1.1342 + continue; 1.1343 + 1.1344 + let appRange = { appID: appID, 1.1345 + appMinVersion: minVersion, 1.1346 + appMaxVersion: maxVersion }; 1.1347 + 1.1348 + // Only use Toolkit app ranges if no ranges match the application ID. 1.1349 + if (appID == TOOLKIT_ID) 1.1350 + toolkitAppRange = appRange; 1.1351 + else 1.1352 + return appRange; 1.1353 + } 1.1354 + return toolkitAppRange; 1.1355 + } 1.1356 + 1.1357 + function parseRangeNode(aNode) { 1.1358 + let type = aNode.getAttribute("type"); 1.1359 + // Only "incompatible" (blacklisting) is supported for now. 1.1360 + if (type != "incompatible") { 1.1361 + logger.debug("Compatibility override of unsupported type found."); 1.1362 + return null; 1.1363 + } 1.1364 + 1.1365 + let override = new AddonManagerPrivate.AddonCompatibilityOverride(type); 1.1366 + 1.1367 + override.minVersion = this._getDirectDescendantTextContent(aNode, "min_version"); 1.1368 + override.maxVersion = this._getDirectDescendantTextContent(aNode, "max_version"); 1.1369 + 1.1370 + if (!override.minVersion) { 1.1371 + logger.debug("Compatibility override is missing min_version."); 1.1372 + return null; 1.1373 + } 1.1374 + if (!override.maxVersion) { 1.1375 + logger.debug("Compatibility override is missing max_version."); 1.1376 + return null; 1.1377 + } 1.1378 + 1.1379 + let appRanges = aNode.querySelectorAll("compatible_applications > application"); 1.1380 + let appRange = findMatchingAppRange.bind(this)(appRanges); 1.1381 + if (!appRange) { 1.1382 + logger.debug("Compatibility override is missing a valid application range."); 1.1383 + return null; 1.1384 + } 1.1385 + 1.1386 + override.appID = appRange.appID; 1.1387 + override.appMinVersion = appRange.appMinVersion; 1.1388 + override.appMaxVersion = appRange.appMaxVersion; 1.1389 + 1.1390 + return override; 1.1391 + } 1.1392 + 1.1393 + let rangeNodes = aElement.querySelectorAll("version_ranges > version_range"); 1.1394 + compat.compatRanges = Array.map(rangeNodes, parseRangeNode.bind(this)) 1.1395 + .filter(function compatRangesFilter(aItem) !!aItem); 1.1396 + if (compat.compatRanges.length == 0) 1.1397 + return; 1.1398 + 1.1399 + aResultObj[compat.id] = compat; 1.1400 + }, 1.1401 + 1.1402 + // Parses addon_compatibility elements. 1.1403 + _parseAddonCompatData: function AddonRepo_parseAddonCompatData(aElements) { 1.1404 + let compatData = {}; 1.1405 + Array.forEach(aElements, this._parseAddonCompatElement.bind(this, compatData)); 1.1406 + return compatData; 1.1407 + }, 1.1408 + 1.1409 + // Begins a new search if one isn't currently executing 1.1410 + _beginSearch: function(aURI, aMaxResults, aCallback, aHandleResults, aTimeout) { 1.1411 + if (this._searching || aURI == null || aMaxResults <= 0) { 1.1412 + logger.warn("AddonRepository search failed: searching " + this._searching + " aURI " + aURI + 1.1413 + " aMaxResults " + aMaxResults); 1.1414 + aCallback.searchFailed(); 1.1415 + return; 1.1416 + } 1.1417 + 1.1418 + this._searching = true; 1.1419 + this._callback = aCallback; 1.1420 + this._maxResults = aMaxResults; 1.1421 + 1.1422 + logger.debug("Requesting " + aURI); 1.1423 + 1.1424 + this._request = new XHRequest(); 1.1425 + this._request.mozBackgroundRequest = true; 1.1426 + this._request.open("GET", aURI, true); 1.1427 + this._request.overrideMimeType("text/xml"); 1.1428 + if (aTimeout) { 1.1429 + this._request.timeout = aTimeout; 1.1430 + } 1.1431 + 1.1432 + this._request.addEventListener("error", aEvent => this._reportFailure(), false); 1.1433 + this._request.addEventListener("timeout", aEvent => this._reportFailure(), false); 1.1434 + this._request.addEventListener("load", aEvent => { 1.1435 + logger.debug("Got metadata search load event"); 1.1436 + let request = aEvent.target; 1.1437 + let responseXML = request.responseXML; 1.1438 + 1.1439 + if (!responseXML || responseXML.documentElement.namespaceURI == XMLURI_PARSE_ERROR || 1.1440 + (request.status != 200 && request.status != 0)) { 1.1441 + this._reportFailure(); 1.1442 + return; 1.1443 + } 1.1444 + 1.1445 + let documentElement = responseXML.documentElement; 1.1446 + let elements = documentElement.getElementsByTagName("addon"); 1.1447 + let totalResults = elements.length; 1.1448 + let parsedTotalResults = parseInt(documentElement.getAttribute("total_results")); 1.1449 + // Parsed value of total results only makes sense if >= elements.length 1.1450 + if (parsedTotalResults >= totalResults) 1.1451 + totalResults = parsedTotalResults; 1.1452 + 1.1453 + let compatElements = documentElement.getElementsByTagName("addon_compatibility"); 1.1454 + let compatData = this._parseAddonCompatData(compatElements); 1.1455 + 1.1456 + aHandleResults(elements, totalResults, compatData); 1.1457 + }, false); 1.1458 + this._request.send(null); 1.1459 + }, 1.1460 + 1.1461 + // Gets the id's of local add-ons, and the sourceURI's of local installs, 1.1462 + // passing the results to aCallback 1.1463 + _getLocalAddonIds: function AddonRepo_getLocalAddonIds(aCallback) { 1.1464 + let self = this; 1.1465 + let localAddonIds = {ids: null, sourceURIs: null}; 1.1466 + 1.1467 + AddonManager.getAllAddons(function getLocalAddonIds_getAllAddons(aAddons) { 1.1468 + localAddonIds.ids = [a.id for each (a in aAddons)]; 1.1469 + if (localAddonIds.sourceURIs) 1.1470 + aCallback(localAddonIds); 1.1471 + }); 1.1472 + 1.1473 + AddonManager.getAllInstalls(function getLocalAddonIds_getAllInstalls(aInstalls) { 1.1474 + localAddonIds.sourceURIs = []; 1.1475 + aInstalls.forEach(function(aInstall) { 1.1476 + if (aInstall.state != AddonManager.STATE_AVAILABLE) 1.1477 + localAddonIds.sourceURIs.push(aInstall.sourceURI.spec); 1.1478 + }); 1.1479 + 1.1480 + if (localAddonIds.ids) 1.1481 + aCallback(localAddonIds); 1.1482 + }); 1.1483 + }, 1.1484 + 1.1485 + // Create url from preference, returning null if preference does not exist 1.1486 + _formatURLPref: function AddonRepo_formatURLPref(aPreference, aSubstitutions) { 1.1487 + let url = null; 1.1488 + try { 1.1489 + url = Services.prefs.getCharPref(aPreference); 1.1490 + } catch(e) { 1.1491 + logger.warn("_formatURLPref: Couldn't get pref: " + aPreference); 1.1492 + return null; 1.1493 + } 1.1494 + 1.1495 + url = url.replace(/%([A-Z_]+)%/g, function urlSubstitution(aMatch, aKey) { 1.1496 + return (aKey in aSubstitutions) ? aSubstitutions[aKey] : aMatch; 1.1497 + }); 1.1498 + 1.1499 + return Services.urlFormatter.formatURL(url); 1.1500 + }, 1.1501 + 1.1502 + // Find a AddonCompatibilityOverride that matches a given aAddonVersion and 1.1503 + // application/platform version. 1.1504 + findMatchingCompatOverride: function AddonRepo_findMatchingCompatOverride(aAddonVersion, 1.1505 + aCompatOverrides, 1.1506 + aAppVersion, 1.1507 + aPlatformVersion) { 1.1508 + for (let override of aCompatOverrides) { 1.1509 + 1.1510 + let appVersion = null; 1.1511 + if (override.appID == TOOLKIT_ID) 1.1512 + appVersion = aPlatformVersion || Services.appinfo.platformVersion; 1.1513 + else 1.1514 + appVersion = aAppVersion || Services.appinfo.version; 1.1515 + 1.1516 + if (Services.vc.compare(override.minVersion, aAddonVersion) <= 0 && 1.1517 + Services.vc.compare(aAddonVersion, override.maxVersion) <= 0 && 1.1518 + Services.vc.compare(override.appMinVersion, appVersion) <= 0 && 1.1519 + Services.vc.compare(appVersion, override.appMaxVersion) <= 0) { 1.1520 + return override; 1.1521 + } 1.1522 + } 1.1523 + return null; 1.1524 + }, 1.1525 + 1.1526 + flush: function() { 1.1527 + return AddonDatabase.flush(); 1.1528 + } 1.1529 +}; 1.1530 + 1.1531 +var AddonDatabase = { 1.1532 + // true if the database connection has been opened 1.1533 + initialized: false, 1.1534 + // false if there was an unrecoverable error openning the database 1.1535 + databaseOk: true, 1.1536 + 1.1537 + // the in-memory database 1.1538 + DB: BLANK_DB(), 1.1539 + 1.1540 + /** 1.1541 + * A getter to retrieve an nsIFile pointer to the DB 1.1542 + */ 1.1543 + get jsonFile() { 1.1544 + delete this.jsonFile; 1.1545 + return this.jsonFile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_DATABASE], true); 1.1546 + }, 1.1547 + 1.1548 + /** 1.1549 + * Synchronously opens a new connection to the database file. 1.1550 + */ 1.1551 + openConnection: function() { 1.1552 + this.DB = BLANK_DB(); 1.1553 + this.initialized = true; 1.1554 + delete this.connection; 1.1555 + 1.1556 + let inputDB, fstream, cstream, schema; 1.1557 + 1.1558 + try { 1.1559 + let data = ""; 1.1560 + fstream = Cc["@mozilla.org/network/file-input-stream;1"] 1.1561 + .createInstance(Ci.nsIFileInputStream); 1.1562 + cstream = Cc["@mozilla.org/intl/converter-input-stream;1"] 1.1563 + .createInstance(Ci.nsIConverterInputStream); 1.1564 + 1.1565 + fstream.init(this.jsonFile, -1, 0, 0); 1.1566 + cstream.init(fstream, "UTF-8", 0, 0); 1.1567 + let (str = {}) { 1.1568 + let read = 0; 1.1569 + do { 1.1570 + read = cstream.readString(0xffffffff, str); // read as much as we can and put it in str.value 1.1571 + data += str.value; 1.1572 + } while (read != 0); 1.1573 + } 1.1574 + 1.1575 + inputDB = JSON.parse(data); 1.1576 + 1.1577 + if (!inputDB.hasOwnProperty("addons") || 1.1578 + !Array.isArray(inputDB.addons)) { 1.1579 + throw new Error("No addons array."); 1.1580 + } 1.1581 + 1.1582 + if (!inputDB.hasOwnProperty("schema")) { 1.1583 + throw new Error("No schema specified."); 1.1584 + } 1.1585 + 1.1586 + schema = parseInt(inputDB.schema, 10); 1.1587 + 1.1588 + if (!Number.isInteger(schema) || 1.1589 + schema < DB_MIN_JSON_SCHEMA) { 1.1590 + throw new Error("Invalid schema value."); 1.1591 + } 1.1592 + 1.1593 + } catch (e if e.result == Cr.NS_ERROR_FILE_NOT_FOUND) { 1.1594 + logger.debug("No " + FILE_DATABASE + " found."); 1.1595 + 1.1596 + // Create a blank addons.json file 1.1597 + this._saveDBToDisk(); 1.1598 + 1.1599 + let dbSchema = 0; 1.1600 + try { 1.1601 + dbSchema = Services.prefs.getIntPref(PREF_GETADDONS_DB_SCHEMA); 1.1602 + } catch (e) {} 1.1603 + 1.1604 + if (dbSchema < DB_MIN_JSON_SCHEMA) { 1.1605 + this._migrationInProgress = AddonRepository_SQLiteMigrator.migrate((results) => { 1.1606 + if (results.length) 1.1607 + this.insertAddons(results); 1.1608 + 1.1609 + if (this._postMigrationCallback) { 1.1610 + this._postMigrationCallback(); 1.1611 + this._postMigrationCallback = null; 1.1612 + } 1.1613 + 1.1614 + this._migrationInProgress = false; 1.1615 + }); 1.1616 + 1.1617 + Services.prefs.setIntPref(PREF_GETADDONS_DB_SCHEMA, DB_SCHEMA); 1.1618 + } 1.1619 + 1.1620 + return; 1.1621 + 1.1622 + } catch (e) { 1.1623 + logger.error("Malformed " + FILE_DATABASE + ": " + e); 1.1624 + this.databaseOk = false; 1.1625 + return; 1.1626 + 1.1627 + } finally { 1.1628 + cstream.close(); 1.1629 + fstream.close(); 1.1630 + } 1.1631 + 1.1632 + Services.prefs.setIntPref(PREF_GETADDONS_DB_SCHEMA, DB_SCHEMA); 1.1633 + 1.1634 + // We use _insertAddon manually instead of calling 1.1635 + // insertAddons to avoid the write to disk which would 1.1636 + // be a waste since this is the data that was just read. 1.1637 + for (let addon of inputDB.addons) { 1.1638 + this._insertAddon(addon); 1.1639 + } 1.1640 + }, 1.1641 + 1.1642 + /** 1.1643 + * A lazy getter for the database connection. 1.1644 + */ 1.1645 + get connection() { 1.1646 + return this.openConnection(); 1.1647 + }, 1.1648 + 1.1649 + /** 1.1650 + * Asynchronously shuts down the database connection and releases all 1.1651 + * cached objects 1.1652 + * 1.1653 + * @param aCallback 1.1654 + * An optional callback to call once complete 1.1655 + * @param aSkipFlush 1.1656 + * An optional boolean to skip flushing data to disk. Useful 1.1657 + * when the database is going to be deleted afterwards. 1.1658 + */ 1.1659 + shutdown: function AD_shutdown(aSkipFlush) { 1.1660 + this.databaseOk = true; 1.1661 + 1.1662 + if (!this.initialized) { 1.1663 + return Promise.resolve(0); 1.1664 + } 1.1665 + 1.1666 + this.initialized = false; 1.1667 + 1.1668 + this.__defineGetter__("connection", function shutdown_connectionGetter() { 1.1669 + return this.openConnection(); 1.1670 + }); 1.1671 + 1.1672 + if (aSkipFlush) { 1.1673 + return Promise.resolve(0); 1.1674 + } else { 1.1675 + return this.Writer.flush(); 1.1676 + } 1.1677 + }, 1.1678 + 1.1679 + /** 1.1680 + * Asynchronously deletes the database, shutting down the connection 1.1681 + * first if initialized 1.1682 + * 1.1683 + * @param aCallback 1.1684 + * An optional callback to call once complete 1.1685 + */ 1.1686 + delete: function AD_delete(aCallback) { 1.1687 + this.DB = BLANK_DB(); 1.1688 + 1.1689 + this._deleting = this.Writer.flush() 1.1690 + .then(null, () => {}) 1.1691 + // shutdown(true) never rejects 1.1692 + .then(() => this.shutdown(true)) 1.1693 + .then(() => OS.File.remove(this.jsonFile.path, {})) 1.1694 + .then(null, error => logger.error("Unable to delete Addon Repository file " + 1.1695 + this.jsonFile.path, error)) 1.1696 + .then(() => this._deleting = null) 1.1697 + .then(aCallback); 1.1698 + }, 1.1699 + 1.1700 + toJSON: function AD_toJSON() { 1.1701 + let json = { 1.1702 + schema: this.DB.schema, 1.1703 + addons: [] 1.1704 + } 1.1705 + 1.1706 + for (let [, value] of this.DB.addons) 1.1707 + json.addons.push(value); 1.1708 + 1.1709 + return json; 1.1710 + }, 1.1711 + 1.1712 + /* 1.1713 + * This is a deferred task writer that is used 1.1714 + * to batch operations done within 50ms of each 1.1715 + * other and thus generating only one write to disk 1.1716 + */ 1.1717 + get Writer() { 1.1718 + delete this.Writer; 1.1719 + this.Writer = new DeferredSave( 1.1720 + this.jsonFile.path, 1.1721 + () => { return JSON.stringify(this); }, 1.1722 + DB_BATCH_TIMEOUT_MS 1.1723 + ); 1.1724 + return this.Writer; 1.1725 + }, 1.1726 + 1.1727 + /** 1.1728 + * Flush any pending I/O on the addons.json file 1.1729 + * @return: Promise{null} 1.1730 + * Resolves when the pending I/O (writing out or deleting 1.1731 + * addons.json) completes 1.1732 + */ 1.1733 + flush: function() { 1.1734 + if (this._deleting) { 1.1735 + return this._deleting; 1.1736 + } 1.1737 + return this.Writer.flush(); 1.1738 + }, 1.1739 + 1.1740 + /** 1.1741 + * Asynchronously retrieve all add-ons from the database, and pass it 1.1742 + * to the specified callback 1.1743 + * 1.1744 + * @param aCallback 1.1745 + * The callback to pass the add-ons back to 1.1746 + */ 1.1747 + retrieveStoredData: function AD_retrieveStoredData(aCallback) { 1.1748 + if (!this.initialized) 1.1749 + this.openConnection(); 1.1750 + 1.1751 + let gatherResults = () => { 1.1752 + let result = {}; 1.1753 + for (let [key, value] of this.DB.addons) 1.1754 + result[key] = value; 1.1755 + 1.1756 + executeSoon(function() aCallback(result)); 1.1757 + }; 1.1758 + 1.1759 + if (this._migrationInProgress) 1.1760 + this._postMigrationCallback = gatherResults; 1.1761 + else 1.1762 + gatherResults(); 1.1763 + }, 1.1764 + 1.1765 + /** 1.1766 + * Asynchronously repopulates the database so it only contains the 1.1767 + * specified add-ons 1.1768 + * 1.1769 + * @param aAddons 1.1770 + * The array of add-ons to repopulate the database with 1.1771 + * @param aCallback 1.1772 + * An optional callback to call once complete 1.1773 + */ 1.1774 + repopulate: function AD_repopulate(aAddons, aCallback) { 1.1775 + this.DB.addons.clear(); 1.1776 + this.insertAddons(aAddons, aCallback); 1.1777 + }, 1.1778 + 1.1779 + /** 1.1780 + * Asynchronously inserts an array of add-ons into the database 1.1781 + * 1.1782 + * @param aAddons 1.1783 + * The array of add-ons to insert 1.1784 + * @param aCallback 1.1785 + * An optional callback to call once complete 1.1786 + */ 1.1787 + insertAddons: function AD_insertAddons(aAddons, aCallback) { 1.1788 + if (!this.initialized) 1.1789 + this.openConnection(); 1.1790 + 1.1791 + for (let addon of aAddons) { 1.1792 + this._insertAddon(addon); 1.1793 + } 1.1794 + 1.1795 + this._saveDBToDisk(); 1.1796 + 1.1797 + if (aCallback) 1.1798 + executeSoon(aCallback); 1.1799 + }, 1.1800 + 1.1801 + /** 1.1802 + * Inserts an individual add-on into the database. If the add-on already 1.1803 + * exists in the database (by id), then the specified add-on will not be 1.1804 + * inserted. 1.1805 + * 1.1806 + * @param aAddon 1.1807 + * The add-on to insert into the database 1.1808 + * @param aCallback 1.1809 + * The callback to call once complete 1.1810 + */ 1.1811 + _insertAddon: function AD__insertAddon(aAddon) { 1.1812 + let newAddon = this._parseAddon(aAddon); 1.1813 + if (!newAddon || 1.1814 + !newAddon.id || 1.1815 + this.DB.addons.has(newAddon.id)) 1.1816 + return; 1.1817 + 1.1818 + this.DB.addons.set(newAddon.id, newAddon); 1.1819 + }, 1.1820 + 1.1821 + /* 1.1822 + * Creates an AddonSearchResult by parsing an object structure 1.1823 + * retrieved from the DB JSON representation. 1.1824 + * 1.1825 + * @param aObj 1.1826 + * The object to parse 1.1827 + * @return Returns an AddonSearchResult object. 1.1828 + */ 1.1829 + _parseAddon: function (aObj) { 1.1830 + if (aObj instanceof AddonSearchResult) 1.1831 + return aObj; 1.1832 + 1.1833 + let id = aObj.id; 1.1834 + if (!aObj.id) 1.1835 + return null; 1.1836 + 1.1837 + let addon = new AddonSearchResult(id); 1.1838 + 1.1839 + for (let [expectedProperty,] of Iterator(AddonSearchResult.prototype)) { 1.1840 + if (!(expectedProperty in aObj) || 1.1841 + typeof(aObj[expectedProperty]) === "function") 1.1842 + continue; 1.1843 + 1.1844 + let value = aObj[expectedProperty]; 1.1845 + 1.1846 + try { 1.1847 + switch (expectedProperty) { 1.1848 + case "sourceURI": 1.1849 + addon.sourceURI = value ? NetUtil.newURI(value) : null; 1.1850 + break; 1.1851 + 1.1852 + case "creator": 1.1853 + addon.creator = value 1.1854 + ? this._makeDeveloper(value) 1.1855 + : null; 1.1856 + break; 1.1857 + 1.1858 + case "updateDate": 1.1859 + addon.updateDate = value ? new Date(value) : null; 1.1860 + break; 1.1861 + 1.1862 + case "developers": 1.1863 + if (!addon.developers) addon.developers = []; 1.1864 + for (let developer of value) { 1.1865 + addon.developers.push(this._makeDeveloper(developer)); 1.1866 + } 1.1867 + break; 1.1868 + 1.1869 + case "screenshots": 1.1870 + if (!addon.screenshots) addon.screenshots = []; 1.1871 + for (let screenshot of value) { 1.1872 + addon.screenshots.push(this._makeScreenshot(screenshot)); 1.1873 + } 1.1874 + break; 1.1875 + 1.1876 + case "compatibilityOverrides": 1.1877 + if (!addon.compatibilityOverrides) addon.compatibilityOverrides = []; 1.1878 + for (let override of value) { 1.1879 + addon.compatibilityOverrides.push( 1.1880 + this._makeCompatOverride(override) 1.1881 + ); 1.1882 + } 1.1883 + break; 1.1884 + 1.1885 + case "icons": 1.1886 + if (!addon.icons) addon.icons = {}; 1.1887 + for (let [size, url] of Iterator(aObj.icons)) { 1.1888 + addon.icons[size] = url; 1.1889 + } 1.1890 + break; 1.1891 + 1.1892 + case "iconURL": 1.1893 + break; 1.1894 + 1.1895 + default: 1.1896 + addon[expectedProperty] = value; 1.1897 + } 1.1898 + } catch (ex) { 1.1899 + logger.warn("Error in parsing property value for " + expectedProperty + " | " + ex); 1.1900 + } 1.1901 + 1.1902 + // delete property from obj to indicate we've already 1.1903 + // handled it. The remaining public properties will 1.1904 + // be stored separately and just passed through to 1.1905 + // be written back to the DB. 1.1906 + delete aObj[expectedProperty]; 1.1907 + } 1.1908 + 1.1909 + // Copy remaining properties to a separate object 1.1910 + // to prevent accidental access on downgraded versions. 1.1911 + // The properties will be merged in the same object 1.1912 + // prior to being written back through toJSON. 1.1913 + for (let remainingProperty of Object.keys(aObj)) { 1.1914 + switch (typeof(aObj[remainingProperty])) { 1.1915 + case "boolean": 1.1916 + case "number": 1.1917 + case "string": 1.1918 + case "object": 1.1919 + // these types are accepted 1.1920 + break; 1.1921 + default: 1.1922 + continue; 1.1923 + } 1.1924 + 1.1925 + if (!remainingProperty.startsWith("_")) 1.1926 + addon._unsupportedProperties[remainingProperty] = 1.1927 + aObj[remainingProperty]; 1.1928 + } 1.1929 + 1.1930 + return addon; 1.1931 + }, 1.1932 + 1.1933 + /** 1.1934 + * Write the in-memory DB to disk, after waiting for 1.1935 + * the DB_BATCH_TIMEOUT_MS timeout. 1.1936 + * 1.1937 + * @return Promise A promise that resolves after the 1.1938 + * write to disk has completed. 1.1939 + */ 1.1940 + _saveDBToDisk: function() { 1.1941 + return this.Writer.saveChanges().then( 1.1942 + null, 1.1943 + e => logger.error("SaveDBToDisk failed", e)); 1.1944 + }, 1.1945 + 1.1946 + /** 1.1947 + * Make a developer object from a vanilla 1.1948 + * JS object from the JSON database 1.1949 + * 1.1950 + * @param aObj 1.1951 + * The JS object to use 1.1952 + * @return The created developer 1.1953 + */ 1.1954 + _makeDeveloper: function (aObj) { 1.1955 + let name = aObj.name; 1.1956 + let url = aObj.url; 1.1957 + return new AddonManagerPrivate.AddonAuthor(name, url); 1.1958 + }, 1.1959 + 1.1960 + /** 1.1961 + * Make a screenshot object from a vanilla 1.1962 + * JS object from the JSON database 1.1963 + * 1.1964 + * @param aObj 1.1965 + * The JS object to use 1.1966 + * @return The created screenshot 1.1967 + */ 1.1968 + _makeScreenshot: function (aObj) { 1.1969 + let url = aObj.url; 1.1970 + let width = aObj.width; 1.1971 + let height = aObj.height; 1.1972 + let thumbnailURL = aObj.thumbnailURL; 1.1973 + let thumbnailWidth = aObj.thumbnailWidth; 1.1974 + let thumbnailHeight = aObj.thumbnailHeight; 1.1975 + let caption = aObj.caption; 1.1976 + return new AddonManagerPrivate.AddonScreenshot(url, width, height, thumbnailURL, 1.1977 + thumbnailWidth, thumbnailHeight, caption); 1.1978 + }, 1.1979 + 1.1980 + /** 1.1981 + * Make a CompatibilityOverride from a vanilla 1.1982 + * JS object from the JSON database 1.1983 + * 1.1984 + * @param aObj 1.1985 + * The JS object to use 1.1986 + * @return The created CompatibilityOverride 1.1987 + */ 1.1988 + _makeCompatOverride: function (aObj) { 1.1989 + let type = aObj.type; 1.1990 + let minVersion = aObj.minVersion; 1.1991 + let maxVersion = aObj.maxVersion; 1.1992 + let appID = aObj.appID; 1.1993 + let appMinVersion = aObj.appMinVersion; 1.1994 + let appMaxVersion = aObj.appMaxVersion; 1.1995 + return new AddonManagerPrivate.AddonCompatibilityOverride(type, 1.1996 + minVersion, 1.1997 + maxVersion, 1.1998 + appID, 1.1999 + appMinVersion, 1.2000 + appMaxVersion); 1.2001 + }, 1.2002 +}; 1.2003 + 1.2004 +function executeSoon(aCallback) { 1.2005 + Services.tm.mainThread.dispatch(aCallback, Ci.nsIThread.DISPATCH_NORMAL); 1.2006 +}