1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
9 ChromeUtils.defineESModuleGetters(lazy, {
10 AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
11 AddonManagerPrivate: "resource://gre/modules/AddonManager.sys.mjs",
12 AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
13 DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
14 NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
15 ServiceRequest: "resource://gre/modules/ServiceRequest.sys.mjs",
18 // The current platform as specified in the AMO API:
19 // http://addons-server.readthedocs.io/en/latest/topics/api/addons.html#addon-detail-platform
20 ChromeUtils.defineLazyGetter(lazy, "PLATFORM", () => {
21 let platform = Services.appinfo.OS;
38 const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled";
40 XPCOMUtils.defineLazyPreferenceGetter(
42 "getAddonsCacheEnabled",
43 PREF_GETADDONS_CACHE_ENABLED
46 const PREF_GETADDONS_CACHE_TYPES = "extensions.getAddons.cache.types";
47 const PREF_GETADDONS_CACHE_ID_ENABLED =
48 "extensions.%ID%.getAddons.cache.enabled";
49 const PREF_GETADDONS_BROWSEADDONS = "extensions.getAddons.browseAddons";
50 const PREF_GETADDONS_BYIDS = "extensions.getAddons.get.url";
51 const PREF_GETADDONS_BROWSESEARCHRESULTS =
52 "extensions.getAddons.search.browseURL";
53 const PREF_GETADDONS_DB_SCHEMA = "extensions.getAddons.databaseSchema";
54 const PREF_GET_LANGPACKS = "extensions.getAddons.langpacks.url";
55 const PREF_GET_BROWSER_MAPPINGS = "extensions.getAddons.browserMappings.url";
57 const PREF_METADATA_LASTUPDATE = "extensions.getAddons.cache.lastUpdate";
58 const PREF_METADATA_UPDATETHRESHOLD_SEC =
59 "extensions.getAddons.cache.updateThreshold";
60 const DEFAULT_METADATA_UPDATETHRESHOLD_SEC = 172800; // two days
62 const DEFAULT_CACHE_TYPES = "extension,theme,locale,dictionary";
64 const FILE_DATABASE = "addons.json";
66 const DB_MIN_JSON_SCHEMA = 5;
67 const DB_BATCH_TIMEOUT_MS = 50;
69 const BLANK_DB = function () {
76 import { Log } from "resource://gre/modules/Log.sys.mjs";
78 const LOGGER_ID = "addons.repository";
80 // Create a new logger for use by the Addons Repository
81 // (Requires AddonManager.jsm)
82 var logger = Log.repository.getLogger(LOGGER_ID);
84 function convertHTMLToPlainText(html) {
89 "@mozilla.org/widget/htmlformatconverter;1"
90 ].createInstance(Ci.nsIFormatConverter);
92 var input = Cc["@mozilla.org/supports-string;1"].createInstance(
95 input.data = html.replace(/\n/g, "<br>");
98 converter.convert("text/html", input, "text/plain", output);
100 if (output.value instanceof Ci.nsISupportsString) {
101 return output.value.data.replace(/\r\n/g, "\n");
106 async function getAddonsToCache(aIds) {
107 let types = Services.prefs.getStringPref(
108 PREF_GETADDONS_CACHE_TYPES,
112 types = types.split(",");
114 let addons = await lazy.AddonManager.getAddonsByIDs(aIds);
117 for (let [i, addon] of addons.entries()) {
118 var preference = PREF_GETADDONS_CACHE_ID_ENABLED.replace("%ID%", aIds[i]);
119 // If the preference doesn't exist caching is enabled by default
120 if (!Services.prefs.getBoolPref(preference, true)) {
124 // The add-ons manager may not know about this ID yet if it is a pending
125 // install. In that case we'll just cache it regardless
127 // Don't cache add-ons of the wrong types
128 if (addon && !types.includes(addon.type)) {
132 // Don't cache system add-ons
133 if (addon && addon.isSystem) {
137 enabledIds.push(aIds[i]);
143 function AddonSearchResult(aId) {
146 this._unsupportedProperties = {};
149 AddonSearchResult.prototype = {
151 * The ID of the add-on
156 * The add-on type (e.g. "extension" or "theme")
161 * The name of the add-on
166 * The version of the add-on
171 * The creator of the add-on
176 * The developers of the add-on
181 * A short description of the add-on
186 * The full description of the add-on
188 fullDescription: null,
191 * The end-user licensing agreement (EULA) of the add-on
196 * The url of the add-on's icon
199 return this.icons && this.icons[32];
203 * The URLs of the add-on's icons, as an object with icon size as key
208 * An array of screenshot urls for the add-on
213 * The homepage for the add-on
218 * The support URL for the add-on
223 * The contribution url of the add-on
225 contributionURL: null,
228 * The rating of the add-on, 0-5
233 * The number of reviews for this add-on
238 * The URL to the list of reviews for this add-on
243 * The number of times the add-on was downloaded the current week
245 weeklyDownloads: null,
248 * The URL to the AMO detail page of this (listed) add-on
253 * AddonInstall object generated from the add-on XPI url
258 * nsIURI storing where this add-on was installed from
263 * The Date that the add-on was most recently updated
270 for (let property of Object.keys(this)) {
271 let value = this[property];
272 if (property.startsWith("_") || typeof value === "function") {
279 json.sourceURI = value ? value.spec : "";
283 json.updateDate = value ? value.getTime() : "";
287 json[property] = value;
290 logger.warn("Error writing property value for " + property);
294 for (let property of Object.keys(this._unsupportedProperties)) {
295 let value = this._unsupportedProperties[property];
296 if (!property.startsWith("_")) {
297 json[property] = value;
306 * The add-on repository is a source of add-ons that can be installed. It can
307 * be searched in three ways. The first takes a list of IDs and returns a
308 * list of the corresponding add-ons. The second returns a list of add-ons that
309 * come highly recommended. This list should change frequently. The third is to
310 * search for specific search terms entered by the user. Searches are
311 * asynchronous and results should be passed to the provided callback object
312 * when complete. The results passed to the callback should only include add-ons
313 * that are compatible with the current application and are not already
316 export var AddonRepository = {
318 * The homepage for visiting this repository. If the corresponding preference
319 * is not defined, defaults to about:blank.
322 let url = this._formatURLPref(PREF_GETADDONS_BROWSEADDONS, {});
323 return url != null ? url : "about:blank";
326 get appIsShuttingDown() {
327 return Services.startup.shuttingDown;
331 * Retrieves the url that can be visited to see search results for the given
332 * terms. If the corresponding preference is not defined, defaults to
335 * @param aSearchTerms
336 * Search terms used to search the repository
338 getSearchURL(aSearchTerms) {
339 let url = this._formatURLPref(PREF_GETADDONS_BROWSESEARCHRESULTS, {
342 return url != null ? url : "about:blank";
346 * Whether caching is currently enabled
349 return lazy.getAddonsCacheEnabled;
353 * Shut down AddonRepository
354 * return: promise{integer} resolves with the result of flushing
355 * the AddonRepository database
358 return AddonDatabase.shutdown(false);
362 let now = Math.round(Date.now() / 1000);
363 let lastUpdate = Services.prefs.getIntPref(PREF_METADATA_LASTUPDATE, 0);
364 return Math.max(0, now - lastUpdate);
368 let threshold = Services.prefs.getIntPref(
369 PREF_METADATA_UPDATETHRESHOLD_SEC,
370 DEFAULT_METADATA_UPDATETHRESHOLD_SEC
372 return this.metadataAge() > threshold;
376 * Asynchronously get a cached add-on by id. The add-on (or null if the
377 * add-on is not found) is passed to the specified callback. If caching is
378 * disabled, null is passed to the specified callback.
380 * The callback variant exists only for existing code in XPIProvider.sys.mjs
381 * and XPIDatabase.sys.mjs that requires a synchronous callback, yuck.
384 * The id of the add-on to get
386 async getCachedAddonByID(aId, aCallback) {
387 if (!aId || !this.cacheEnabled) {
394 if (aCallback && AddonDatabase._loaded) {
395 let addon = AddonDatabase.getAddon(aId);
400 await AddonDatabase.openConnection();
402 let addon = AddonDatabase.getAddon(aId);
410 * Clear and delete the AddonRepository database
411 * @return Promise{null} resolves when the database is deleted
414 return AddonDatabase.delete().then(() =>
415 lazy.AddonManagerPrivate.updateAddonRepositoryData()
420 * Create a ServiceRequest instance.
421 * @return ServiceRequest returns a ServiceRequest instance.
423 _createServiceRequest() {
424 return new lazy.ServiceRequest({ mozAnon: true });
428 * Fetch data from an API where the results may span multiple "pages".
429 * This function will take care of issuing multiple requests until all
430 * the results have been fetched, and will coalesce them all into a
431 * single return value. The handling here is specific to the way AMO
432 * implements paging (ie a JSON result with a "next" property).
434 * @param {string} pref
435 * The pref name that contains the API URL to call.
436 * @param {object} params
437 * A key-value object that contains the parameters to replace
439 * @param {function} handler
440 * This function will be called once per page of results,
441 * it should return an array of objects (the type depends
442 * on the particular API being called of course).
444 * @returns Promise{array} An array of all the individual results from
447 _fetchPaged(pref, params, handler) {
448 const startURL = this._formatURLPref(pref, params);
451 const fetchNextPage = url => {
452 return new Promise((resolve, reject) => {
453 if (this.appIsShuttingDown) {
455 "Rejecting AddonRepository._fetchPaged call, shutdown already in progress"
459 `Reject ServiceRequest for "${url}", shutdown already in progress`
464 let request = this._createServiceRequest();
465 request.mozBackgroundRequest = true;
466 request.open("GET", url, true);
467 request.responseType = "json";
469 request.addEventListener("error", aEvent => {
470 reject(new Error(`GET ${url} failed`));
472 request.addEventListener("timeout", aEvent => {
473 reject(new Error(`GET ${url} timed out`));
475 request.addEventListener("load", aEvent => {
476 let response = request.response;
477 if (!response || (request.status != 200 && request.status != 0)) {
478 reject(new Error(`GET ${url} failed (status ${request.status})`));
483 results.push(...handler(response.results));
489 resolve(fetchNextPage(response.next));
499 return fetchNextPage(startURL);
503 * Fetch metadata for a given set of addons from AMO.
506 * The array of ids to retrieve metadata for.
507 * @returns {array<AddonSearchResult>}
509 async getAddonsByIDs(aIDs) {
510 const idCheck = aIDs.map(id => {
511 if (id.startsWith("rta:")) {
512 return atob(id.split(":")[1]);
517 const addons = await this._fetchPaged(
518 PREF_GETADDONS_BYIDS,
519 { IDS: aIDs.join(",") },
522 .map(entry => this._parseAddon(entry))
523 // Only return the add-ons corresponding the IDs passed to this method.
524 .filter(addon => idCheck.includes(addon.id))
531 * Fetch the Firefox add-ons mapped to the list of extension IDs for the
532 * browser ID passed to this method.
534 * See: https://addons-server.readthedocs.io/en/latest/topics/api/addons.html#browser-mappings
537 * The browser ID used to retrieve the mapping of IDs.
538 * @param extensionIDs
539 * The array of browser (non-Firefox) extension IDs to retrieve
541 * @returns {object} result
542 * The result of the mapping.
543 * @returns {array<AddonSearchResult>} result.addons
544 * The AddonSearchResults for the addons that were successfully mapped.
545 * @returns {array<string>} result.matchedIDs
546 * The IDs of the extensions that were successfully matched to
547 * equivalents that can be installed in this browser. These are
548 * the IDs before matching to equivalents.
549 * @returns {array<string>} result.unmatchedIDs
550 * The IDs of the extensions that were not matched to equivalents.
552 async getMappedAddons(browserID, extensionIDs) {
553 let matchedExtensionIDs = new Set();
554 let unmatchedExtensionIDs = new Set(extensionIDs);
556 const addonIds = await this._fetchPaged(
557 PREF_GET_BROWSER_MAPPINGS,
558 { BROWSER: browserID },
561 // Filter out all the entries with an extension ID not in the list
562 // passed to the method.
564 if (unmatchedExtensionIDs.has(entry.extension_id)) {
565 unmatchedExtensionIDs.delete(entry.extension_id);
566 matchedExtensionIDs.add(entry.extension_id);
571 // Return the add-on ID (stored as `guid` on AMO).
572 .map(entry => entry.addon_guid)
575 if (!addonIds.length) {
579 unmatchedIDs: [...unmatchedExtensionIDs],
584 addons: await this.getAddonsByIDs(addonIds),
585 matchedIDs: [...matchedExtensionIDs],
586 unmatchedIDs: [...unmatchedExtensionIDs],
591 * Asynchronously add add-ons to the cache corresponding to the specified
592 * ids. If caching is disabled, the cache is unchanged.
595 * The array of add-on ids to add to the cache
596 * @returns {array<AddonSearchResult>} Add-ons to add to the cache.
598 async cacheAddons(aIds) {
600 "cacheAddons: enabled " + this.cacheEnabled + " IDs " + aIds.toSource()
602 if (!this.cacheEnabled) {
606 let ids = await getAddonsToCache(aIds);
608 // If there are no add-ons to cache, act as if caching is disabled
615 addons = await this.getAddonsByIDs(ids);
617 logger.error(`Error in addon metadata check: ${err.message}`);
620 await AddonDatabase.update(addons);
626 * Get all installed addons from the AddonManager singleton.
628 * @return Promise{array<AddonWrapper>} Resolves to an array of AddonWrapper instances.
630 _getAllInstalledAddons() {
631 return lazy.AddonManager.getAllAddons();
635 * Performs the periodic background update check.
637 * In Firefox Desktop builds, the background update check is triggered on a
638 * daily basis as part of the AOM background update check and registered
639 * from: `toolkit/mozapps/extensions/extensions.manifest`
641 * In GeckoView builds, add-ons are checked for updates individually. The
642 * `AddonRepository.backgroundUpdateCheck()` method is called by the
643 * `updateWebExtension()` method defined in `GeckoViewWebExtensions.sys.mjs`
644 * but only when `AddonRepository.isMetadataStale()` returns true.
646 * @return Promise{null} Resolves when the metadata update is complete.
648 async backgroundUpdateCheck() {
649 let shutter = (async () => {
650 if (this.appIsShuttingDown) {
652 "Returning earlier from backgroundUpdateCheck, shutdown already in progress"
657 let allAddons = await this._getAllInstalledAddons();
659 // Completely remove cache if caching is not enabled
660 if (!this.cacheEnabled) {
661 logger.debug("Clearing cache because it is disabled");
662 await this._clearCache();
666 let ids = allAddons.map(a => a.id);
667 logger.debug("Repopulate add-on cache with " + ids.toSource());
669 let addonsToCache = await getAddonsToCache(ids);
671 // Completely remove cache if there are no add-ons to cache
672 if (!addonsToCache.length) {
673 logger.debug("Clearing cache because 0 add-ons were requested");
674 await this._clearCache();
680 addons = await this.getAddonsByIDs(addonsToCache);
682 // This is likely to happen if the server is unreachable, e.g. when
683 // there is no network connectivity.
684 logger.error(`Error in addon metadata lookup: ${err.message}`);
685 // Return now to avoid calling repopulate with an empty array;
686 // doing so would clear the cache.
690 AddonDatabase.repopulate(addons);
692 // Always call AddonManager updateAddonRepositoryData after we refill the cache
693 await lazy.AddonManagerPrivate.updateAddonRepositoryData();
695 lazy.AddonManager.beforeShutdown.addBlocker(
696 "AddonRepository Background Updater",
700 lazy.AddonManager.beforeShutdown.removeBlocker(shutter);
704 * Creates an AddonSearchResult by parsing an entry from the AMO API.
707 * An entry from the AMO search API to parse.
708 * @return Result object containing the parsed AddonSearchResult
710 _parseAddon(aEntry) {
711 let addon = new AddonSearchResult(aEntry.guid);
713 addon.name = aEntry.name;
714 if (typeof aEntry.current_version == "object") {
715 addon.version = String(aEntry.current_version.version);
716 if (Array.isArray(aEntry.current_version.files)) {
717 for (let file of aEntry.current_version.files) {
718 if (file.platform == "all" || file.platform == lazy.PLATFORM) {
720 addon.sourceURI = lazy.NetUtil.newURI(file.url);
727 addon.homepageURL = aEntry.homepage;
728 addon.supportURL = aEntry.support_url;
729 addon.amoListingURL = aEntry.url;
731 addon.description = convertHTMLToPlainText(aEntry.summary);
732 addon.fullDescription = convertHTMLToPlainText(aEntry.description);
734 addon.weeklyDownloads = aEntry.weekly_downloads;
736 switch (aEntry.type) {
739 addon.type = "theme";
743 addon.type = "locale";
747 addon.type = aEntry.type;
751 if (Array.isArray(aEntry.authors)) {
752 let authors = aEntry.authors.map(
754 new lazy.AddonManagerPrivate.AddonAuthor(author.name, author.url)
756 if (authors.length) {
757 addon.creator = authors[0];
758 addon.developers = authors.slice(1);
762 if (typeof aEntry.previews == "object") {
763 addon.screenshots = aEntry.previews.map(shot => {
764 let safeSize = orig =>
765 Array.isArray(orig) && orig.length >= 2 ? orig : [null, null];
766 let imageSize = safeSize(shot.image_size);
767 let thumbSize = safeSize(shot.thumbnail_size);
768 return new lazy.AddonManagerPrivate.AddonScreenshot(
780 addon.contributionURL = aEntry.contributions_url;
782 if (typeof aEntry.ratings == "object") {
783 addon.averageRating = Math.min(5, aEntry.ratings.average);
784 addon.reviewCount = aEntry.ratings.text_count;
787 addon.reviewURL = aEntry.ratings_url;
788 if (aEntry.last_updated) {
789 addon.updateDate = new Date(aEntry.last_updated);
792 addon.icons = aEntry.icons || {};
797 // Create url from preference, returning null if preference does not exist
798 _formatURLPref(aPreference, aSubstitutions = {}) {
799 let url = Services.prefs.getCharPref(aPreference, "");
801 logger.warn("_formatURLPref: Couldn't get pref: " + aPreference);
805 url = url.replace(/%([A-Z_]+)%/g, function (aMatch, aKey) {
806 return aKey in aSubstitutions
807 ? encodeURIComponent(aSubstitutions[aKey])
811 return Services.urlFormatter.formatURL(url);
815 return AddonDatabase.flush();
818 async getAvailableLangpacks() {
819 // This should be the API endpoint documented at:
820 // http://addons-server.readthedocs.io/en/latest/topics/api/addons.html#language-tools
821 let url = this._formatURLPref(PREF_GET_LANGPACKS);
823 let response = await fetch(url, { credentials: "omit" });
825 throw new Error("fetching available language packs failed");
828 let data = await response.json();
831 for (let entry of data.results) {
833 !entry.current_compatible_version ||
834 !entry.current_compatible_version.files
839 for (let file of entry.current_compatible_version.files) {
841 file.platform == "all" ||
842 file.platform == Services.appinfo.OS.toLowerCase()
845 target_locale: entry.target_locale,
857 var AddonDatabase = {
858 connectionPromise: null,
861 _blockerAdded: false,
863 // the in-memory database
867 * A getter to retrieve the path to the DB
870 return PathUtils.join(
871 Services.dirsvc.get("ProfD", Ci.nsIFile).path,
877 * Asynchronously opens a new connection to the database file.
879 * @return {Promise} a promise that resolves to the database.
882 if (!this.connectionPromise) {
883 this.connectionPromise = (async () => {
887 let data = await IOUtils.readUTF8(this.jsonFile);
888 inputDB = JSON.parse(data);
891 !inputDB.hasOwnProperty("addons") ||
892 !Array.isArray(inputDB.addons)
894 throw new Error("No addons array.");
897 if (!inputDB.hasOwnProperty("schema")) {
898 throw new Error("No schema specified.");
901 schema = parseInt(inputDB.schema, 10);
903 if (!Number.isInteger(schema) || schema < DB_MIN_JSON_SCHEMA) {
904 throw new Error("Invalid schema value.");
907 if (e.name == "NotFoundError") {
908 logger.debug("No " + FILE_DATABASE + " found.");
911 `Malformed ${FILE_DATABASE}: ${e} - resetting to empty`
915 // Create a blank addons.json file
918 Services.prefs.setIntPref(PREF_GETADDONS_DB_SCHEMA, DB_SCHEMA);
923 Services.prefs.setIntPref(PREF_GETADDONS_DB_SCHEMA, DB_SCHEMA);
925 // Convert the addon objects as necessary
926 // and store them in our in-memory copy of the database.
927 for (let addon of inputDB.addons) {
930 let entry = this._parseAddon(addon);
931 this.DB.addons.set(id, entry);
939 return this.connectionPromise;
943 * Asynchronously shuts down the database connection and releases all
947 * An optional callback to call once complete
949 * An optional boolean to skip flushing data to disk. Useful
950 * when the database is going to be deleted afterwards.
952 shutdown(aSkipFlush) {
953 if (!this.connectionPromise) {
954 return Promise.resolve();
957 this.connectionPromise = null;
958 this._loaded = false;
961 return Promise.resolve();
968 * Asynchronously deletes the database, shutting down the connection
969 * first if initialized
972 * An optional callback to call once complete
973 * @return Promise{null} resolves when the database has been deleted
976 this.DB = BLANK_DB();
978 if (this._saveTask) {
979 this._saveTask.disarm();
980 this._saveTask = null;
983 // shutdown(true) never rejects
984 this._deleting = this.shutdown(true)
985 .then(() => IOUtils.remove(this.jsonFile))
988 "Unable to delete Addon Repository file " + this.jsonFile,
992 .then(() => (this._deleting = null))
995 return this._deleting;
1000 schema: this.DB.schema,
1001 addons: Array.from(this.DB.addons.values()),
1004 await IOUtils.writeUTF8(this.jsonFile, JSON.stringify(json), {
1005 tmpPath: `${this.jsonFile}.tmp`,
1010 if (!this._saveTask) {
1011 this._saveTask = new lazy.DeferredTask(
1012 () => this._saveNow(),
1016 if (!this._blockerAdded) {
1017 lazy.AsyncShutdown.profileBeforeChange.addBlocker(
1018 "Flush AddonRepository",
1021 this._blockerAdded = true;
1024 this._saveTask.arm();
1028 * Flush any pending I/O on the addons.json file
1029 * @return: Promise{null}
1030 * Resolves when the pending I/O (writing out or deleting
1031 * addons.json) completes
1034 if (this._deleting) {
1035 return this._deleting;
1038 if (this._saveTask) {
1039 let promise = this._saveTask.finalize();
1040 this._saveTask = null;
1044 return Promise.resolve();
1048 * Get an individual addon entry from the in-memory cache.
1049 * Note: calling this function before the database is read will
1052 * @param {string} aId The id of the addon to retrieve.
1055 return this.DB.addons.get(aId);
1059 * Asynchronously repopulates the database so it only contains the
1062 * @param {array<AddonSearchResult>} aAddons
1063 * Add-ons to repopulate the database with.
1065 repopulate(aAddons) {
1066 this.DB = BLANK_DB();
1067 this._update(aAddons);
1069 let now = Math.round(Date.now() / 1000);
1071 "Cache repopulated, setting " + PREF_METADATA_LASTUPDATE + " to " + now
1073 Services.prefs.setIntPref(PREF_METADATA_LASTUPDATE, now);
1077 * Asynchronously insert new addons into the database.
1079 * @param {array<AddonSearchResult>} aAddons
1080 * Add-ons to insert/update in the database
1082 async update(aAddons) {
1083 await this.openConnection();
1085 this._update(aAddons);
1089 * Merge the given addons into the database.
1091 * @param {array<AddonSearchResult>} aAddons
1092 * Add-ons to insert/update in the database
1095 for (let addon of aAddons) {
1096 this.DB.addons.set(addon.id, this._parseAddon(addon));
1103 * Creates an AddonSearchResult by parsing an object structure
1104 * retrieved from the DB JSON representation.
1107 * The object to parse
1108 * @return Returns an AddonSearchResult object.
1111 if (aObj instanceof AddonSearchResult) {
1120 let addon = new AddonSearchResult(id);
1122 for (let expectedProperty of Object.keys(AddonSearchResult.prototype)) {
1124 !(expectedProperty in aObj) ||
1125 typeof aObj[expectedProperty] === "function"
1130 let value = aObj[expectedProperty];
1133 switch (expectedProperty) {
1135 addon.sourceURI = value ? lazy.NetUtil.newURI(value) : null;
1139 addon.creator = value ? this._makeDeveloper(value) : null;
1143 addon.updateDate = value ? new Date(value) : null;
1147 if (!addon.developers) {
1148 addon.developers = [];
1150 for (let developer of value) {
1151 addon.developers.push(this._makeDeveloper(developer));
1156 if (!addon.screenshots) {
1157 addon.screenshots = [];
1159 for (let screenshot of value) {
1160 addon.screenshots.push(this._makeScreenshot(screenshot));
1168 for (let size of Object.keys(aObj.icons)) {
1169 addon.icons[size] = aObj.icons[size];
1177 addon[expectedProperty] = value;
1181 "Error in parsing property value for " + expectedProperty + " | " + ex
1185 // delete property from obj to indicate we've already
1186 // handled it. The remaining public properties will
1187 // be stored separately and just passed through to
1188 // be written back to the DB.
1189 delete aObj[expectedProperty];
1192 // Copy remaining properties to a separate object
1193 // to prevent accidental access on downgraded versions.
1194 // The properties will be merged in the same object
1195 // prior to being written back through toJSON.
1196 for (let remainingProperty of Object.keys(aObj)) {
1197 switch (typeof aObj[remainingProperty]) {
1202 // these types are accepted
1208 if (!remainingProperty.startsWith("_")) {
1209 addon._unsupportedProperties[remainingProperty] =
1210 aObj[remainingProperty];
1218 * Make a developer object from a vanilla
1219 * JS object from the JSON database
1222 * The JS object to use
1223 * @return The created developer
1225 _makeDeveloper(aObj) {
1226 let name = aObj.name;
1228 return new lazy.AddonManagerPrivate.AddonAuthor(name, url);
1232 * Make a screenshot object from a vanilla
1233 * JS object from the JSON database
1236 * The JS object to use
1237 * @return The created screenshot
1239 _makeScreenshot(aObj) {
1241 let width = aObj.width;
1242 let height = aObj.height;
1243 let thumbnailURL = aObj.thumbnailURL;
1244 let thumbnailWidth = aObj.thumbnailWidth;
1245 let thumbnailHeight = aObj.thumbnailHeight;
1246 let caption = aObj.caption;
1247 return new lazy.AddonManagerPrivate.AddonScreenshot(