summaryrefslogtreecommitdiffstats
path: root/toolkit/mozapps/extensions/internal
diff options
context:
space:
mode:
authorMatt A. Tobin <email@mattatobin.com>2018-02-11 07:03:16 -0500
committerMatt A. Tobin <email@mattatobin.com>2018-02-11 07:03:16 -0500
commit203eb0f61a09372310a2a8fb57e169cb3f47800b (patch)
tree8490329d3dae4de3c7ffd127bce1f65fdc009abd /toolkit/mozapps/extensions/internal
parente45706ca3acbb6530419433212becc61d5953a2d (diff)
parent8f6d3dab81c7f8f97ef197e26ab9439b09735b8f (diff)
downloadUXP-203eb0f61a09372310a2a8fb57e169cb3f47800b.tar
UXP-203eb0f61a09372310a2a8fb57e169cb3f47800b.tar.gz
UXP-203eb0f61a09372310a2a8fb57e169cb3f47800b.tar.lz
UXP-203eb0f61a09372310a2a8fb57e169cb3f47800b.tar.xz
UXP-203eb0f61a09372310a2a8fb57e169cb3f47800b.zip
Merge branch 'ext-work'FF_Checkpoint_1
Diffstat (limited to 'toolkit/mozapps/extensions/internal')
-rw-r--r--toolkit/mozapps/extensions/internal/AddonLogging.jsm180
-rw-r--r--toolkit/mozapps/extensions/internal/AddonRepository.jsm2031
-rw-r--r--toolkit/mozapps/extensions/internal/AddonRepository_SQLiteMigrator.jsm518
-rw-r--r--toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm772
-rw-r--r--toolkit/mozapps/extensions/internal/Content.js31
-rw-r--r--toolkit/mozapps/extensions/internal/GMPProvider.jsm614
-rw-r--r--toolkit/mozapps/extensions/internal/LightweightThemeImageOptimizer.jsm198
-rw-r--r--toolkit/mozapps/extensions/internal/PluginProvider.jsm595
-rw-r--r--toolkit/mozapps/extensions/internal/SpellCheckDictionaryBootstrap.js17
-rw-r--r--toolkit/mozapps/extensions/internal/XPIProvider.jsm7875
-rw-r--r--toolkit/mozapps/extensions/internal/XPIProviderUtils.js1481
-rw-r--r--toolkit/mozapps/extensions/internal/moz.build40
12 files changed, 14352 insertions, 0 deletions
diff --git a/toolkit/mozapps/extensions/internal/AddonLogging.jsm b/toolkit/mozapps/extensions/internal/AddonLogging.jsm
new file mode 100644
index 000000000..362439bae
--- /dev/null
+++ b/toolkit/mozapps/extensions/internal/AddonLogging.jsm
@@ -0,0 +1,180 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+
+const KEY_PROFILEDIR = "ProfD";
+const FILE_EXTENSIONS_LOG = "extensions.log";
+const PREF_LOGGING_ENABLED = "extensions.logging.enabled";
+
+const LOGGER_FILE_PERM = parseInt("666", 8);
+
+const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed";
+
+Components.utils.import("resource://gre/modules/FileUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+this.EXPORTED_SYMBOLS = [ "LogManager" ];
+
+var gDebugLogEnabled = false;
+
+function formatLogMessage(aType, aName, aStr, aException) {
+ let message = aType.toUpperCase() + " " + aName + ": " + aStr;
+ if (aException) {
+ if (typeof aException == "number")
+ return message + ": " + Components.Exception("", aException).name;
+
+ message = message + ": " + aException;
+ // instanceOf doesn't work here, let's duck type
+ if (aException.fileName)
+ message = message + " (" + aException.fileName + ":" + aException.lineNumber + ")";
+
+ if (aException.message == "too much recursion")
+ dump(message + "\n" + aException.stack + "\n");
+ }
+ return message;
+}
+
+function getStackDetails(aException) {
+ // Defensively wrap all this to ensure that failing to get the message source
+ // doesn't stop the message from being logged
+ try {
+ if (aException) {
+ if (aException instanceof Ci.nsIException) {
+ return {
+ sourceName: aException.filename,
+ lineNumber: aException.lineNumber
+ };
+ }
+
+ if (typeof aException == "object") {
+ return {
+ sourceName: aException.fileName,
+ lineNumber: aException.lineNumber
+ };
+ }
+ }
+
+ let stackFrame = Components.stack.caller.caller.caller;
+ return {
+ sourceName: stackFrame.filename,
+ lineNumber: stackFrame.lineNumber
+ };
+ }
+ catch (e) {
+ return {
+ sourceName: null,
+ lineNumber: 0
+ };
+ }
+}
+
+function AddonLogger(aName) {
+ this.name = aName;
+}
+
+AddonLogger.prototype = {
+ name: null,
+
+ error: function AddonLogger_error(aStr, aException) {
+ let message = formatLogMessage("error", this.name, aStr, aException);
+
+ let stack = getStackDetails(aException);
+
+ let consoleMessage = Cc["@mozilla.org/scripterror;1"].
+ createInstance(Ci.nsIScriptError);
+ consoleMessage.init(message, stack.sourceName, null, stack.lineNumber, 0,
+ Ci.nsIScriptError.errorFlag, "component javascript");
+ Services.console.logMessage(consoleMessage);
+
+ // Always dump errors, in case the Console Service isn't listening yet
+ dump("*** " + message + "\n");
+
+ try {
+ var tstamp = new Date();
+ var logfile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_EXTENSIONS_LOG]);
+ var stream = Cc["@mozilla.org/network/file-output-stream;1"].
+ createInstance(Ci.nsIFileOutputStream);
+ stream.init(logfile, 0x02 | 0x08 | 0x10, LOGGER_FILE_PERM, 0); // write, create, append
+ var writer = Cc["@mozilla.org/intl/converter-output-stream;1"].
+ createInstance(Ci.nsIConverterOutputStream);
+ writer.init(stream, "UTF-8", 0, 0x0000);
+ writer.writeString(tstamp.toLocaleFormat("%Y-%m-%d %H:%M:%S ") +
+ message + " at " + stack.sourceName + ":" +
+ stack.lineNumber + "\n");
+ writer.close();
+ }
+ catch (e) { }
+ },
+
+ warn: function AddonLogger_warn(aStr, aException) {
+ let message = formatLogMessage("warn", this.name, aStr, aException);
+
+ let stack = getStackDetails(aException);
+
+ let consoleMessage = Cc["@mozilla.org/scripterror;1"].
+ createInstance(Ci.nsIScriptError);
+ consoleMessage.init(message, stack.sourceName, null, stack.lineNumber, 0,
+ Ci.nsIScriptError.warningFlag, "component javascript");
+ Services.console.logMessage(consoleMessage);
+
+ if (gDebugLogEnabled)
+ dump("*** " + message + "\n");
+ },
+
+ log: function AddonLogger_log(aStr, aException) {
+ if (gDebugLogEnabled) {
+ let message = formatLogMessage("log", this.name, aStr, aException);
+ dump("*** " + message + "\n");
+ Services.console.logStringMessage(message);
+ }
+ }
+};
+
+this.LogManager = {
+ getLogger: function LogManager_getLogger(aName, aTarget) {
+ let logger = new AddonLogger(aName);
+
+ if (aTarget) {
+ ["error", "warn", "log"].forEach(function(name) {
+ let fname = name.toUpperCase();
+ delete aTarget[fname];
+ aTarget[fname] = function LogManager_targetName(aStr, aException) {
+ logger[name](aStr, aException);
+ };
+ });
+ }
+
+ return logger;
+ }
+};
+
+var PrefObserver = {
+ init: function PrefObserver_init() {
+ Services.prefs.addObserver(PREF_LOGGING_ENABLED, this, false);
+ Services.obs.addObserver(this, "xpcom-shutdown", false);
+ this.observe(null, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID, PREF_LOGGING_ENABLED);
+ },
+
+ observe: function PrefObserver_observe(aSubject, aTopic, aData) {
+ if (aTopic == "xpcom-shutdown") {
+ Services.prefs.removeObserver(PREF_LOGGING_ENABLED, this);
+ Services.obs.removeObserver(this, "xpcom-shutdown");
+ }
+ else if (aTopic == NS_PREFBRANCH_PREFCHANGE_TOPIC_ID) {
+ try {
+ gDebugLogEnabled = Services.prefs.getBoolPref(PREF_LOGGING_ENABLED);
+ }
+ catch (e) {
+ gDebugLogEnabled = false;
+ }
+ }
+ }
+};
+
+PrefObserver.init();
diff --git a/toolkit/mozapps/extensions/internal/AddonRepository.jsm b/toolkit/mozapps/extensions/internal/AddonRepository.jsm
new file mode 100644
index 000000000..adcecbee7
--- /dev/null
+++ b/toolkit/mozapps/extensions/internal/AddonRepository.jsm
@@ -0,0 +1,2031 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/AddonManager.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DeferredSave",
+ "resource://gre/modules/DeferredSave.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository_SQLiteMigrator",
+ "resource://gre/modules/addons/AddonRepository_SQLiteMigrator.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+
+
+this.EXPORTED_SYMBOLS = [ "AddonRepository" ];
+
+const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled";
+const PREF_GETADDONS_CACHE_TYPES = "extensions.getAddons.cache.types";
+const PREF_GETADDONS_CACHE_ID_ENABLED = "extensions.%ID%.getAddons.cache.enabled"
+const PREF_GETADDONS_BROWSEADDONS = "extensions.getAddons.browseAddons";
+const PREF_GETADDONS_BYIDS = "extensions.getAddons.get.url";
+const PREF_GETADDONS_BYIDS_PERFORMANCE = "extensions.getAddons.getWithPerformance.url";
+const PREF_GETADDONS_BROWSERECOMMENDED = "extensions.getAddons.recommended.browseURL";
+const PREF_GETADDONS_GETRECOMMENDED = "extensions.getAddons.recommended.url";
+const PREF_GETADDONS_BROWSESEARCHRESULTS = "extensions.getAddons.search.browseURL";
+const PREF_GETADDONS_GETSEARCHRESULTS = "extensions.getAddons.search.url";
+const PREF_GETADDONS_DB_SCHEMA = "extensions.getAddons.databaseSchema"
+
+const PREF_METADATA_LASTUPDATE = "extensions.getAddons.cache.lastUpdate";
+const PREF_METADATA_UPDATETHRESHOLD_SEC = "extensions.getAddons.cache.updateThreshold";
+const DEFAULT_METADATA_UPDATETHRESHOLD_SEC = 172800; // two days
+
+const XMLURI_PARSE_ERROR = "http://www.mozilla.org/newlayout/xml/parsererror.xml";
+
+const API_VERSION = "1.5";
+const DEFAULT_CACHE_TYPES = "extension,theme,locale,dictionary";
+
+const KEY_PROFILEDIR = "ProfD";
+const FILE_DATABASE = "addons.json";
+const DB_SCHEMA = 5;
+const DB_MIN_JSON_SCHEMA = 5;
+const DB_BATCH_TIMEOUT_MS = 50;
+
+const BLANK_DB = function() {
+ return {
+ addons: new Map(),
+ schema: DB_SCHEMA
+ };
+}
+
+const TOOLKIT_ID = "toolkit@mozilla.org";
+#ifdef MOZ_PHOENIX_EXTENSIONS
+const FIREFOX_ID = "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}";
+#endif
+Cu.import("resource://gre/modules/Log.jsm");
+const LOGGER_ID = "addons.repository";
+
+// Create a new logger for use by the Addons Repository
+// (Requires AddonManager.jsm)
+let logger = Log.repository.getLogger(LOGGER_ID);
+
+// A map between XML keys to AddonSearchResult keys for string values
+// that require no extra parsing from XML
+const STRING_KEY_MAP = {
+ name: "name",
+ version: "version",
+ homepage: "homepageURL",
+ support: "supportURL"
+};
+
+// A map between XML keys to AddonSearchResult keys for string values
+// that require parsing from HTML
+const HTML_KEY_MAP = {
+ summary: "description",
+ description: "fullDescription",
+ developer_comments: "developerComments",
+ eula: "eula"
+};
+
+// A map between XML keys to AddonSearchResult keys for integer values
+// that require no extra parsing from XML
+const INTEGER_KEY_MAP = {
+ total_downloads: "totalDownloads",
+ weekly_downloads: "weeklyDownloads",
+ daily_users: "dailyUsers"
+};
+
+// Wrap the XHR factory so that tests can override with a mock
+let XHRequest = Components.Constructor("@mozilla.org/xmlextras/xmlhttprequest;1",
+ "nsIXMLHttpRequest");
+
+function convertHTMLToPlainText(html) {
+ if (!html)
+ return html;
+ var converter = Cc["@mozilla.org/widget/htmlformatconverter;1"].
+ createInstance(Ci.nsIFormatConverter);
+
+ var input = Cc["@mozilla.org/supports-string;1"].
+ createInstance(Ci.nsISupportsString);
+ input.data = html.replace(/\n/g, "<br>");
+
+ var output = {};
+ converter.convert("text/html", input, input.data.length, "text/unicode",
+ output, {});
+
+ if (output.value instanceof Ci.nsISupportsString)
+ return output.value.data.replace(/\r\n/g, "\n");
+ return html;
+}
+
+function getAddonsToCache(aIds, aCallback) {
+ try {
+ var types = Services.prefs.getCharPref(PREF_GETADDONS_CACHE_TYPES);
+ }
+ catch (e) { }
+ if (!types)
+ types = DEFAULT_CACHE_TYPES;
+
+ types = types.split(",");
+
+ AddonManager.getAddonsByIDs(aIds, function getAddonsToCache_getAddonsByIDs(aAddons) {
+ let enabledIds = [];
+ for (var i = 0; i < aIds.length; i++) {
+ var preference = PREF_GETADDONS_CACHE_ID_ENABLED.replace("%ID%", aIds[i]);
+ try {
+ if (!Services.prefs.getBoolPref(preference))
+ continue;
+ } catch(e) {
+ // If the preference doesn't exist caching is enabled by default
+ }
+
+ // The add-ons manager may not know about this ID yet if it is a pending
+ // install. In that case we'll just cache it regardless
+ if (aAddons[i] && (types.indexOf(aAddons[i].type) == -1))
+ continue;
+
+ enabledIds.push(aIds[i]);
+ }
+
+ aCallback(enabledIds);
+ });
+}
+
+function AddonSearchResult(aId) {
+ this.id = aId;
+ this.icons = {};
+ this._unsupportedProperties = {};
+}
+
+AddonSearchResult.prototype = {
+ /**
+ * The ID of the add-on
+ */
+ id: null,
+
+ /**
+ * The add-on type (e.g. "extension" or "theme")
+ */
+ type: null,
+
+ /**
+ * The name of the add-on
+ */
+ name: null,
+
+ /**
+ * The version of the add-on
+ */
+ version: null,
+
+ /**
+ * The creator of the add-on
+ */
+ creator: null,
+
+ /**
+ * The developers of the add-on
+ */
+ developers: null,
+
+ /**
+ * A short description of the add-on
+ */
+ description: null,
+
+ /**
+ * The full description of the add-on
+ */
+ fullDescription: null,
+
+ /**
+ * The developer comments for the add-on. This includes any information
+ * that may be helpful to end users that isn't necessarily applicable to
+ * the add-on description (e.g. known major bugs)
+ */
+ developerComments: null,
+
+ /**
+ * The end-user licensing agreement (EULA) of the add-on
+ */
+ eula: null,
+
+ /**
+ * The url of the add-on's icon
+ */
+ get iconURL() {
+ return this.icons && this.icons[32];
+ },
+
+ /**
+ * The URLs of the add-on's icons, as an object with icon size as key
+ */
+ icons: null,
+
+ /**
+ * An array of screenshot urls for the add-on
+ */
+ screenshots: null,
+
+ /**
+ * The homepage for the add-on
+ */
+ homepageURL: null,
+
+ /**
+ * The homepage for the add-on
+ */
+ learnmoreURL: null,
+
+ /**
+ * The support URL for the add-on
+ */
+ supportURL: null,
+
+ /**
+ * The contribution url of the add-on
+ */
+ contributionURL: null,
+
+ /**
+ * The suggested contribution amount
+ */
+ contributionAmount: null,
+
+ /**
+ * The URL to visit in order to purchase the add-on
+ */
+ purchaseURL: null,
+
+ /**
+ * The numerical cost of the add-on in some currency, for sorting purposes
+ * only
+ */
+ purchaseAmount: null,
+
+ /**
+ * The display cost of the add-on, for display purposes only
+ */
+ purchaseDisplayAmount: null,
+
+ /**
+ * The rating of the add-on, 0-5
+ */
+ averageRating: null,
+
+ /**
+ * The number of reviews for this add-on
+ */
+ reviewCount: null,
+
+ /**
+ * The URL to the list of reviews for this add-on
+ */
+ reviewURL: null,
+
+ /**
+ * The total number of times the add-on was downloaded
+ */
+ totalDownloads: null,
+
+ /**
+ * The number of times the add-on was downloaded the current week
+ */
+ weeklyDownloads: null,
+
+ /**
+ * The number of daily users for the add-on
+ */
+ dailyUsers: null,
+
+ /**
+ * AddonInstall object generated from the add-on XPI url
+ */
+ install: null,
+
+ /**
+ * nsIURI storing where this add-on was installed from
+ */
+ sourceURI: null,
+
+ /**
+ * The status of the add-on in the repository (e.g. 4 = "Public")
+ */
+ repositoryStatus: null,
+
+ /**
+ * The size of the add-on's files in bytes. For an add-on that have not yet
+ * been downloaded this may be an estimated value.
+ */
+ size: null,
+
+ /**
+ * The Date that the add-on was most recently updated
+ */
+ updateDate: null,
+
+ /**
+ * True or false depending on whether the add-on is compatible with the
+ * current version of the application
+ */
+ isCompatible: true,
+
+ /**
+ * True or false depending on whether the add-on is compatible with the
+ * current platform
+ */
+ isPlatformCompatible: true,
+
+ /**
+ * Array of AddonCompatibilityOverride objects, that describe overrides for
+ * compatibility with an application versions.
+ **/
+ compatibilityOverrides: null,
+
+ /**
+ * True if the add-on has a secure means of updating
+ */
+ providesUpdatesSecurely: true,
+
+ /**
+ * The current blocklist state of the add-on
+ */
+ blocklistState: Ci.nsIBlocklistService.STATE_NOT_BLOCKED,
+
+ /**
+ * True if this add-on cannot be used in the application based on version
+ * compatibility, dependencies and blocklisting
+ */
+ appDisabled: false,
+
+ /**
+ * True if the user wants this add-on to be disabled
+ */
+ userDisabled: false,
+
+ /**
+ * Indicates what scope the add-on is installed in, per profile, user,
+ * system or application
+ */
+ scope: AddonManager.SCOPE_PROFILE,
+
+ /**
+ * True if the add-on is currently functional
+ */
+ isActive: true,
+
+ /**
+ * A bitfield holding all of the current operations that are waiting to be
+ * performed for this add-on
+ */
+ pendingOperations: AddonManager.PENDING_NONE,
+
+ /**
+ * A bitfield holding all the the operations that can be performed on
+ * this add-on
+ */
+ permissions: 0,
+
+ /**
+ * Tests whether this add-on is known to be compatible with a
+ * particular application and platform version.
+ *
+ * @param appVersion
+ * An application version to test against
+ * @param platformVersion
+ * A platform version to test against
+ * @return Boolean representing if the add-on is compatible
+ */
+ isCompatibleWith: function ASR_isCompatibleWith(aAppVerison, aPlatformVersion) {
+ return true;
+ },
+
+ /**
+ * Starts an update check for this add-on. This will perform
+ * asynchronously and deliver results to the given listener.
+ *
+ * @param aListener
+ * An UpdateListener for the update process
+ * @param aReason
+ * A reason code for performing the update
+ * @param aAppVersion
+ * An application version to check for updates for
+ * @param aPlatformVersion
+ * A platform version to check for updates for
+ */
+ findUpdates: function ASR_findUpdates(aListener, aReason, aAppVersion, aPlatformVersion) {
+ if ("onNoCompatibilityUpdateAvailable" in aListener)
+ aListener.onNoCompatibilityUpdateAvailable(this);
+ if ("onNoUpdateAvailable" in aListener)
+ aListener.onNoUpdateAvailable(this);
+ if ("onUpdateFinished" in aListener)
+ aListener.onUpdateFinished(this);
+ },
+
+ toJSON: function() {
+ let json = {};
+
+ for (let [property, value] of Iterator(this)) {
+ if (property.startsWith("_") ||
+ typeof(value) === "function")
+ continue;
+
+ try {
+ switch (property) {
+ case "sourceURI":
+ json.sourceURI = value ? value.spec : "";
+ break;
+
+ case "updateDate":
+ json.updateDate = value ? value.getTime() : "";
+ break;
+
+ default:
+ json[property] = value;
+ }
+ } catch (ex) {
+ logger.warn("Error writing property value for " + property);
+ }
+ }
+
+ for (let [property, value] of Iterator(this._unsupportedProperties)) {
+ if (!property.startsWith("_"))
+ json[property] = value;
+ }
+
+ return json;
+ }
+}
+
+/**
+ * The add-on repository is a source of add-ons that can be installed. It can
+ * be searched in three ways. The first takes a list of IDs and returns a
+ * list of the corresponding add-ons. The second returns a list of add-ons that
+ * come highly recommended. This list should change frequently. The third is to
+ * search for specific search terms entered by the user. Searches are
+ * asynchronous and results should be passed to the provided callback object
+ * when complete. The results passed to the callback should only include add-ons
+ * that are compatible with the current application and are not already
+ * installed.
+ */
+this.AddonRepository = {
+ /**
+ * Whether caching is currently enabled
+ */
+ get cacheEnabled() {
+ // Act as though caching is disabled if there was an unrecoverable error
+ // openning the database.
+ if (!AddonDatabase.databaseOk) {
+ logger.warn("Cache is disabled because database is not OK");
+ return false;
+ }
+
+ let preference = PREF_GETADDONS_CACHE_ENABLED;
+ let enabled = false;
+ try {
+ enabled = Services.prefs.getBoolPref(preference);
+ } catch(e) {
+ logger.warn("cacheEnabled: Couldn't get pref: " + preference);
+ }
+
+ return enabled;
+ },
+
+ // A cache of the add-ons stored in the database
+ _addons: null,
+
+ // Whether a search is currently in progress
+ _searching: false,
+
+ // XHR associated with the current request
+ _request: null,
+
+ /*
+ * Addon search results callback object that contains two functions
+ *
+ * searchSucceeded - Called when a search has suceeded.
+ *
+ * @param aAddons
+ * An array of the add-on results. In the case of searching for
+ * specific terms the ordering of results may be determined by
+ * the search provider.
+ * @param aAddonCount
+ * The length of aAddons
+ * @param aTotalResults
+ * The total results actually available in the repository
+ *
+ *
+ * searchFailed - Called when an error occurred when performing a search.
+ */
+ _callback: null,
+
+ // Maximum number of results to return
+ _maxResults: null,
+
+ /**
+ * Shut down AddonRepository
+ * return: promise{integer} resolves with the result of flushing
+ * the AddonRepository database
+ */
+ shutdown: function AddonRepo_shutdown() {
+ this.cancelSearch();
+
+ this._addons = null;
+ return AddonDatabase.shutdown(false);
+ },
+
+ metadataAge: function() {
+ let now = Math.round(Date.now() / 1000);
+
+ let lastUpdate = 0;
+ try {
+ lastUpdate = Services.prefs.getIntPref(PREF_METADATA_LASTUPDATE);
+ } catch (e) {}
+
+ // Handle clock jumps
+ if (now < lastUpdate) {
+ return now;
+ }
+ return now - lastUpdate;
+ },
+
+ isMetadataStale: function AddonRepo_isMetadataStale() {
+ let threshold = DEFAULT_METADATA_UPDATETHRESHOLD_SEC;
+ try {
+ threshold = Services.prefs.getIntPref(PREF_METADATA_UPDATETHRESHOLD_SEC);
+ } catch (e) {}
+ return (this.metadataAge() > threshold);
+ },
+
+ /**
+ * Asynchronously get a cached add-on by id. The add-on (or null if the
+ * add-on is not found) is passed to the specified callback. If caching is
+ * disabled, null is passed to the specified callback.
+ *
+ * @param aId
+ * The id of the add-on to get
+ * @param aCallback
+ * The callback to pass the result back to
+ */
+ getCachedAddonByID: Task.async(function* (aId, aCallback) {
+ if (!aId || !this.cacheEnabled) {
+ aCallback(null);
+ return;
+ }
+
+ function getAddon(aAddons) {
+ aCallback(aAddons.get(aId) || null);
+ }
+
+ if (this._addons == null) {
+ AddonDatabase.retrieveStoredData().then(aAddons => {
+ this._addons = aAddons;
+ getAddon(aAddons);
+ });
+
+ return;
+ }
+
+ getAddon(this._addons);
+ }),
+
+ /**
+ * Asynchronously repopulate cache so it only contains the add-ons
+ * corresponding to the specified ids. If caching is disabled,
+ * the cache is completely removed.
+ *
+ * @param aTimeout
+ * (Optional) timeout in milliseconds to abandon the XHR request
+ * if we have not received a response from the server.
+ * @return Promise{null}
+ * Resolves when the metadata ping is complete
+ */
+ repopulateCache: function(aTimeout) {
+ return this._repopulateCacheInternal(false, aTimeout);
+ },
+
+ /*
+ * Clear and delete the AddonRepository database
+ * @return Promise{null} resolves when the database is deleted
+ */
+ _clearCache: function () {
+ this._addons = null;
+ return AddonDatabase.delete().then(() =>
+ new Promise((resolve, reject) =>
+ AddonManagerPrivate.updateAddonRepositoryData(resolve))
+ );
+ },
+
+ _repopulateCacheInternal: Task.async(function* (aSendPerformance, aTimeout) {
+ let allAddons = yield new Promise((resolve, reject) =>
+ AddonManager.getAllAddons(resolve));
+
+ // Completely remove cache if caching is not enabled
+ if (!this.cacheEnabled) {
+ logger.debug("Clearing cache because it is disabled");
+ return this._clearCache();
+ }
+
+ // Tycho: let ids = [a.id for (a of allAddons)];
+ let ids = [];
+ for (let a of allAddons) {
+ ids.push(a.id);
+ }
+
+ logger.debug("Repopulate add-on cache with " + ids.toSource());
+
+ let self = this;
+ let addonsToCache = yield new Promise((resolve, reject) =>
+ getAddonsToCache(ids, resolve));
+
+ // Completely remove cache if there are no add-ons to cache
+ if (addonsToCache.length == 0) {
+ logger.debug("Clearing cache because 0 add-ons were requested");
+ return this._clearCache();
+ }
+
+ yield new Promise((resolve, reject) =>
+ self._beginGetAddons(addonsToCache, {
+ searchSucceeded: function repopulateCacheInternal_searchSucceeded(aAddons) {
+ self._addons = new Map();
+ for (let addon of aAddons) {
+ self._addons.set(addon.id, addon);
+ }
+ AddonDatabase.repopulate(aAddons, resolve);
+ },
+ searchFailed: function repopulateCacheInternal_searchFailed() {
+ logger.warn("Search failed when repopulating cache");
+ resolve();
+ }
+ }, aSendPerformance, aTimeout));
+
+ // Always call AddonManager updateAddonRepositoryData after we refill the cache
+ yield new Promise((resolve, reject) =>
+ AddonManagerPrivate.updateAddonRepositoryData(resolve));
+ }),
+
+ /**
+ * Asynchronously add add-ons to the cache corresponding to the specified
+ * ids. If caching is disabled, the cache is unchanged and the callback is
+ * immediately called if it is defined.
+ *
+ * @param aIds
+ * The array of add-on ids to add to the cache
+ * @param aCallback
+ * The optional callback to call once complete
+ */
+ cacheAddons: function AddonRepo_cacheAddons(aIds, aCallback) {
+ logger.debug("cacheAddons: enabled " + this.cacheEnabled + " IDs " + aIds.toSource());
+ if (!this.cacheEnabled) {
+ if (aCallback)
+ aCallback();
+ return;
+ }
+
+ let self = this;
+ getAddonsToCache(aIds, function cacheAddons_getAddonsToCache(aAddons) {
+ // If there are no add-ons to cache, act as if caching is disabled
+ if (aAddons.length == 0) {
+ if (aCallback)
+ aCallback();
+ return;
+ }
+
+ self.getAddonsByIDs(aAddons, {
+ searchSucceeded: function cacheAddons_searchSucceeded(aAddons) {
+ for (let addon of aAddons) {
+ self._addons.set(addon.id, addon);
+ }
+ AddonDatabase.insertAddons(aAddons, aCallback);
+ },
+ searchFailed: function cacheAddons_searchFailed() {
+ logger.warn("Search failed when adding add-ons to cache");
+ if (aCallback)
+ aCallback();
+ }
+ });
+ });
+ },
+
+ /**
+ * The homepage for visiting this repository. If the corresponding preference
+ * is not defined, defaults to about:blank.
+ */
+ get homepageURL() {
+ let url = this._formatURLPref(PREF_GETADDONS_BROWSEADDONS, {});
+ return (url != null) ? url : "about:blank";
+ },
+
+ /**
+ * Returns whether this instance is currently performing a search. New
+ * searches will not be performed while this is the case.
+ */
+ get isSearching() {
+ return this._searching;
+ },
+
+ /**
+ * The url that can be visited to see recommended add-ons in this repository.
+ * If the corresponding preference is not defined, defaults to about:blank.
+ */
+ getRecommendedURL: function AddonRepo_getRecommendedURL() {
+ let url = this._formatURLPref(PREF_GETADDONS_BROWSERECOMMENDED, {});
+ return (url != null) ? url : "about:blank";
+ },
+
+ /**
+ * Retrieves the url that can be visited to see search results for the given
+ * terms. If the corresponding preference is not defined, defaults to
+ * about:blank.
+ *
+ * @param aSearchTerms
+ * Search terms used to search the repository
+ */
+ getSearchURL: function AddonRepo_getSearchURL(aSearchTerms) {
+ let url = this._formatURLPref(PREF_GETADDONS_BROWSESEARCHRESULTS, {
+ TERMS : encodeURIComponent(aSearchTerms)
+ });
+ return (url != null) ? url : "about:blank";
+ },
+
+ /**
+ * Cancels the search in progress. If there is no search in progress this
+ * does nothing.
+ */
+ cancelSearch: function AddonRepo_cancelSearch() {
+ this._searching = false;
+ if (this._request) {
+ this._request.abort();
+ this._request = null;
+ }
+ this._callback = null;
+ },
+
+ /**
+ * Begins a search for add-ons in this repository by ID. Results will be
+ * passed to the given callback.
+ *
+ * @param aIDs
+ * The array of ids to search for
+ * @param aCallback
+ * The callback to pass results to
+ */
+ getAddonsByIDs: function AddonRepo_getAddonsByIDs(aIDs, aCallback) {
+ return this._beginGetAddons(aIDs, aCallback, false);
+ },
+
+ /**
+ * Begins a search of add-ons, potentially sending performance data.
+ *
+ * @param aIDs
+ * Array of ids to search for.
+ * @param aCallback
+ * Function to pass results to.
+ * @param aSendPerformance
+ * Boolean indicating whether to send performance data with the
+ * request.
+ * @param aTimeout
+ * (Optional) timeout in milliseconds to abandon the XHR request
+ * if we have not received a response from the server.
+ */
+ _beginGetAddons: function(aIDs, aCallback, aSendPerformance, aTimeout) {
+ let ids = aIDs.slice(0);
+
+ let params = {
+ API_VERSION : API_VERSION,
+ IDS : ids.map(encodeURIComponent).join(',')
+ };
+
+ let pref = PREF_GETADDONS_BYIDS;
+
+ if (aSendPerformance) {
+ let type = Services.prefs.getPrefType(PREF_GETADDONS_BYIDS_PERFORMANCE);
+ if (type == Services.prefs.PREF_STRING) {
+ pref = PREF_GETADDONS_BYIDS_PERFORMANCE;
+
+ let startupInfo = Cc["@mozilla.org/toolkit/app-startup;1"].
+ getService(Ci.nsIAppStartup).
+ getStartupInfo();
+
+ params.TIME_MAIN = "";
+ params.TIME_FIRST_PAINT = "";
+ params.TIME_SESSION_RESTORED = "";
+ if (startupInfo.process) {
+ if (startupInfo.main) {
+ params.TIME_MAIN = startupInfo.main - startupInfo.process;
+ }
+ if (startupInfo.firstPaint) {
+ params.TIME_FIRST_PAINT = startupInfo.firstPaint -
+ startupInfo.process;
+ }
+ if (startupInfo.sessionRestored) {
+ params.TIME_SESSION_RESTORED = startupInfo.sessionRestored -
+ startupInfo.process;
+ }
+ }
+ }
+ }
+
+ let url = this._formatURLPref(pref, params);
+
+ let self = this;
+ function handleResults(aElements, aTotalResults, aCompatData) {
+ // Don't use this._parseAddons() so that, for example,
+ // incompatible add-ons are not filtered out
+ let results = [];
+ for (let i = 0; i < aElements.length && results.length < self._maxResults; i++) {
+ let result = self._parseAddon(aElements[i], null, aCompatData);
+ if (result == null)
+ continue;
+
+ // Ignore add-on if it wasn't actually requested
+ let idIndex = ids.indexOf(result.addon.id);
+ if (idIndex == -1)
+ continue;
+
+ // Ignore add-on if the add-on manager doesn't know about its type:
+ if (!(result.addon.type in AddonManager.addonTypes)) {
+ continue;
+ }
+
+ results.push(result);
+ // Ignore this add-on from now on
+ ids.splice(idIndex, 1);
+ }
+
+ // Include any compatibility overrides for addons not hosted by the
+ // remote repository.
+ for each (let addonCompat in aCompatData) {
+ if (addonCompat.hosted)
+ continue;
+
+ let addon = new AddonSearchResult(addonCompat.id);
+ // Compatibility overrides can only be for extensions.
+ addon.type = "extension";
+ addon.compatibilityOverrides = addonCompat.compatRanges;
+ let result = {
+ addon: addon,
+ xpiURL: null,
+ xpiHash: null
+ };
+ results.push(result);
+ }
+
+ // aTotalResults irrelevant
+ self._reportSuccess(results, -1);
+ }
+
+ this._beginSearch(url, ids.length, aCallback, handleResults, aTimeout);
+ },
+
+ /**
+ * Performs the daily background update check.
+ *
+ * This API both searches for the add-on IDs specified and sends performance
+ * data. It is meant to be called as part of the daily update ping. It should
+ * not be used for any other purpose. Use repopulateCache instead.
+ *
+ * @return Promise{null} Resolves when the metadata update is complete.
+ */
+ backgroundUpdateCheck: function () {
+ return this._repopulateCacheInternal(true);
+ },
+
+ /**
+ * Begins a search for recommended add-ons in this repository. Results will
+ * be passed to the given callback.
+ *
+ * @param aMaxResults
+ * The maximum number of results to return
+ * @param aCallback
+ * The callback to pass results to
+ */
+ retrieveRecommendedAddons: function AddonRepo_retrieveRecommendedAddons(aMaxResults, aCallback) {
+ let url = this._formatURLPref(PREF_GETADDONS_GETRECOMMENDED, {
+ API_VERSION : API_VERSION,
+
+ // Get twice as many results to account for potential filtering
+ MAX_RESULTS : 2 * aMaxResults
+ });
+
+ let self = this;
+ function handleResults(aElements, aTotalResults) {
+ self._getLocalAddonIds(function retrieveRecommendedAddons_getLocalAddonIds(aLocalAddonIds) {
+ // aTotalResults irrelevant
+ self._parseAddons(aElements, -1, aLocalAddonIds);
+ });
+ }
+
+ this._beginSearch(url, aMaxResults, aCallback, handleResults);
+ },
+
+ /**
+ * Begins a search for add-ons in this repository. Results will be passed to
+ * the given callback.
+ *
+ * @param aSearchTerms
+ * The terms to search for
+ * @param aMaxResults
+ * The maximum number of results to return
+ * @param aCallback
+ * The callback to pass results to
+ */
+ searchAddons: function AddonRepo_searchAddons(aSearchTerms, aMaxResults, aCallback) {
+ let compatMode = "normal";
+ if (!AddonManager.checkCompatibility)
+ compatMode = "ignore";
+ else if (AddonManager.strictCompatibility)
+ compatMode = "strict";
+
+ let substitutions = {
+ API_VERSION : API_VERSION,
+ TERMS : encodeURIComponent(aSearchTerms),
+ // Get twice as many results to account for potential filtering
+ MAX_RESULTS : 2 * aMaxResults,
+ COMPATIBILITY_MODE : compatMode,
+ };
+
+ let url = this._formatURLPref(PREF_GETADDONS_GETSEARCHRESULTS, substitutions);
+
+ let self = this;
+ function handleResults(aElements, aTotalResults) {
+ self._getLocalAddonIds(function searchAddons_getLocalAddonIds(aLocalAddonIds) {
+ self._parseAddons(aElements, aTotalResults, aLocalAddonIds);
+ });
+ }
+
+ this._beginSearch(url, aMaxResults, aCallback, handleResults);
+ },
+
+ // Posts results to the callback
+ _reportSuccess: function AddonRepo_reportSuccess(aResults, aTotalResults) {
+ this._searching = false;
+ this._request = null;
+ // The callback may want to trigger a new search so clear references early
+ // Tycho: let addons = [result.addon for each(result in aResults)];
+ let addons = [];
+ for each(let result in aResults) {
+ addons.push(result.addon);
+ }
+
+ let callback = this._callback;
+ this._callback = null;
+ callback.searchSucceeded(addons, addons.length, aTotalResults);
+ },
+
+ // Notifies the callback of a failure
+ _reportFailure: function AddonRepo_reportFailure() {
+ this._searching = false;
+ this._request = null;
+ // The callback may want to trigger a new search so clear references early
+ let callback = this._callback;
+ this._callback = null;
+ callback.searchFailed();
+ },
+
+ // Get descendant by unique tag name. Returns null if not unique tag name.
+ _getUniqueDescendant: function AddonRepo_getUniqueDescendant(aElement, aTagName) {
+ let elementsList = aElement.getElementsByTagName(aTagName);
+ return (elementsList.length == 1) ? elementsList[0] : null;
+ },
+
+ // Get direct descendant by unique tag name.
+ // Returns null if not unique tag name.
+ _getUniqueDirectDescendant: function AddonRepo_getUniqueDirectDescendant(aElement, aTagName) {
+ let elementsList = Array.filter(aElement.children,
+ function arrayFiltering(aChild) aChild.tagName == aTagName);
+ return (elementsList.length == 1) ? elementsList[0] : null;
+ },
+
+ // Parse out trimmed text content. Returns null if text content empty.
+ _getTextContent: function AddonRepo_getTextContent(aElement) {
+ let textContent = aElement.textContent.trim();
+ return (textContent.length > 0) ? textContent : null;
+ },
+
+ // Parse out trimmed text content of a descendant with the specified tag name
+ // Returns null if the parsing unsuccessful.
+ _getDescendantTextContent: function AddonRepo_getDescendantTextContent(aElement, aTagName) {
+ let descendant = this._getUniqueDescendant(aElement, aTagName);
+ return (descendant != null) ? this._getTextContent(descendant) : null;
+ },
+
+ // Parse out trimmed text content of a direct descendant with the specified
+ // tag name.
+ // Returns null if the parsing unsuccessful.
+ _getDirectDescendantTextContent: function AddonRepo_getDirectDescendantTextContent(aElement, aTagName) {
+ let descendant = this._getUniqueDirectDescendant(aElement, aTagName);
+ return (descendant != null) ? this._getTextContent(descendant) : null;
+ },
+
+ /*
+ * Creates an AddonSearchResult by parsing an <addon> element
+ *
+ * @param aElement
+ * The <addon> element to parse
+ * @param aSkip
+ * Object containing ids and sourceURIs of add-ons to skip.
+ * @param aCompatData
+ * Array of parsed addon_compatibility elements to accosiate with the
+ * resulting AddonSearchResult. Optional.
+ * @return Result object containing the parsed AddonSearchResult, xpiURL and
+ * xpiHash if the parsing was successful. Otherwise returns null.
+ */
+ _parseAddon: function AddonRepo_parseAddon(aElement, aSkip, aCompatData) {
+ let skipIDs = (aSkip && aSkip.ids) ? aSkip.ids : [];
+ let skipSourceURIs = (aSkip && aSkip.sourceURIs) ? aSkip.sourceURIs : [];
+
+ let guid = this._getDescendantTextContent(aElement, "guid");
+ if (guid == null || skipIDs.indexOf(guid) != -1)
+ return null;
+
+ let addon = new AddonSearchResult(guid);
+ let result = {
+ addon: addon,
+ xpiURL: null,
+ xpiHash: null
+ };
+
+ if (aCompatData && guid in aCompatData)
+ addon.compatibilityOverrides = aCompatData[guid].compatRanges;
+
+ let self = this;
+ for (let node = aElement.firstChild; node; node = node.nextSibling) {
+ if (!(node instanceof Ci.nsIDOMElement))
+ continue;
+
+ let localName = node.localName;
+
+ // Handle case where the wanted string value is located in text content
+ // but only if the content is not empty
+ if (localName in STRING_KEY_MAP) {
+ addon[STRING_KEY_MAP[localName]] = this._getTextContent(node) || addon[STRING_KEY_MAP[localName]];
+ continue;
+ }
+
+ // Handle case where the wanted string value is html located in text content
+ if (localName in HTML_KEY_MAP) {
+ addon[HTML_KEY_MAP[localName]] = convertHTMLToPlainText(this._getTextContent(node));
+ continue;
+ }
+
+ // Handle case where the wanted integer value is located in text content
+ if (localName in INTEGER_KEY_MAP) {
+ let value = parseInt(this._getTextContent(node));
+ if (value >= 0)
+ addon[INTEGER_KEY_MAP[localName]] = value;
+ continue;
+ }
+
+ // Handle cases that aren't as simple as grabbing the text content
+ switch (localName) {
+ case "type":
+ // Map AMO's type id to corresponding string
+ // https://github.com/mozilla/olympia/blob/master/apps/constants/base.py#L127
+ // These definitions need to be updated whenever AMO adds a new type.
+ let id = parseInt(node.getAttribute("id"));
+ switch (id) {
+ case 1:
+ addon.type = "extension";
+ break;
+ case 2:
+ addon.type = "theme";
+ break;
+ case 3:
+ addon.type = "dictionary";
+ break;
+ case 4:
+ addon.type = "search";
+ break;
+ case 5:
+ case 6:
+ addon.type = "locale";
+ break;
+ case 7:
+ addon.type = "plugin";
+ break;
+ case 8:
+ addon.type = "api";
+ break;
+ case 9:
+ addon.type = "lightweight-theme";
+ break;
+ case 11:
+ addon.type = "webapp";
+ break;
+ default:
+ logger.info("Unknown type id " + id + " found when parsing response for GUID " + guid);
+ }
+ break;
+ case "authors":
+ let authorNodes = node.getElementsByTagName("author");
+ for (let authorNode of authorNodes) {
+ let name = self._getDescendantTextContent(authorNode, "name");
+ let link = self._getDescendantTextContent(authorNode, "link");
+ if (name == null || link == null)
+ continue;
+
+ let author = new AddonManagerPrivate.AddonAuthor(name, link);
+ if (addon.creator == null)
+ addon.creator = author;
+ else {
+ if (addon.developers == null)
+ addon.developers = [];
+
+ addon.developers.push(author);
+ }
+ }
+ break;
+ case "previews":
+ let previewNodes = node.getElementsByTagName("preview");
+ for (let previewNode of previewNodes) {
+ let full = self._getUniqueDescendant(previewNode, "full");
+ if (full == null)
+ continue;
+
+ let fullURL = self._getTextContent(full);
+ let fullWidth = full.getAttribute("width");
+ let fullHeight = full.getAttribute("height");
+
+ let thumbnailURL, thumbnailWidth, thumbnailHeight;
+ let thumbnail = self._getUniqueDescendant(previewNode, "thumbnail");
+ if (thumbnail) {
+ thumbnailURL = self._getTextContent(thumbnail);
+ thumbnailWidth = thumbnail.getAttribute("width");
+ thumbnailHeight = thumbnail.getAttribute("height");
+ }
+ let caption = self._getDescendantTextContent(previewNode, "caption");
+ let screenshot = new AddonManagerPrivate.AddonScreenshot(fullURL, fullWidth, fullHeight,
+ thumbnailURL, thumbnailWidth,
+ thumbnailHeight, caption);
+
+ if (addon.screenshots == null)
+ addon.screenshots = [];
+
+ if (previewNode.getAttribute("primary") == 1)
+ addon.screenshots.unshift(screenshot);
+ else
+ addon.screenshots.push(screenshot);
+ }
+ break;
+ case "learnmore":
+ addon.learnmoreURL = this._getTextContent(node);
+ addon.homepageURL = addon.homepageURL || addon.learnmoreURL;
+ break;
+ case "contribution_data":
+ let meetDevelopers = this._getDescendantTextContent(node, "meet_developers");
+ let suggestedAmount = this._getDescendantTextContent(node, "suggested_amount");
+ if (meetDevelopers != null) {
+ addon.contributionURL = meetDevelopers;
+ addon.contributionAmount = suggestedAmount;
+ }
+ break
+ case "payment_data":
+ let link = this._getDescendantTextContent(node, "link");
+ let amountTag = this._getUniqueDescendant(node, "amount");
+ let amount = parseFloat(amountTag.getAttribute("amount"));
+ let displayAmount = this._getTextContent(amountTag);
+ if (link != null && amount != null && displayAmount != null) {
+ addon.purchaseURL = link;
+ addon.purchaseAmount = amount;
+ addon.purchaseDisplayAmount = displayAmount;
+ }
+ break
+ case "rating":
+ let averageRating = parseInt(this._getTextContent(node));
+ if (averageRating >= 0)
+ addon.averageRating = Math.min(5, averageRating);
+ break;
+ case "reviews":
+ let url = this._getTextContent(node);
+ let num = parseInt(node.getAttribute("num"));
+ if (url != null && num >= 0) {
+ addon.reviewURL = url;
+ addon.reviewCount = num;
+ }
+ break;
+ case "status":
+ let repositoryStatus = parseInt(node.getAttribute("id"));
+ if (!isNaN(repositoryStatus))
+ addon.repositoryStatus = repositoryStatus;
+ break;
+ case "all_compatible_os":
+ let nodes = node.getElementsByTagName("os");
+ addon.isPlatformCompatible = Array.some(nodes, function parseAddon_platformCompatFilter(aNode) {
+ let text = aNode.textContent.toLowerCase().trim();
+ return text == "all" || text == Services.appinfo.OS.toLowerCase();
+ });
+ break;
+ case "install":
+ // No os attribute means the xpi is compatible with any os
+ if (node.hasAttribute("os")) {
+ let os = node.getAttribute("os").trim().toLowerCase();
+ // If the os is not ALL and not the current OS then ignore this xpi
+ if (os != "all" && os != Services.appinfo.OS.toLowerCase())
+ break;
+ }
+
+ let xpiURL = this._getTextContent(node);
+ if (xpiURL == null)
+ break;
+
+ if (skipSourceURIs.indexOf(xpiURL) != -1)
+ return null;
+
+ result.xpiURL = xpiURL;
+ addon.sourceURI = NetUtil.newURI(xpiURL);
+
+ let size = parseInt(node.getAttribute("size"));
+ addon.size = (size >= 0) ? size : null;
+
+ let xpiHash = node.getAttribute("hash");
+ if (xpiHash != null)
+ xpiHash = xpiHash.trim();
+ result.xpiHash = xpiHash ? xpiHash : null;
+ break;
+ case "last_updated":
+ let epoch = parseInt(node.getAttribute("epoch"));
+ if (!isNaN(epoch))
+ addon.updateDate = new Date(1000 * epoch);
+ break;
+ case "icon":
+ addon.icons[node.getAttribute("size")] = this._getTextContent(node);
+ break;
+ }
+ }
+
+ return result;
+ },
+
+ _parseAddons: function AddonRepo_parseAddons(aElements, aTotalResults, aSkip) {
+ let self = this;
+ let results = [];
+
+ function isSameApplication(aAppNode) {
+#ifdef MOZ_PHOENIX_EXTENSIONS
+ if (self._getTextContent(aAppNode) == Services.appinfo.ID ||
+ self._getTextContent(aAppNode) == FIREFOX_ID) {
+#else
+ if (self._getTextContent(aAppNode) == Services.appinfo.ID) {
+#endif
+ return true;
+ }
+ return false;
+ }
+
+ for (let i = 0; i < aElements.length && results.length < this._maxResults; i++) {
+ let element = aElements[i];
+
+ let tags = this._getUniqueDescendant(element, "compatible_applications");
+ if (tags == null)
+ continue;
+
+ let applications = tags.getElementsByTagName("appID");
+ let compatible = Array.some(applications, function parseAddons_applicationsCompatFilter(aAppNode) {
+ if (!isSameApplication(aAppNode))
+ return false;
+
+ let parent = aAppNode.parentNode;
+ let minVersion = self._getDescendantTextContent(parent, "min_version");
+ let maxVersion = self._getDescendantTextContent(parent, "max_version");
+ if (minVersion == null || maxVersion == null)
+ return false;
+
+ let currentVersion = Services.appinfo.version;
+ return (Services.vc.compare(minVersion, currentVersion) <= 0 &&
+ ((!AddonManager.strictCompatibility) ||
+ Services.vc.compare(currentVersion, maxVersion) <= 0));
+ });
+
+ // Ignore add-ons not compatible with this Application
+ if (!compatible) {
+ if (AddonManager.checkCompatibility)
+ continue;
+
+ if (!Array.some(applications, isSameApplication))
+ continue;
+ }
+
+ // Add-on meets all requirements, so parse out data.
+ // Don't pass in compatiblity override data, because that's only returned
+ // in GUID searches, which don't use _parseAddons().
+ let result = this._parseAddon(element, aSkip);
+ if (result == null)
+ continue;
+
+ // Ignore add-on missing a required attribute
+ let requiredAttributes = ["id", "name", "version", "type", "creator"];
+ if (requiredAttributes.some(function parseAddons_attributeFilter(aAttribute) !result.addon[aAttribute]))
+ continue;
+
+ // Ignore add-on with a type AddonManager doesn't understand:
+ if (!(result.addon.type in AddonManager.addonTypes))
+ continue;
+
+ // Add only if the add-on is compatible with the platform
+ if (!result.addon.isPlatformCompatible)
+ continue;
+
+ // Add only if there was an xpi compatible with this OS or there was a
+ // way to purchase the add-on
+ if (!result.xpiURL && !result.addon.purchaseURL)
+ continue;
+
+ result.addon.isCompatible = compatible;
+
+ results.push(result);
+ // Ignore this add-on from now on by adding it to the skip array
+ aSkip.ids.push(result.addon.id);
+ }
+
+ // Immediately report success if no AddonInstall instances to create
+ let pendingResults = results.length;
+ if (pendingResults == 0) {
+ this._reportSuccess(results, aTotalResults);
+ return;
+ }
+
+ // Create an AddonInstall for each result
+ results.forEach(function(aResult) {
+ let addon = aResult.addon;
+ let callback = function addonInstallCallback(aInstall) {
+ addon.install = aInstall;
+ pendingResults--;
+ if (pendingResults == 0)
+ self._reportSuccess(results, aTotalResults);
+ }
+
+ if (aResult.xpiURL) {
+ AddonManager.getInstallForURL(aResult.xpiURL, callback,
+ "application/x-xpinstall", aResult.xpiHash,
+ addon.name, addon.icons, addon.version);
+ }
+ else {
+ callback(null);
+ }
+ });
+ },
+
+ // Parses addon_compatibility nodes, that describe compatibility overrides.
+ _parseAddonCompatElement: function AddonRepo_parseAddonCompatElement(aResultObj, aElement) {
+ let guid = this._getDescendantTextContent(aElement, "guid");
+ if (!guid) {
+ logger.debug("Compatibility override is missing guid.");
+ return;
+ }
+
+ let compat = {id: guid};
+ compat.hosted = aElement.getAttribute("hosted") != "false";
+
+ function findMatchingAppRange(aNodes) {
+ let toolkitAppRange = null;
+ for (let node of aNodes) {
+ let appID = this._getDescendantTextContent(node, "appID");
+ if (appID != Services.appinfo.ID && appID != TOOLKIT_ID)
+ continue;
+
+ let minVersion = this._getDescendantTextContent(node, "min_version");
+ let maxVersion = this._getDescendantTextContent(node, "max_version");
+ if (minVersion == null || maxVersion == null)
+ continue;
+
+ let appRange = { appID: appID,
+ appMinVersion: minVersion,
+ appMaxVersion: maxVersion };
+
+ // Only use Toolkit app ranges if no ranges match the application ID.
+ if (appID == TOOLKIT_ID)
+ toolkitAppRange = appRange;
+ else
+ return appRange;
+ }
+ return toolkitAppRange;
+ }
+
+ function parseRangeNode(aNode) {
+ let type = aNode.getAttribute("type");
+ // Only "incompatible" (blacklisting) is supported for now.
+ if (type != "incompatible") {
+ logger.debug("Compatibility override of unsupported type found.");
+ return null;
+ }
+
+ let override = new AddonManagerPrivate.AddonCompatibilityOverride(type);
+
+ override.minVersion = this._getDirectDescendantTextContent(aNode, "min_version");
+ override.maxVersion = this._getDirectDescendantTextContent(aNode, "max_version");
+
+ if (!override.minVersion) {
+ logger.debug("Compatibility override is missing min_version.");
+ return null;
+ }
+ if (!override.maxVersion) {
+ logger.debug("Compatibility override is missing max_version.");
+ return null;
+ }
+
+ let appRanges = aNode.querySelectorAll("compatible_applications > application");
+ let appRange = findMatchingAppRange.bind(this)(appRanges);
+ if (!appRange) {
+ logger.debug("Compatibility override is missing a valid application range.");
+ return null;
+ }
+
+ override.appID = appRange.appID;
+ override.appMinVersion = appRange.appMinVersion;
+ override.appMaxVersion = appRange.appMaxVersion;
+
+ return override;
+ }
+
+ let rangeNodes = aElement.querySelectorAll("version_ranges > version_range");
+ compat.compatRanges = Array.map(rangeNodes, parseRangeNode.bind(this))
+ .filter(function compatRangesFilter(aItem) !!aItem);
+ if (compat.compatRanges.length == 0)
+ return;
+
+ aResultObj[compat.id] = compat;
+ },
+
+ // Parses addon_compatibility elements.
+ _parseAddonCompatData: function AddonRepo_parseAddonCompatData(aElements) {
+ let compatData = {};
+ Array.forEach(aElements, this._parseAddonCompatElement.bind(this, compatData));
+ return compatData;
+ },
+
+ // Begins a new search if one isn't currently executing
+ _beginSearch: function(aURI, aMaxResults, aCallback, aHandleResults, aTimeout) {
+ if (this._searching || aURI == null || aMaxResults <= 0) {
+ logger.warn("AddonRepository search failed: searching " + this._searching + " aURI " + aURI +
+ " aMaxResults " + aMaxResults);
+ aCallback.searchFailed();
+ return;
+ }
+
+ this._searching = true;
+ this._callback = aCallback;
+ this._maxResults = aMaxResults;
+
+ logger.debug("Requesting " + aURI);
+
+ this._request = new XHRequest();
+ this._request.mozBackgroundRequest = true;
+ this._request.open("GET", aURI, true);
+ this._request.overrideMimeType("text/xml");
+ if (aTimeout) {
+ this._request.timeout = aTimeout;
+ }
+
+ this._request.addEventListener("error", aEvent => this._reportFailure(), false);
+ this._request.addEventListener("timeout", aEvent => this._reportFailure(), false);
+ this._request.addEventListener("load", aEvent => {
+ logger.debug("Got metadata search load event");
+ let request = aEvent.target;
+ let responseXML = request.responseXML;
+
+ if (!responseXML || responseXML.documentElement.namespaceURI == XMLURI_PARSE_ERROR ||
+ (request.status != 200 && request.status != 0)) {
+ this._reportFailure();
+ return;
+ }
+
+ let documentElement = responseXML.documentElement;
+ let elements = documentElement.getElementsByTagName("addon");
+ let totalResults = elements.length;
+ let parsedTotalResults = parseInt(documentElement.getAttribute("total_results"));
+ // Parsed value of total results only makes sense if >= elements.length
+ if (parsedTotalResults >= totalResults)
+ totalResults = parsedTotalResults;
+
+ let compatElements = documentElement.getElementsByTagName("addon_compatibility");
+ let compatData = this._parseAddonCompatData(compatElements);
+
+ aHandleResults(elements, totalResults, compatData);
+ }, false);
+ this._request.send(null);
+ },
+
+ // Gets the id's of local add-ons, and the sourceURI's of local installs,
+ // passing the results to aCallback
+ _getLocalAddonIds: function AddonRepo_getLocalAddonIds(aCallback) {
+ let self = this;
+ let localAddonIds = {ids: null, sourceURIs: null};
+
+ AddonManager.getAllAddons(function getLocalAddonIds_getAllAddons(aAddons) {
+ // Tycho: localAddonIds.ids = [a.id for each (a in aAddons)];
+ localAddonIds.ids = [];
+
+ for each(let a in aAddons) {
+ localAddonIds.ids.push(a.id);
+ }
+
+ if (localAddonIds.sourceURIs)
+ aCallback(localAddonIds);
+ });
+
+ AddonManager.getAllInstalls(function getLocalAddonIds_getAllInstalls(aInstalls) {
+ localAddonIds.sourceURIs = [];
+ aInstalls.forEach(function(aInstall) {
+ if (aInstall.state != AddonManager.STATE_AVAILABLE)
+ localAddonIds.sourceURIs.push(aInstall.sourceURI.spec);
+ });
+
+ if (localAddonIds.ids)
+ aCallback(localAddonIds);
+ });
+ },
+
+ // Create url from preference, returning null if preference does not exist
+ _formatURLPref: function AddonRepo_formatURLPref(aPreference, aSubstitutions) {
+ let url = null;
+ try {
+ url = Services.prefs.getCharPref(aPreference);
+ } catch(e) {
+ logger.warn("_formatURLPref: Couldn't get pref: " + aPreference);
+ return null;
+ }
+
+ url = url.replace(/%([A-Z_]+)%/g, function urlSubstitution(aMatch, aKey) {
+ return (aKey in aSubstitutions) ? aSubstitutions[aKey] : aMatch;
+ });
+
+ return Services.urlFormatter.formatURL(url);
+ },
+
+ // Find a AddonCompatibilityOverride that matches a given aAddonVersion and
+ // application/platform version.
+ findMatchingCompatOverride: function AddonRepo_findMatchingCompatOverride(aAddonVersion,
+ aCompatOverrides,
+ aAppVersion,
+ aPlatformVersion) {
+ for (let override of aCompatOverrides) {
+
+ let appVersion = null;
+ if (override.appID == TOOLKIT_ID)
+ appVersion = aPlatformVersion || Services.appinfo.platformVersion;
+ else
+ appVersion = aAppVersion || Services.appinfo.version;
+
+ if (Services.vc.compare(override.minVersion, aAddonVersion) <= 0 &&
+ Services.vc.compare(aAddonVersion, override.maxVersion) <= 0 &&
+ Services.vc.compare(override.appMinVersion, appVersion) <= 0 &&
+ Services.vc.compare(appVersion, override.appMaxVersion) <= 0) {
+ return override;
+ }
+ }
+ return null;
+ },
+
+ flush: function() {
+ return AddonDatabase.flush();
+ }
+};
+
+var AddonDatabase = {
+ // false if there was an unrecoverable error opening the database
+ databaseOk: true,
+
+ connectionPromise: null,
+ // the in-memory database
+ DB: BLANK_DB(),
+
+ /**
+ * A getter to retrieve the path to the DB
+ */
+ get jsonFile() {
+ return OS.Path.join(OS.Constants.Path.profileDir, FILE_DATABASE);
+ },
+
+ /**
+ * Asynchronously opens a new connection to the database file.
+ *
+ * @return {Promise} a promise that resolves to the database.
+ */
+ openConnection: function() {
+ if (!this.connectionPromise) {
+ this.connectionPromise = Task.spawn(function*() {
+ this.DB = BLANK_DB();
+
+ let inputDB, schema;
+
+ try {
+ let data = yield OS.File.read(this.jsonFile, { encoding: "utf-8"})
+ inputDB = JSON.parse(data);
+
+ if (!inputDB.hasOwnProperty("addons") ||
+ !Array.isArray(inputDB.addons)) {
+ throw new Error("No addons array.");
+ }
+
+ if (!inputDB.hasOwnProperty("schema")) {
+ throw new Error("No schema specified.");
+ }
+
+ schema = parseInt(inputDB.schema, 10);
+
+ if (!Number.isInteger(schema) ||
+ schema < DB_MIN_JSON_SCHEMA) {
+ throw new Error("Invalid schema value.");
+ }
+ } catch (e if e instanceof OS.File.Error && e.becauseNoSuchFile) {
+ logger.debug("No " + FILE_DATABASE + " found.");
+
+ // Create a blank addons.json file
+ this._saveDBToDisk();
+
+ let dbSchema = 0;
+ try {
+ dbSchema = Services.prefs.getIntPref(PREF_GETADDONS_DB_SCHEMA);
+ } catch (e) {}
+
+ if (dbSchema < DB_MIN_JSON_SCHEMA) {
+ let results = yield new Promise((resolve, reject) => {
+ AddonRepository_SQLiteMigrator.migrate(resolve);
+ });
+
+ if (results.length) {
+ yield this._insertAddons(results);
+ }
+
+ Services.prefs.setIntPref(PREF_GETADDONS_DB_SCHEMA, DB_SCHEMA);
+ }
+
+ return this.DB;
+ } catch (e) {
+ logger.error("Malformed " + FILE_DATABASE + ": " + e);
+ this.databaseOk = false;
+
+ return this.DB;
+ }
+
+ Services.prefs.setIntPref(PREF_GETADDONS_DB_SCHEMA, DB_SCHEMA);
+
+ // We use _insertAddon manually instead of calling
+ // insertAddons to avoid the write to disk which would
+ // be a waste since this is the data that was just read.
+ for (let addon of inputDB.addons) {
+ this._insertAddon(addon);
+ }
+
+ return this.DB;
+ }.bind(this));
+ }
+
+ return this.connectionPromise;
+ },
+
+ /**
+ * A lazy getter for the database connection.
+ */
+ get connection() {
+ return this.openConnection();
+ },
+
+ /**
+ * Asynchronously shuts down the database connection and releases all
+ * cached objects
+ *
+ * @param aCallback
+ * An optional callback to call once complete
+ * @param aSkipFlush
+ * An optional boolean to skip flushing data to disk. Useful
+ * when the database is going to be deleted afterwards.
+ */
+ shutdown: function AD_shutdown(aSkipFlush) {
+ this.databaseOk = true;
+
+ if (!this.connectionPromise) {
+ return Promise.resolve();
+ }
+
+ this.connectionPromise = null;
+
+ if (aSkipFlush) {
+ return Promise.resolve();
+ } else {
+ return this.Writer.flush();
+ }
+ },
+
+ /**
+ * Asynchronously deletes the database, shutting down the connection
+ * first if initialized
+ *
+ * @param aCallback
+ * An optional callback to call once complete
+ * @return Promise{null} resolves when the database has been deleted
+ */
+ delete: function AD_delete(aCallback) {
+ this.DB = BLANK_DB();
+
+ this._deleting = this.Writer.flush()
+ .then(null, () => {})
+ // shutdown(true) never rejects
+ .then(() => this.shutdown(true))
+ .then(() => OS.File.remove(this.jsonFile, {}))
+ .then(null, error => logger.error("Unable to delete Addon Repository file " +
+ this.jsonFile, error))
+ .then(() => this._deleting = null)
+ .then(aCallback);
+ return this._deleting;
+ },
+
+ toJSON: function AD_toJSON() {
+ let json = {
+ schema: this.DB.schema,
+ addons: []
+ }
+
+ for (let [, value] of this.DB.addons)
+ json.addons.push(value);
+
+ return json;
+ },
+
+ /*
+ * This is a deferred task writer that is used
+ * to batch operations done within 50ms of each
+ * other and thus generating only one write to disk
+ */
+ get Writer() {
+ delete this.Writer;
+ this.Writer = new DeferredSave(
+ this.jsonFile,
+ () => { return JSON.stringify(this); },
+ DB_BATCH_TIMEOUT_MS
+ );
+ return this.Writer;
+ },
+
+ /**
+ * Flush any pending I/O on the addons.json file
+ * @return: Promise{null}
+ * Resolves when the pending I/O (writing out or deleting
+ * addons.json) completes
+ */
+ flush: function() {
+ if (this._deleting) {
+ return this._deleting;
+ }
+ return this.Writer.flush();
+ },
+
+ /**
+ * Asynchronously retrieve all add-ons from the database
+ * @return: Promise{Map}
+ * Resolves when the add-ons are retrieved from the database
+ */
+ retrieveStoredData: function (){
+ return this.openConnection().then(db => db.addons);
+ },
+
+ /**
+ * Asynchronously repopulates the database so it only contains the
+ * specified add-ons
+ *
+ * @param aAddons
+ * The array of add-ons to repopulate the database with
+ * @param aCallback
+ * An optional callback to call once complete
+ */
+ repopulate: function AD_repopulate(aAddons, aCallback) {
+ this.DB.addons.clear();
+ this.insertAddons(aAddons, function repopulate_insertAddons() {
+ let now = Math.round(Date.now() / 1000);
+ logger.debug("Cache repopulated, setting " + PREF_METADATA_LASTUPDATE + " to " + now);
+ Services.prefs.setIntPref(PREF_METADATA_LASTUPDATE, now);
+ if (aCallback)
+ aCallback();
+ });
+ },
+
+ /**
+ * Asynchronously inserts an array of add-ons into the database
+ *
+ * @param aAddons
+ * The array of add-ons to insert
+ * @param aCallback
+ * An optional callback to call once complete
+ */
+ insertAddons: Task.async(function* (aAddons, aCallback) {
+ yield this.openConnection();
+ yield this._insertAddons(aAddons, aCallback);
+ }),
+
+ _insertAddons: Task.async(function* (aAddons, aCallback) {
+ for (let addon of aAddons) {
+ this._insertAddon(addon);
+ }
+
+ yield this._saveDBToDisk();
+ aCallback && aCallback();
+ }),
+
+ /**
+ * Inserts an individual add-on into the database. If the add-on already
+ * exists in the database (by id), then the specified add-on will not be
+ * inserted.
+ *
+ * @param aAddon
+ * The add-on to insert into the database
+ * @param aCallback
+ * The callback to call once complete
+ */
+ _insertAddon: function AD__insertAddon(aAddon) {
+ let newAddon = this._parseAddon(aAddon);
+ if (!newAddon ||
+ !newAddon.id ||
+ this.DB.addons.has(newAddon.id))
+ return;
+
+ this.DB.addons.set(newAddon.id, newAddon);
+ },
+
+ /*
+ * Creates an AddonSearchResult by parsing an object structure
+ * retrieved from the DB JSON representation.
+ *
+ * @param aObj
+ * The object to parse
+ * @return Returns an AddonSearchResult object.
+ */
+ _parseAddon: function (aObj) {
+ if (aObj instanceof AddonSearchResult)
+ return aObj;
+
+ let id = aObj.id;
+ if (!aObj.id)
+ return null;
+
+ let addon = new AddonSearchResult(id);
+
+ for (let [expectedProperty,] of Iterator(AddonSearchResult.prototype)) {
+ if (!(expectedProperty in aObj) ||
+ typeof(aObj[expectedProperty]) === "function")
+ continue;
+
+ let value = aObj[expectedProperty];
+
+ try {
+ switch (expectedProperty) {
+ case "sourceURI":
+ addon.sourceURI = value ? NetUtil.newURI(value) : null;
+ break;
+
+ case "creator":
+ addon.creator = value
+ ? this._makeDeveloper(value)
+ : null;
+ break;
+
+ case "updateDate":
+ addon.updateDate = value ? new Date(value) : null;
+ break;
+
+ case "developers":
+ if (!addon.developers) addon.developers = [];
+ for (let developer of value) {
+ addon.developers.push(this._makeDeveloper(developer));
+ }
+ break;
+
+ case "screenshots":
+ if (!addon.screenshots) addon.screenshots = [];
+ for (let screenshot of value) {
+ addon.screenshots.push(this._makeScreenshot(screenshot));
+ }
+ break;
+
+ case "compatibilityOverrides":
+ if (!addon.compatibilityOverrides) addon.compatibilityOverrides = [];
+ for (let override of value) {
+ addon.compatibilityOverrides.push(
+ this._makeCompatOverride(override)
+ );
+ }
+ break;
+
+ case "icons":
+ if (!addon.icons) addon.icons = {};
+ for (let [size, url] of Iterator(aObj.icons)) {
+ addon.icons[size] = url;
+ }
+ break;
+
+ case "iconURL":
+ break;
+
+ default:
+ addon[expectedProperty] = value;
+ }
+ } catch (ex) {
+ logger.warn("Error in parsing property value for " + expectedProperty + " | " + ex);
+ }
+
+ // delete property from obj to indicate we've already
+ // handled it. The remaining public properties will
+ // be stored separately and just passed through to
+ // be written back to the DB.
+ delete aObj[expectedProperty];
+ }
+
+ // Copy remaining properties to a separate object
+ // to prevent accidental access on downgraded versions.
+ // The properties will be merged in the same object
+ // prior to being written back through toJSON.
+ for (let remainingProperty of Object.keys(aObj)) {
+ switch (typeof(aObj[remainingProperty])) {
+ case "boolean":
+ case "number":
+ case "string":
+ case "object":
+ // these types are accepted
+ break;
+ default:
+ continue;
+ }
+
+ if (!remainingProperty.startsWith("_"))
+ addon._unsupportedProperties[remainingProperty] =
+ aObj[remainingProperty];
+ }
+
+ return addon;
+ },
+
+ /**
+ * Write the in-memory DB to disk, after waiting for
+ * the DB_BATCH_TIMEOUT_MS timeout.
+ *
+ * @return Promise A promise that resolves after the
+ * write to disk has completed.
+ */
+ _saveDBToDisk: function() {
+ return this.Writer.saveChanges().then(
+ null,
+ e => logger.error("SaveDBToDisk failed", e));
+ },
+
+ /**
+ * Make a developer object from a vanilla
+ * JS object from the JSON database
+ *
+ * @param aObj
+ * The JS object to use
+ * @return The created developer
+ */
+ _makeDeveloper: function (aObj) {
+ let name = aObj.name;
+ let url = aObj.url;
+ return new AddonManagerPrivate.AddonAuthor(name, url);
+ },
+
+ /**
+ * Make a screenshot object from a vanilla
+ * JS object from the JSON database
+ *
+ * @param aObj
+ * The JS object to use
+ * @return The created screenshot
+ */
+ _makeScreenshot: function (aObj) {
+ let url = aObj.url;
+ let width = aObj.width;
+ let height = aObj.height;
+ let thumbnailURL = aObj.thumbnailURL;
+ let thumbnailWidth = aObj.thumbnailWidth;
+ let thumbnailHeight = aObj.thumbnailHeight;
+ let caption = aObj.caption;
+ return new AddonManagerPrivate.AddonScreenshot(url, width, height, thumbnailURL,
+ thumbnailWidth, thumbnailHeight, caption);
+ },
+
+ /**
+ * Make a CompatibilityOverride from a vanilla
+ * JS object from the JSON database
+ *
+ * @param aObj
+ * The JS object to use
+ * @return The created CompatibilityOverride
+ */
+ _makeCompatOverride: function (aObj) {
+ let type = aObj.type;
+ let minVersion = aObj.minVersion;
+ let maxVersion = aObj.maxVersion;
+ let appID = aObj.appID;
+ let appMinVersion = aObj.appMinVersion;
+ let appMaxVersion = aObj.appMaxVersion;
+ return new AddonManagerPrivate.AddonCompatibilityOverride(type,
+ minVersion,
+ maxVersion,
+ appID,
+ appMinVersion,
+ appMaxVersion);
+ },
+};
diff --git a/toolkit/mozapps/extensions/internal/AddonRepository_SQLiteMigrator.jsm b/toolkit/mozapps/extensions/internal/AddonRepository_SQLiteMigrator.jsm
new file mode 100644
index 000000000..128146bbe
--- /dev/null
+++ b/toolkit/mozapps/extensions/internal/AddonRepository_SQLiteMigrator.jsm
@@ -0,0 +1,518 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/AddonManager.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+
+const KEY_PROFILEDIR = "ProfD";
+const FILE_DATABASE = "addons.sqlite";
+const LAST_DB_SCHEMA = 4;
+
+// Add-on properties present in the columns of the database
+const PROP_SINGLE = ["id", "type", "name", "version", "creator", "description",
+ "fullDescription", "developerComments", "eula",
+ "homepageURL", "supportURL", "contributionURL",
+ "contributionAmount", "averageRating", "reviewCount",
+ "reviewURL", "totalDownloads", "weeklyDownloads",
+ "dailyUsers", "sourceURI", "repositoryStatus", "size",
+ "updateDate"];
+
+Cu.import("resource://gre/modules/Log.jsm");
+const LOGGER_ID = "addons.repository.sqlmigrator";
+
+// Create a new logger for use by the Addons Repository SQL Migrator
+// (Requires AddonManager.jsm)
+let logger = Log.repository.getLogger(LOGGER_ID);
+
+this.EXPORTED_SYMBOLS = ["AddonRepository_SQLiteMigrator"];
+
+
+this.AddonRepository_SQLiteMigrator = {
+
+ /**
+ * Migrates data from a previous SQLite version of the
+ * database to the JSON version.
+ *
+ * @param structFunctions an object that contains functions
+ * to create the various objects used
+ * in the new JSON format
+ * @param aCallback A callback to be called when migration
+ * finishes, with the results in an array
+ * @returns bool True if a migration will happen (DB was
+ * found and succesfully opened)
+ */
+ migrate: function(aCallback) {
+ if (!this._openConnection()) {
+ this._closeConnection();
+ aCallback([]);
+ return false;
+ }
+
+ logger.debug("Importing addon repository from previous " + FILE_DATABASE + " storage.");
+
+ this._retrieveStoredData((results) => {
+ this._closeConnection();
+ let resultArray = [addon for ([,addon] of Iterator(results))];
+ logger.debug(resultArray.length + " addons imported.")
+ aCallback(resultArray);
+ });
+
+ return true;
+ },
+
+ /**
+ * Synchronously opens a new connection to the database file.
+ *
+ * @return bool Whether the DB was opened successfully.
+ */
+ _openConnection: function AD_openConnection() {
+ delete this.connection;
+
+ let dbfile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_DATABASE], true);
+ if (!dbfile.exists())
+ return false;
+
+ try {
+ this.connection = Services.storage.openUnsharedDatabase(dbfile);
+ } catch (e) {
+ return false;
+ }
+
+ this.connection.executeSimpleSQL("PRAGMA locking_mode = EXCLUSIVE");
+
+ // Any errors in here should rollback
+ try {
+ this.connection.beginTransaction();
+
+ switch (this.connection.schemaVersion) {
+ case 0:
+ return false;
+
+ case 1:
+ logger.debug("Upgrading database schema to version 2");
+ this.connection.executeSimpleSQL("ALTER TABLE screenshot ADD COLUMN width INTEGER");
+ this.connection.executeSimpleSQL("ALTER TABLE screenshot ADD COLUMN height INTEGER");
+ this.connection.executeSimpleSQL("ALTER TABLE screenshot ADD COLUMN thumbnailWidth INTEGER");
+ this.connection.executeSimpleSQL("ALTER TABLE screenshot ADD COLUMN thumbnailHeight INTEGER");
+ case 2:
+ logger.debug("Upgrading database schema to version 3");
+ this.connection.createTable("compatibility_override",
+ "addon_internal_id INTEGER, " +
+ "num INTEGER, " +
+ "type TEXT, " +
+ "minVersion TEXT, " +
+ "maxVersion TEXT, " +
+ "appID TEXT, " +
+ "appMinVersion TEXT, " +
+ "appMaxVersion TEXT, " +
+ "PRIMARY KEY (addon_internal_id, num)");
+ case 3:
+ logger.debug("Upgrading database schema to version 4");
+ this.connection.createTable("icon",
+ "addon_internal_id INTEGER, " +
+ "size INTEGER, " +
+ "url TEXT, " +
+ "PRIMARY KEY (addon_internal_id, size)");
+ this._createIndices();
+ this._createTriggers();
+ this.connection.schemaVersion = LAST_DB_SCHEMA;
+ case LAST_DB_SCHEMA:
+ break;
+ default:
+ return false;
+ }
+ this.connection.commitTransaction();
+ } catch (e) {
+ logger.error("Failed to open " + FILE_DATABASE + ". Data import will not happen.", e);
+ this.logSQLError(this.connection.lastError, this.connection.lastErrorString);
+ this.connection.rollbackTransaction();
+ return false;
+ }
+
+ return true;
+ },
+
+ _closeConnection: function() {
+ for each (let stmt in this.asyncStatementsCache)
+ stmt.finalize();
+ this.asyncStatementsCache = {};
+
+ if (this.connection)
+ this.connection.asyncClose();
+
+ delete this.connection;
+ },
+
+ /**
+ * Asynchronously retrieve all add-ons from the database, and pass it
+ * to the specified callback
+ *
+ * @param aCallback
+ * The callback to pass the add-ons back to
+ */
+ _retrieveStoredData: function AD_retrieveStoredData(aCallback) {
+ let self = this;
+ let addons = {};
+
+ // Retrieve all data from the addon table
+ function getAllAddons() {
+ self.getAsyncStatement("getAllAddons").executeAsync({
+ handleResult: function getAllAddons_handleResult(aResults) {
+ let row = null;
+ while ((row = aResults.getNextRow())) {
+ let internal_id = row.getResultByName("internal_id");
+ addons[internal_id] = self._makeAddonFromAsyncRow(row);
+ }
+ },
+
+ handleError: self.asyncErrorLogger,
+
+ handleCompletion: function getAllAddons_handleCompletion(aReason) {
+ if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) {
+ logger.error("Error retrieving add-ons from database. Returning empty results");
+ aCallback({});
+ return;
+ }
+
+ getAllDevelopers();
+ }
+ });
+ }
+
+ // Retrieve all data from the developer table
+ function getAllDevelopers() {
+ self.getAsyncStatement("getAllDevelopers").executeAsync({
+ handleResult: function getAllDevelopers_handleResult(aResults) {
+ let row = null;
+ while ((row = aResults.getNextRow())) {
+ let addon_internal_id = row.getResultByName("addon_internal_id");
+ if (!(addon_internal_id in addons)) {
+ logger.warn("Found a developer not linked to an add-on in database");
+ continue;
+ }
+
+ let addon = addons[addon_internal_id];
+ if (!addon.developers)
+ addon.developers = [];
+
+ addon.developers.push(self._makeDeveloperFromAsyncRow(row));
+ }
+ },
+
+ handleError: self.asyncErrorLogger,
+
+ handleCompletion: function getAllDevelopers_handleCompletion(aReason) {
+ if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) {
+ logger.error("Error retrieving developers from database. Returning empty results");
+ aCallback({});
+ return;
+ }
+
+ getAllScreenshots();
+ }
+ });
+ }
+
+ // Retrieve all data from the screenshot table
+ function getAllScreenshots() {
+ self.getAsyncStatement("getAllScreenshots").executeAsync({
+ handleResult: function getAllScreenshots_handleResult(aResults) {
+ let row = null;
+ while ((row = aResults.getNextRow())) {
+ let addon_internal_id = row.getResultByName("addon_internal_id");
+ if (!(addon_internal_id in addons)) {
+ logger.warn("Found a screenshot not linked to an add-on in database");
+ continue;
+ }
+
+ let addon = addons[addon_internal_id];
+ if (!addon.screenshots)
+ addon.screenshots = [];
+ addon.screenshots.push(self._makeScreenshotFromAsyncRow(row));
+ }
+ },
+
+ handleError: self.asyncErrorLogger,
+
+ handleCompletion: function getAllScreenshots_handleCompletion(aReason) {
+ if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) {
+ logger.error("Error retrieving screenshots from database. Returning empty results");
+ aCallback({});
+ return;
+ }
+
+ getAllCompatOverrides();
+ }
+ });
+ }
+
+ function getAllCompatOverrides() {
+ self.getAsyncStatement("getAllCompatOverrides").executeAsync({
+ handleResult: function getAllCompatOverrides_handleResult(aResults) {
+ let row = null;
+ while ((row = aResults.getNextRow())) {
+ let addon_internal_id = row.getResultByName("addon_internal_id");
+ if (!(addon_internal_id in addons)) {
+ logger.warn("Found a compatibility override not linked to an add-on in database");
+ continue;
+ }
+
+ let addon = addons[addon_internal_id];
+ if (!addon.compatibilityOverrides)
+ addon.compatibilityOverrides = [];
+ addon.compatibilityOverrides.push(self._makeCompatOverrideFromAsyncRow(row));
+ }
+ },
+
+ handleError: self.asyncErrorLogger,
+
+ handleCompletion: function getAllCompatOverrides_handleCompletion(aReason) {
+ if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) {
+ logger.error("Error retrieving compatibility overrides from database. Returning empty results");
+ aCallback({});
+ return;
+ }
+
+ getAllIcons();
+ }
+ });
+ }
+
+ function getAllIcons() {
+ self.getAsyncStatement("getAllIcons").executeAsync({
+ handleResult: function getAllIcons_handleResult(aResults) {
+ let row = null;
+ while ((row = aResults.getNextRow())) {
+ let addon_internal_id = row.getResultByName("addon_internal_id");
+ if (!(addon_internal_id in addons)) {
+ logger.warn("Found an icon not linked to an add-on in database");
+ continue;
+ }
+
+ let addon = addons[addon_internal_id];
+ let { size, url } = self._makeIconFromAsyncRow(row);
+ addon.icons[size] = url;
+ if (size == 32)
+ addon.iconURL = url;
+ }
+ },
+
+ handleError: self.asyncErrorLogger,
+
+ handleCompletion: function getAllIcons_handleCompletion(aReason) {
+ if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) {
+ logger.error("Error retrieving icons from database. Returning empty results");
+ aCallback({});
+ return;
+ }
+
+ let returnedAddons = {};
+ for each (let addon in addons)
+ returnedAddons[addon.id] = addon;
+ aCallback(returnedAddons);
+ }
+ });
+ }
+
+ // Begin asynchronous process
+ getAllAddons();
+ },
+
+ // A cache of statements that are used and need to be finalized on shutdown
+ asyncStatementsCache: {},
+
+ /**
+ * Gets a cached async statement or creates a new statement if it doesn't
+ * already exist.
+ *
+ * @param aKey
+ * A unique key to reference the statement
+ * @return a mozIStorageAsyncStatement for the SQL corresponding to the
+ * unique key
+ */
+ getAsyncStatement: function AD_getAsyncStatement(aKey) {
+ if (aKey in this.asyncStatementsCache)
+ return this.asyncStatementsCache[aKey];
+
+ let sql = this.queries[aKey];
+ try {
+ return this.asyncStatementsCache[aKey] = this.connection.createAsyncStatement(sql);
+ } catch (e) {
+ logger.error("Error creating statement " + aKey + " (" + sql + ")");
+ throw Components.Exception("Error creating statement " + aKey + " (" + sql + "): " + e,
+ e.result);
+ }
+ },
+
+ // The queries used by the database
+ queries: {
+ getAllAddons: "SELECT internal_id, id, type, name, version, " +
+ "creator, creatorURL, description, fullDescription, " +
+ "developerComments, eula, homepageURL, supportURL, " +
+ "contributionURL, contributionAmount, averageRating, " +
+ "reviewCount, reviewURL, totalDownloads, weeklyDownloads, " +
+ "dailyUsers, sourceURI, repositoryStatus, size, updateDate " +
+ "FROM addon",
+
+ getAllDevelopers: "SELECT addon_internal_id, name, url FROM developer " +
+ "ORDER BY addon_internal_id, num",
+
+ getAllScreenshots: "SELECT addon_internal_id, url, width, height, " +
+ "thumbnailURL, thumbnailWidth, thumbnailHeight, caption " +
+ "FROM screenshot ORDER BY addon_internal_id, num",
+
+ getAllCompatOverrides: "SELECT addon_internal_id, type, minVersion, " +
+ "maxVersion, appID, appMinVersion, appMaxVersion " +
+ "FROM compatibility_override " +
+ "ORDER BY addon_internal_id, num",
+
+ getAllIcons: "SELECT addon_internal_id, size, url FROM icon " +
+ "ORDER BY addon_internal_id, size",
+ },
+
+ /**
+ * Make add-on structure from an asynchronous row.
+ *
+ * @param aRow
+ * The asynchronous row to use
+ * @return The created add-on
+ */
+ _makeAddonFromAsyncRow: function AD__makeAddonFromAsyncRow(aRow) {
+ // This is intentionally not an AddonSearchResult object in order
+ // to allow AddonDatabase._parseAddon to parse it, same as if it
+ // was read from the JSON database.
+
+ let addon = { icons: {} };
+
+ for (let prop of PROP_SINGLE) {
+ addon[prop] = aRow.getResultByName(prop)
+ };
+
+ return addon;
+ },
+
+ /**
+ * Make a developer from an asynchronous row
+ *
+ * @param aRow
+ * The asynchronous row to use
+ * @return The created developer
+ */
+ _makeDeveloperFromAsyncRow: function AD__makeDeveloperFromAsyncRow(aRow) {
+ let name = aRow.getResultByName("name");
+ let url = aRow.getResultByName("url")
+ return new AddonManagerPrivate.AddonAuthor(name, url);
+ },
+
+ /**
+ * Make a screenshot from an asynchronous row
+ *
+ * @param aRow
+ * The asynchronous row to use
+ * @return The created screenshot
+ */
+ _makeScreenshotFromAsyncRow: function AD__makeScreenshotFromAsyncRow(aRow) {
+ let url = aRow.getResultByName("url");
+ let width = aRow.getResultByName("width");
+ let height = aRow.getResultByName("height");
+ let thumbnailURL = aRow.getResultByName("thumbnailURL");
+ let thumbnailWidth = aRow.getResultByName("thumbnailWidth");
+ let thumbnailHeight = aRow.getResultByName("thumbnailHeight");
+ let caption = aRow.getResultByName("caption");
+ return new AddonManagerPrivate.AddonScreenshot(url, width, height, thumbnailURL,
+ thumbnailWidth, thumbnailHeight, caption);
+ },
+
+ /**
+ * Make a CompatibilityOverride from an asynchronous row
+ *
+ * @param aRow
+ * The asynchronous row to use
+ * @return The created CompatibilityOverride
+ */
+ _makeCompatOverrideFromAsyncRow: function AD_makeCompatOverrideFromAsyncRow(aRow) {
+ let type = aRow.getResultByName("type");
+ let minVersion = aRow.getResultByName("minVersion");
+ let maxVersion = aRow.getResultByName("maxVersion");
+ let appID = aRow.getResultByName("appID");
+ let appMinVersion = aRow.getResultByName("appMinVersion");
+ let appMaxVersion = aRow.getResultByName("appMaxVersion");
+ return new AddonManagerPrivate.AddonCompatibilityOverride(type,
+ minVersion,
+ maxVersion,
+ appID,
+ appMinVersion,
+ appMaxVersion);
+ },
+
+ /**
+ * Make an icon from an asynchronous row
+ *
+ * @param aRow
+ * The asynchronous row to use
+ * @return An object containing the size and URL of the icon
+ */
+ _makeIconFromAsyncRow: function AD_makeIconFromAsyncRow(aRow) {
+ let size = aRow.getResultByName("size");
+ let url = aRow.getResultByName("url");
+ return { size: size, url: url };
+ },
+
+ /**
+ * A helper function to log an SQL error.
+ *
+ * @param aError
+ * The storage error code associated with the error
+ * @param aErrorString
+ * An error message
+ */
+ logSQLError: function AD_logSQLError(aError, aErrorString) {
+ logger.error("SQL error " + aError + ": " + aErrorString);
+ },
+
+ /**
+ * A helper function to log any errors that occur during async statements.
+ *
+ * @param aError
+ * A mozIStorageError to log
+ */
+ asyncErrorLogger: function AD_asyncErrorLogger(aError) {
+ logger.error("Async SQL error " + aError.result + ": " + aError.message);
+ },
+
+ /**
+ * Synchronously creates the triggers in the database.
+ */
+ _createTriggers: function AD__createTriggers() {
+ this.connection.executeSimpleSQL("DROP TRIGGER IF EXISTS delete_addon");
+ this.connection.executeSimpleSQL("CREATE TRIGGER delete_addon AFTER DELETE " +
+ "ON addon BEGIN " +
+ "DELETE FROM developer WHERE addon_internal_id=old.internal_id; " +
+ "DELETE FROM screenshot WHERE addon_internal_id=old.internal_id; " +
+ "DELETE FROM compatibility_override WHERE addon_internal_id=old.internal_id; " +
+ "DELETE FROM icon WHERE addon_internal_id=old.internal_id; " +
+ "END");
+ },
+
+ /**
+ * Synchronously creates the indices in the database.
+ */
+ _createIndices: function AD__createIndices() {
+ this.connection.executeSimpleSQL("CREATE INDEX IF NOT EXISTS developer_idx " +
+ "ON developer (addon_internal_id)");
+ this.connection.executeSimpleSQL("CREATE INDEX IF NOT EXISTS screenshot_idx " +
+ "ON screenshot (addon_internal_id)");
+ this.connection.executeSimpleSQL("CREATE INDEX IF NOT EXISTS compatibility_override_idx " +
+ "ON compatibility_override (addon_internal_id)");
+ this.connection.executeSimpleSQL("CREATE INDEX IF NOT EXISTS icon_idx " +
+ "ON icon (addon_internal_id)");
+ }
+}
diff --git a/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm b/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm
new file mode 100644
index 000000000..7e86fceab
--- /dev/null
+++ b/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm
@@ -0,0 +1,772 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * The AddonUpdateChecker is responsible for retrieving the update information
+ * from an add-on's remote update manifest.
+ */
+
+"use strict";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+this.EXPORTED_SYMBOLS = [ "AddonUpdateChecker" ];
+
+const TIMEOUT = 60 * 1000;
+const PREFIX_NS_RDF = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+const PREFIX_NS_EM = "http://www.mozilla.org/2004/em-rdf#";
+const PREFIX_ITEM = "urn:mozilla:item:";
+const PREFIX_EXTENSION = "urn:mozilla:extension:";
+const PREFIX_THEME = "urn:mozilla:theme:";
+const TOOLKIT_ID = "toolkit@mozilla.org"
+#ifdef MOZ_PHOENIX_EXTENSIONS
+const FIREFOX_ID = "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"
+#endif
+const XMLURI_PARSE_ERROR = "http://www.mozilla.org/newlayout/xml/parsererror.xml"
+
+const PREF_UPDATE_REQUIREBUILTINCERTS = "extensions.update.requireBuiltInCerts";
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
+ "resource://gre/modules/AddonManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository",
+ "resource://gre/modules/addons/AddonRepository.jsm");
+
+// Shared code for suppressing bad cert dialogs.
+XPCOMUtils.defineLazyGetter(this, "CertUtils", function certUtilsLazyGetter() {
+ let certUtils = {};
+ Components.utils.import("resource://gre/modules/CertUtils.jsm", certUtils);
+ return certUtils;
+});
+
+var gRDF = Cc["@mozilla.org/rdf/rdf-service;1"].
+ getService(Ci.nsIRDFService);
+
+Cu.import("resource://gre/modules/Log.jsm");
+const LOGGER_ID = "addons.update-checker";
+
+// Create a new logger for use by the Addons Update Checker
+// (Requires AddonManager.jsm)
+let logger = Log.repository.getLogger(LOGGER_ID);
+
+/**
+ * A serialisation method for RDF data that produces an identical string
+ * for matching RDF graphs.
+ * The serialisation is not complete, only assertions stemming from a given
+ * resource are included, multiple references to the same resource are not
+ * permitted, and the RDF prolog and epilog are not included.
+ * RDF Blob and Date literals are not supported.
+ */
+function RDFSerializer() {
+ this.cUtils = Cc["@mozilla.org/rdf/container-utils;1"].
+ getService(Ci.nsIRDFContainerUtils);
+ this.resources = [];
+}
+
+RDFSerializer.prototype = {
+ INDENT: " ", // The indent used for pretty-printing
+ resources: null, // Array of the resources that have been found
+
+ /**
+ * Escapes characters from a string that should not appear in XML.
+ *
+ * @param aString
+ * The string to be escaped
+ * @return a string with all characters invalid in XML character data
+ * converted to entity references.
+ */
+ escapeEntities: function RDFS_escapeEntities(aString) {
+ aString = aString.replace(/&/g, "&amp;");
+ aString = aString.replace(/</g, "&lt;");
+ aString = aString.replace(/>/g, "&gt;");
+ return aString.replace(/"/g, "&quot;");
+ },
+
+ /**
+ * Serializes all the elements of an RDF container.
+ *
+ * @param aDs
+ * The RDF datasource
+ * @param aContainer
+ * The RDF container to output the child elements of
+ * @param aIndent
+ * The current level of indent for pretty-printing
+ * @return a string containing the serialized elements.
+ */
+ serializeContainerItems: function RDFS_serializeContainerItems(aDs, aContainer,
+ aIndent) {
+ var result = "";
+ var items = aContainer.GetElements();
+ while (items.hasMoreElements()) {
+ var item = items.getNext().QueryInterface(Ci.nsIRDFResource);
+ result += aIndent + "<RDF:li>\n"
+ result += this.serializeResource(aDs, item, aIndent + this.INDENT);
+ result += aIndent + "</RDF:li>\n"
+ }
+ return result;
+ },
+
+ /**
+ * Serializes all em:* (see EM_NS) properties of an RDF resource except for
+ * the em:signature property. As this serialization is to be compared against
+ * the manifest signature it cannot contain the em:signature property itself.
+ *
+ * @param aDs
+ * The RDF datasource
+ * @param aResource
+ * The RDF resource that contains the properties to serialize
+ * @param aIndent
+ * The current level of indent for pretty-printing
+ * @return a string containing the serialized properties.
+ * @throws if the resource contains a property that cannot be serialized
+ */
+ serializeResourceProperties: function RDFS_serializeResourceProperties(aDs,
+ aResource,
+ aIndent) {
+ var result = "";
+ var items = [];
+ var arcs = aDs.ArcLabelsOut(aResource);
+ while (arcs.hasMoreElements()) {
+ var arc = arcs.getNext().QueryInterface(Ci.nsIRDFResource);
+ if (arc.ValueUTF8.substring(0, PREFIX_NS_EM.length) != PREFIX_NS_EM)
+ continue;
+ var prop = arc.ValueUTF8.substring(PREFIX_NS_EM.length);
+ if (prop == "signature")
+ continue;
+
+ var targets = aDs.GetTargets(aResource, arc, true);
+ while (targets.hasMoreElements()) {
+ var target = targets.getNext();
+ if (target instanceof Ci.nsIRDFResource) {
+ var item = aIndent + "<em:" + prop + ">\n";
+ item += this.serializeResource(aDs, target, aIndent + this.INDENT);
+ item += aIndent + "</em:" + prop + ">\n";
+ items.push(item);
+ }
+ else if (target instanceof Ci.nsIRDFLiteral) {
+ items.push(aIndent + "<em:" + prop + ">" +
+ this.escapeEntities(target.Value) + "</em:" + prop + ">\n");
+ }
+ else if (target instanceof Ci.nsIRDFInt) {
+ items.push(aIndent + "<em:" + prop + " NC:parseType=\"Integer\">" +
+ target.Value + "</em:" + prop + ">\n");
+ }
+ else {
+ throw Components.Exception("Cannot serialize unknown literal type");
+ }
+ }
+ }
+ items.sort();
+ result += items.join("");
+ return result;
+ },
+
+ /**
+ * Recursively serializes an RDF resource and all resources it links to.
+ * This will only output EM_NS properties and will ignore any em:signature
+ * property.
+ *
+ * @param aDs
+ * The RDF datasource
+ * @param aResource
+ * The RDF resource to serialize
+ * @param aIndent
+ * The current level of indent for pretty-printing. If undefined no
+ * indent will be added
+ * @return a string containing the serialized resource.
+ * @throws if the RDF data contains multiple references to the same resource.
+ */
+ serializeResource: function RDFS_serializeResource(aDs, aResource, aIndent) {
+ if (this.resources.indexOf(aResource) != -1 ) {
+ // We cannot output multiple references to the same resource.
+ throw Components.Exception("Cannot serialize multiple references to " + aResource.Value);
+ }
+ if (aIndent === undefined)
+ aIndent = "";
+
+ this.resources.push(aResource);
+ var container = null;
+ var type = "Description";
+ if (this.cUtils.IsSeq(aDs, aResource)) {
+ type = "Seq";
+ container = this.cUtils.MakeSeq(aDs, aResource);
+ }
+ else if (this.cUtils.IsAlt(aDs, aResource)) {
+ type = "Alt";
+ container = this.cUtils.MakeAlt(aDs, aResource);
+ }
+ else if (this.cUtils.IsBag(aDs, aResource)) {
+ type = "Bag";
+ container = this.cUtils.MakeBag(aDs, aResource);
+ }
+
+ var result = aIndent + "<RDF:" + type;
+ if (!gRDF.IsAnonymousResource(aResource))
+ result += " about=\"" + this.escapeEntities(aResource.ValueUTF8) + "\"";
+ result += ">\n";
+
+ if (container)
+ result += this.serializeContainerItems(aDs, container, aIndent + this.INDENT);
+
+ result += this.serializeResourceProperties(aDs, aResource, aIndent + this.INDENT);
+
+ result += aIndent + "</RDF:" + type + ">\n";
+ return result;
+ }
+}
+
+/**
+ * Parses an RDF style update manifest into an array of update objects.
+ *
+ * @param aId
+ * The ID of the add-on being checked for updates
+ * @param aUpdateKey
+ * An optional update key for the add-on
+ * @param aRequest
+ * The XMLHttpRequest that has retrieved the update manifest
+ * @return an array of update objects
+ * @throws if the update manifest is invalid in any way
+ */
+function parseRDFManifest(aId, aUpdateKey, aRequest) {
+ function EM_R(aProp) {
+ return gRDF.GetResource(PREFIX_NS_EM + aProp);
+ }
+
+ function getValue(aLiteral) {
+ if (aLiteral instanceof Ci.nsIRDFLiteral)
+ return aLiteral.Value;
+ if (aLiteral instanceof Ci.nsIRDFResource)
+ return aLiteral.Value;
+ if (aLiteral instanceof Ci.nsIRDFInt)
+ return aLiteral.Value;
+ return null;
+ }
+
+ function getProperty(aDs, aSource, aProperty) {
+ return getValue(aDs.GetTarget(aSource, EM_R(aProperty), true));
+ }
+
+ function getBooleanProperty(aDs, aSource, aProperty) {
+ let propValue = aDs.GetTarget(aSource, EM_R(aProperty), true);
+ if (!propValue)
+ return undefined;
+ return getValue(propValue) == "true";
+ }
+
+ function getRequiredProperty(aDs, aSource, aProperty) {
+ let value = getProperty(aDs, aSource, aProperty);
+ if (!value)
+ throw Components.Exception("Update manifest is missing a required " + aProperty + " property.");
+ return value;
+ }
+
+ let rdfParser = Cc["@mozilla.org/rdf/xml-parser;1"].
+ createInstance(Ci.nsIRDFXMLParser);
+ let ds = Cc["@mozilla.org/rdf/datasource;1?name=in-memory-datasource"].
+ createInstance(Ci.nsIRDFDataSource);
+ rdfParser.parseString(ds, aRequest.channel.URI, aRequest.responseText);
+
+ // Differentiating between add-on types is deprecated
+ let extensionRes = gRDF.GetResource(PREFIX_EXTENSION + aId);
+ let themeRes = gRDF.GetResource(PREFIX_THEME + aId);
+ let itemRes = gRDF.GetResource(PREFIX_ITEM + aId);
+ let addonRes = ds.ArcLabelsOut(extensionRes).hasMoreElements() ? extensionRes
+ : ds.ArcLabelsOut(themeRes).hasMoreElements() ? themeRes
+ : itemRes;
+
+ // If we have an update key then the update manifest must be signed
+ if (aUpdateKey) {
+ let signature = getProperty(ds, addonRes, "signature");
+ if (!signature)
+ throw Components.Exception("Update manifest for " + aId + " does not contain a required signature");
+ let serializer = new RDFSerializer();
+ let updateString = null;
+
+ try {
+ updateString = serializer.serializeResource(ds, addonRes);
+ }
+ catch (e) {
+ throw Components.Exception("Failed to generate signed string for " + aId + ". Serializer threw " + e,
+ e.result);
+ }
+
+ let result = false;
+
+ try {
+ let verifier = Cc["@mozilla.org/security/datasignatureverifier;1"].
+ getService(Ci.nsIDataSignatureVerifier);
+ result = verifier.verifyData(updateString, signature, aUpdateKey);
+ }
+ catch (e) {
+ throw Components.Exception("The signature or updateKey for " + aId + " is malformed." +
+ "Verifier threw " + e, e.result);
+ }
+
+ if (!result)
+ throw Components.Exception("The signature for " + aId + " was not created by the add-on's updateKey");
+ }
+
+ let updates = ds.GetTarget(addonRes, EM_R("updates"), true);
+
+ // A missing updates property doesn't count as a failure, just as no avialable
+ // update information
+ if (!updates) {
+ logger.warn("Update manifest for " + aId + " did not contain an updates property");
+ return [];
+ }
+
+ if (!(updates instanceof Ci.nsIRDFResource))
+ throw Components.Exception("Missing updates property for " + addonRes.Value);
+
+ let cu = Cc["@mozilla.org/rdf/container-utils;1"].
+ getService(Ci.nsIRDFContainerUtils);
+ if (!cu.IsContainer(ds, updates))
+ throw Components.Exception("Updates property was not an RDF container");
+
+ let results = [];
+ let ctr = Cc["@mozilla.org/rdf/container;1"].
+ createInstance(Ci.nsIRDFContainer);
+ ctr.Init(ds, updates);
+ let items = ctr.GetElements();
+ while (items.hasMoreElements()) {
+ let item = items.getNext().QueryInterface(Ci.nsIRDFResource);
+ let version = getProperty(ds, item, "version");
+ if (!version) {
+ logger.warn("Update manifest is missing a required version property.");
+ continue;
+ }
+
+ logger.debug("Found an update entry for " + aId + " version " + version);
+
+ let targetApps = ds.GetTargets(item, EM_R("targetApplication"), true);
+ while (targetApps.hasMoreElements()) {
+ let targetApp = targetApps.getNext().QueryInterface(Ci.nsIRDFResource);
+
+ let appEntry = {};
+ try {
+ appEntry.id = getRequiredProperty(ds, targetApp, "id");
+ appEntry.minVersion = getRequiredProperty(ds, targetApp, "minVersion");
+ appEntry.maxVersion = getRequiredProperty(ds, targetApp, "maxVersion");
+ }
+ catch (e) {
+ logger.warn(e);
+ continue;
+ }
+
+ let result = {
+ id: aId,
+ version: version,
+ multiprocessCompatible: getBooleanProperty(ds, item, "multiprocessCompatible"),
+ updateURL: getProperty(ds, targetApp, "updateLink"),
+ updateHash: getProperty(ds, targetApp, "updateHash"),
+ updateInfoURL: getProperty(ds, targetApp, "updateInfoURL"),
+ strictCompatibility: !!getBooleanProperty(ds, targetApp, "strictCompatibility"),
+ targetApplications: [appEntry]
+ };
+
+ if (result.updateURL && AddonManager.checkUpdateSecurity &&
+ result.updateURL.substring(0, 6) != "https:" &&
+ (!result.updateHash || result.updateHash.substring(0, 3) != "sha")) {
+ logger.warn("updateLink " + result.updateURL + " is not secure and is not verified" +
+ " by a strong enough hash (needs to be sha1 or stronger).");
+ delete result.updateURL;
+ delete result.updateHash;
+ }
+ results.push(result);
+ }
+ }
+ return results;
+}
+
+/**
+ * Starts downloading an update manifest and then passes it to an appropriate
+ * parser to convert to an array of update objects
+ *
+ * @param aId
+ * The ID of the add-on being checked for updates
+ * @param aUpdateKey
+ * An optional update key for the add-on
+ * @param aUrl
+ * The URL of the update manifest
+ * @param aObserver
+ * An observer to pass results to
+ */
+function UpdateParser(aId, aUpdateKey, aUrl, aObserver) {
+ this.id = aId;
+ this.updateKey = aUpdateKey;
+ this.observer = aObserver;
+ this.url = aUrl;
+
+ let requireBuiltIn = true;
+ try {
+ requireBuiltIn = Services.prefs.getBoolPref(PREF_UPDATE_REQUIREBUILTINCERTS);
+ }
+ catch (e) {
+ }
+
+ logger.debug("Requesting " + aUrl);
+ try {
+ this.request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
+ createInstance(Ci.nsIXMLHttpRequest);
+ this.request.open("GET", this.url, true);
+ this.request.channel.notificationCallbacks = new CertUtils.BadCertHandler(!requireBuiltIn);
+ this.request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ // Prevent the request from writing to cache.
+ this.request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
+ this.request.overrideMimeType("text/xml");
+ this.request.setRequestHeader("Moz-XPI-Update", "1", true);
+ this.request.timeout = TIMEOUT;
+ var self = this;
+ this.request.addEventListener("load", function loadEventListener(event) { self.onLoad() }, false);
+ this.request.addEventListener("error", function errorEventListener(event) { self.onError() }, false);
+ this.request.addEventListener("timeout", function timeoutEventListener(event) { self.onTimeout() }, false);
+ this.request.send(null);
+ }
+ catch (e) {
+ logger.error("Failed to request update manifest", e);
+ }
+}
+
+UpdateParser.prototype = {
+ id: null,
+ updateKey: null,
+ observer: null,
+ request: null,
+ url: null,
+
+ /**
+ * Called when the manifest has been successfully loaded.
+ */
+ onLoad: function UP_onLoad() {
+ let request = this.request;
+ this.request = null;
+ this._doneAt = new Error("place holder");
+
+ let requireBuiltIn = true;
+ try {
+ requireBuiltIn = Services.prefs.getBoolPref(PREF_UPDATE_REQUIREBUILTINCERTS);
+ }
+ catch (e) {
+ }
+
+ try {
+ CertUtils.checkCert(request.channel, !requireBuiltIn);
+ }
+ catch (e) {
+ this.notifyError(AddonUpdateChecker.ERROR_DOWNLOAD_ERROR);
+ return;
+ }
+
+ if (!Components.isSuccessCode(request.status)) {
+ logger.warn("Request failed: " + this.url + " - " + request.status);
+ this.notifyError(AddonUpdateChecker.ERROR_DOWNLOAD_ERROR);
+ return;
+ }
+
+ let channel = request.channel;
+ if (channel instanceof Ci.nsIHttpChannel && !channel.requestSucceeded) {
+ logger.warn("Request failed: " + this.url + " - " + channel.responseStatus +
+ ": " + channel.responseStatusText);
+ this.notifyError(AddonUpdateChecker.ERROR_DOWNLOAD_ERROR);
+ return;
+ }
+
+ let xml = request.responseXML;
+ if (!xml || xml.documentElement.namespaceURI == XMLURI_PARSE_ERROR) {
+ logger.warn("Update manifest was not valid XML");
+ this.notifyError(AddonUpdateChecker.ERROR_PARSE_ERROR);
+ return;
+ }
+
+ // We currently only know about RDF update manifests
+ if (xml.documentElement.namespaceURI == PREFIX_NS_RDF) {
+ let results = null;
+
+ try {
+ results = parseRDFManifest(this.id, this.updateKey, request);
+ }
+ catch (e) {
+ logger.warn("onUpdateCheckComplete failed to parse RDF manifest", e);
+ this.notifyError(AddonUpdateChecker.ERROR_PARSE_ERROR);
+ return;
+ }
+ if ("onUpdateCheckComplete" in this.observer) {
+ try {
+ this.observer.onUpdateCheckComplete(results);
+ }
+ catch (e) {
+ logger.warn("onUpdateCheckComplete notification failed", e);
+ }
+ }
+ else {
+ logger.warn("onUpdateCheckComplete may not properly cancel", new Error("stack marker"));
+ }
+ return;
+ }
+
+ logger.warn("Update manifest had an unrecognised namespace: " + xml.documentElement.namespaceURI);
+ this.notifyError(AddonUpdateChecker.ERROR_UNKNOWN_FORMAT);
+ },
+
+ /**
+ * Called when the request times out
+ */
+ onTimeout: function() {
+ this.request = null;
+ this._doneAt = new Error("Timed out");
+ logger.warn("Request for " + this.url + " timed out");
+ this.notifyError(AddonUpdateChecker.ERROR_TIMEOUT);
+ },
+
+ /**
+ * Called when the manifest failed to load.
+ */
+ onError: function UP_onError() {
+ if (!Components.isSuccessCode(this.request.status)) {
+ logger.warn("Request failed: " + this.url + " - " + this.request.status);
+ }
+ else if (this.request.channel instanceof Ci.nsIHttpChannel) {
+ try {
+ if (this.request.channel.requestSucceeded) {
+ logger.warn("Request failed: " + this.url + " - " +
+ this.request.channel.responseStatus + ": " +
+ this.request.channel.responseStatusText);
+ }
+ }
+ catch (e) {
+ logger.warn("HTTP Request failed for an unknown reason");
+ }
+ }
+ else {
+ logger.warn("Request failed for an unknown reason");
+ }
+
+ this.request = null;
+ this._doneAt = new Error("UP_onError");
+
+ this.notifyError(AddonUpdateChecker.ERROR_DOWNLOAD_ERROR);
+ },
+
+ /**
+ * Helper method to notify the observer that an error occured.
+ */
+ notifyError: function UP_notifyError(aStatus) {
+ if ("onUpdateCheckError" in this.observer) {
+ try {
+ this.observer.onUpdateCheckError(aStatus);
+ }
+ catch (e) {
+ logger.warn("onUpdateCheckError notification failed", e);
+ }
+ }
+ },
+
+ /**
+ * Called to cancel an in-progress update check.
+ */
+ cancel: function UP_cancel() {
+ if (!this.request) {
+ logger.error("Trying to cancel already-complete request", this._doneAt);
+ return;
+ }
+ this.request.abort();
+ this.request = null;
+ this._doneAt = new Error("UP_cancel");
+ this.notifyError(AddonUpdateChecker.ERROR_CANCELLED);
+ }
+};
+
+/**
+ * Tests if an update matches a version of the application or platform
+ *
+ * @param aUpdate
+ * The available update
+ * @param aAppVersion
+ * The application version to use
+ * @param aPlatformVersion
+ * The platform version to use
+ * @param aIgnoreMaxVersion
+ * Ignore maxVersion when testing if an update matches. Optional.
+ * @param aIgnoreStrictCompat
+ * Ignore strictCompatibility when testing if an update matches. Optional.
+ * @param aCompatOverrides
+ * AddonCompatibilityOverride objects to match against. Optional.
+ * @return true if the update is compatible with the application/platform
+ */
+function matchesVersions(aUpdate, aAppVersion, aPlatformVersion,
+ aIgnoreMaxVersion, aIgnoreStrictCompat,
+ aCompatOverrides) {
+ if (aCompatOverrides) {
+ let override = AddonRepository.findMatchingCompatOverride(aUpdate.version,
+ aCompatOverrides,
+ aAppVersion,
+ aPlatformVersion);
+ if (override && override.type == "incompatible")
+ return false;
+ }
+
+ if (aUpdate.strictCompatibility && !aIgnoreStrictCompat)
+ aIgnoreMaxVersion = false;
+
+ let result = false;
+ for (let app of aUpdate.targetApplications) {
+ if (app.id == Services.appinfo.ID) {
+ return (Services.vc.compare(aAppVersion, app.minVersion) >= 0) &&
+ (aIgnoreMaxVersion || (Services.vc.compare(aAppVersion, app.maxVersion) <= 0));
+ }
+#ifdef MOZ_PHOENIX_EXTENSIONS
+ if (app.id == FIREFOX_ID) {
+ return (Services.vc.compare(aAppVersion, app.minVersion) >= 0) &&
+ (aIgnoreMaxVersion || (Services.vc.compare(aAppVersion, app.maxVersion) <= 0));
+ }
+#endif
+ if (app.id == TOOLKIT_ID) {
+ result = (Services.vc.compare(aPlatformVersion, app.minVersion) >= 0) &&
+ (aIgnoreMaxVersion || (Services.vc.compare(aPlatformVersion, app.maxVersion) <= 0));
+ }
+ }
+ return result;
+}
+
+this.AddonUpdateChecker = {
+ // These must be kept in sync with AddonManager
+ // The update check timed out
+ ERROR_TIMEOUT: -1,
+ // There was an error while downloading the update information.
+ ERROR_DOWNLOAD_ERROR: -2,
+ // The update information was malformed in some way.
+ ERROR_PARSE_ERROR: -3,
+ // The update information was not in any known format.
+ ERROR_UNKNOWN_FORMAT: -4,
+ // The update information was not correctly signed or there was an SSL error.
+ ERROR_SECURITY_ERROR: -5,
+ // The update was cancelled
+ ERROR_CANCELLED: -6,
+
+ /**
+ * Retrieves the best matching compatibility update for the application from
+ * a list of available update objects.
+ *
+ * @param aUpdates
+ * An array of update objects
+ * @param aVersion
+ * The version of the add-on to get new compatibility information for
+ * @param aIgnoreCompatibility
+ * An optional parameter to get the first compatibility update that
+ * is compatible with any version of the application or toolkit
+ * @param aAppVersion
+ * The version of the application or null to use the current version
+ * @param aPlatformVersion
+ * The version of the platform or null to use the current version
+ * @param aIgnoreMaxVersion
+ * Ignore maxVersion when testing if an update matches. Optional.
+ * @param aIgnoreStrictCompat
+ * Ignore strictCompatibility when testing if an update matches. Optional.
+ * @return an update object if one matches or null if not
+ */
+ getCompatibilityUpdate: function AUC_getCompatibilityUpdate(aUpdates, aVersion,
+ aIgnoreCompatibility,
+ aAppVersion,
+ aPlatformVersion,
+ aIgnoreMaxVersion,
+ aIgnoreStrictCompat) {
+ if (!aAppVersion)
+ aAppVersion = Services.appinfo.version;
+ if (!aPlatformVersion)
+ aPlatformVersion = Services.appinfo.platformVersion;
+
+ for (let update of aUpdates) {
+ if (Services.vc.compare(update.version, aVersion) == 0) {
+ if (aIgnoreCompatibility) {
+ for (let targetApp of update.targetApplications) {
+ let id = targetApp.id;
+#ifdef MOZ_PHOENIX_EXTENSIONS
+ if (id == Services.appinfo.ID || id == FIREFOX_ID ||
+ id == TOOLKIT_ID)
+#else
+ if (id == Services.appinfo.ID || id == TOOLKIT_ID)
+#endif
+ return update;
+ }
+ }
+ else if (matchesVersions(update, aAppVersion, aPlatformVersion,
+ aIgnoreMaxVersion, aIgnoreStrictCompat)) {
+ return update;
+ }
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Returns the newest available update from a list of update objects.
+ *
+ * @param aUpdates
+ * An array of update objects
+ * @param aAppVersion
+ * The version of the application or null to use the current version
+ * @param aPlatformVersion
+ * The version of the platform or null to use the current version
+ * @param aIgnoreMaxVersion
+ * When determining compatible updates, ignore maxVersion. Optional.
+ * @param aIgnoreStrictCompat
+ * When determining compatible updates, ignore strictCompatibility. Optional.
+ * @param aCompatOverrides
+ * Array of AddonCompatibilityOverride to take into account. Optional.
+ * @return an update object if one matches or null if not
+ */
+ getNewestCompatibleUpdate: function AUC_getNewestCompatibleUpdate(aUpdates,
+ aAppVersion,
+ aPlatformVersion,
+ aIgnoreMaxVersion,
+ aIgnoreStrictCompat,
+ aCompatOverrides) {
+ if (!aAppVersion)
+ aAppVersion = Services.appinfo.version;
+ if (!aPlatformVersion)
+ aPlatformVersion = Services.appinfo.platformVersion;
+
+ let blocklist = Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(Ci.nsIBlocklistService);
+
+ let newest = null;
+ for (let update of aUpdates) {
+ if (!update.updateURL)
+ continue;
+ let state = blocklist.getAddonBlocklistState(update, aAppVersion, aPlatformVersion);
+ if (state != Ci.nsIBlocklistService.STATE_NOT_BLOCKED)
+ continue;
+ if ((newest == null || (Services.vc.compare(newest.version, update.version) < 0)) &&
+ matchesVersions(update, aAppVersion, aPlatformVersion,
+ aIgnoreMaxVersion, aIgnoreStrictCompat,
+ aCompatOverrides)) {
+ newest = update;
+ }
+ }
+ return newest;
+ },
+
+ /**
+ * Starts an update check.
+ *
+ * @param aId
+ * The ID of the add-on being checked for updates
+ * @param aUpdateKey
+ * An optional update key for the add-on
+ * @param aUrl
+ * The URL of the add-on's update manifest
+ * @param aObserver
+ * An observer to notify of results
+ * @return UpdateParser so that the caller can use UpdateParser.cancel() to shut
+ * down in-progress update requests
+ */
+ checkForUpdates: function AUC_checkForUpdates(aId, aUpdateKey, aUrl,
+ aObserver) {
+ return new UpdateParser(aId, aUpdateKey, aUrl, aObserver);
+ }
+};
diff --git a/toolkit/mozapps/extensions/internal/Content.js b/toolkit/mozapps/extensions/internal/Content.js
new file mode 100644
index 000000000..29c0ed8ce
--- /dev/null
+++ b/toolkit/mozapps/extensions/internal/Content.js
@@ -0,0 +1,31 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+(function() {
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
+let nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile",
+ "initWithPath");
+
+const MSG_JAR_FLUSH = "AddonJarFlush";
+
+
+try {
+ if (Services.appinfo.processType !== Services.appinfo.PROCESS_TYPE_DEFAULT) {
+ // Propagate JAR cache flush notifications across process boundaries.
+ addMessageListener(MSG_JAR_FLUSH, function jar_flushMessageListener(message) {
+ let file = new nsIFile(message.data);
+ Services.obs.notifyObservers(file, "flush-cache-entry", null);
+ });
+ }
+} catch(e) {
+ Cu.reportError(e);
+}
+
+})();
diff --git a/toolkit/mozapps/extensions/internal/GMPProvider.jsm b/toolkit/mozapps/extensions/internal/GMPProvider.jsm
new file mode 100644
index 000000000..a55457f6e
--- /dev/null
+++ b/toolkit/mozapps/extensions/internal/GMPProvider.jsm
@@ -0,0 +1,614 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+this.EXPORTED_SYMBOLS = [];
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/AddonManager.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/GMPUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(
+ this, "GMPInstallManager", "resource://gre/modules/GMPInstallManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(
+ this, "setTimeout", "resource://gre/modules/Timer.jsm");
+
+const URI_EXTENSION_STRINGS = "chrome://mozapps/locale/extensions/extensions.properties";
+const STRING_TYPE_NAME = "type.%ID%.name";
+
+const SEC_IN_A_DAY = 24 * 60 * 60;
+// How long to wait after a user enabled EME before attempting to download CDMs.
+const GMP_CHECK_DELAY = 10 * 1000; // milliseconds
+
+const NS_GRE_DIR = "GreD";
+const CLEARKEY_PLUGIN_ID = "gmp-clearkey";
+const CLEARKEY_VERSION = "0.1";
+
+const GMP_LICENSE_INFO = "gmp_license_info";
+
+const GMP_PLUGINS = [
+ {
+ id: OPEN_H264_ID,
+ name: "openH264_name",
+ description: "openH264_description2",
+ // The following licenseURL is part of an awful hack to include the OpenH264
+ // license without having bug 624602 fixed yet, and intentionally ignores
+ // localisation.
+ licenseURL: "chrome://mozapps/content/extensions/OpenH264-license.txt",
+ homepageURL: "http://www.openh264.org/",
+ optionsURL: "chrome://mozapps/content/extensions/gmpPrefs.xul"
+ },
+ {
+ id: EME_ADOBE_ID,
+ name: "eme-adobe_name",
+ description: "eme-adobe_description",
+ licenseURL: "http://help.adobe.com/en_US/primetime/drm/HTML5_CDM_EULA/index.html",
+ homepageURL: "http://help.adobe.com/en_US/primetime/drm/HTML5_CDM",
+ optionsURL: "chrome://mozapps/content/extensions/gmpPrefs.xul",
+ isEME: true
+ }];
+
+XPCOMUtils.defineLazyGetter(this, "pluginsBundle",
+ () => Services.strings.createBundle("chrome://global/locale/plugins.properties"));
+XPCOMUtils.defineLazyGetter(this, "gmpService",
+ () => Cc["@mozilla.org/gecko-media-plugin-service;1"].getService(Ci.mozIGeckoMediaPluginChromeService));
+
+let messageManager = Cc["@mozilla.org/globalmessagemanager;1"]
+ .getService(Ci.nsIMessageListenerManager);
+
+let gLogger;
+let gLogAppenderDump = null;
+
+function configureLogging() {
+ if (!gLogger) {
+ gLogger = Log.repository.getLogger("Toolkit.GMP");
+ gLogger.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter()));
+ }
+ gLogger.level = GMPPrefs.get(GMPPrefs.KEY_LOGGING_LEVEL, Log.Level.Warn);
+
+ let logDumping = GMPPrefs.get(GMPPrefs.KEY_LOGGING_DUMP, false);
+ if (logDumping != !!gLogAppenderDump) {
+ if (logDumping) {
+ gLogAppenderDump = new Log.DumpAppender(new Log.BasicFormatter());
+ gLogger.addAppender(gLogAppenderDump);
+ } else {
+ gLogger.removeAppender(gLogAppenderDump);
+ gLogAppenderDump = null;
+ }
+ }
+}
+
+
+
+/**
+ * The GMPWrapper provides the info for the various GMP plugins to public
+ * callers through the API.
+ */
+function GMPWrapper(aPluginInfo) {
+ this._plugin = aPluginInfo;
+ this._log =
+ Log.repository.getLoggerWithMessagePrefix("Toolkit.GMP",
+ "GMPWrapper(" +
+ this._plugin.id + ") ");
+ Preferences.observe(GMPPrefs.getPrefKey(GMPPrefs.KEY_PLUGIN_ENABLED,
+ this._plugin.id),
+ this.onPrefEnabledChanged, this);
+ Preferences.observe(GMPPrefs.getPrefKey(GMPPrefs.KEY_PLUGIN_VERSION,
+ this._plugin.id),
+ this.onPrefVersionChanged, this);
+ if (this._plugin.isEME) {
+ Preferences.observe(GMPPrefs.KEY_EME_ENABLED,
+ this.onPrefEMEGlobalEnabledChanged, this);
+ messageManager.addMessageListener("EMEVideo:ContentMediaKeysRequest", this);
+ }
+}
+
+GMPWrapper.prototype = {
+ // An active task that checks for plugin updates and installs them.
+ _updateTask: null,
+ _gmpPath: null,
+ _isUpdateCheckPending: false,
+
+ optionsType: AddonManager.OPTIONS_TYPE_INLINE,
+ get optionsURL() { return this._plugin.optionsURL; },
+
+ set gmpPath(aPath) { this._gmpPath = aPath; },
+ get gmpPath() {
+ if (!this._gmpPath && this.isInstalled) {
+ this._gmpPath = OS.Path.join(OS.Constants.Path.profileDir,
+ this._plugin.id,
+ GMPPrefs.get(GMPPrefs.KEY_PLUGIN_VERSION,
+ null, this._plugin.id));
+ }
+ return this._gmpPath;
+ },
+
+ get id() { return this._plugin.id; },
+ get type() { return "plugin"; },
+ get isGMPlugin() { return true; },
+ get name() { return this._plugin.name; },
+ get creator() { return null; },
+ get homepageURL() { return this._plugin.homepageURL; },
+
+ get description() { return this._plugin.description; },
+ get fullDescription() { return this._plugin.fullDescription; },
+
+ get version() { return GMPPrefs.get(GMPPrefs.KEY_PLUGIN_VERSION, null,
+ this._plugin.id); },
+
+ get isActive() { return !this.appDisabled && !this.userDisabled; },
+ get appDisabled() {
+ if (this._plugin.isEME && !GMPPrefs.get(GMPPrefs.KEY_EME_ENABLED, true)) {
+ // If "media.eme.enabled" is false, all EME plugins are disabled.
+ return true;
+ }
+ return false;
+ },
+
+ get userDisabled() {
+ return !GMPPrefs.get(GMPPrefs.KEY_PLUGIN_ENABLED, true, this._plugin.id);
+ },
+ set userDisabled(aVal) { GMPPrefs.set(GMPPrefs.KEY_PLUGIN_ENABLED,
+ aVal === false,
+ this._plugin.id); },
+
+ get blocklistState() { return Ci.nsIBlocklistService.STATE_NOT_BLOCKED; },
+ get size() { return 0; },
+ get scope() { return AddonManager.SCOPE_APPLICATION; },
+ get pendingOperations() { return AddonManager.PENDING_NONE; },
+
+ get operationsRequiringRestart() { return AddonManager.OP_NEEDS_RESTART_NONE },
+
+ get permissions() {
+ let permissions = 0;
+ if (!this.appDisabled) {
+ permissions |= AddonManager.PERM_CAN_UPGRADE;
+ permissions |= this.userDisabled ? AddonManager.PERM_CAN_ENABLE :
+ AddonManager.PERM_CAN_DISABLE;
+ }
+ return permissions;
+ },
+
+ get updateDate() {
+ let time = Number(GMPPrefs.get(GMPPrefs.KEY_PLUGIN_LAST_UPDATE, null,
+ this._plugin.id));
+ if (time !== NaN && this.isInstalled) {
+ return new Date(time * 1000)
+ }
+ return null;
+ },
+
+ get isCompatible() {
+ return true;
+ },
+
+ get isPlatformCompatible() {
+ return true;
+ },
+
+ get providesUpdatesSecurely() {
+ return true;
+ },
+
+ get foreignInstall() {
+ return false;
+ },
+
+ isCompatibleWith: function(aAppVersion, aPlatformVersion) {
+ return true;
+ },
+
+ get applyBackgroundUpdates() {
+ if (!GMPPrefs.isSet(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, this._plugin.id)) {
+ return AddonManager.AUTOUPDATE_DEFAULT;
+ }
+
+ return GMPPrefs.get(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, true, this._plugin.id) ?
+ AddonManager.AUTOUPDATE_ENABLE : AddonManager.AUTOUPDATE_DISABLE;
+ },
+
+ set applyBackgroundUpdates(aVal) {
+ if (aVal == AddonManager.AUTOUPDATE_DEFAULT) {
+ GMPPrefs.reset(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, this._plugin.id);
+ } else if (aVal == AddonManager.AUTOUPDATE_ENABLE) {
+ GMPPrefs.set(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, true, this._plugin.id);
+ } else if (aVal == AddonManager.AUTOUPDATE_DISABLE) {
+ GMPPrefs.set(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, false, this._plugin.id);
+ }
+ },
+
+ findUpdates: function(aListener, aReason, aAppVersion, aPlatformVersion) {
+ this._log.trace("findUpdates() - " + this._plugin.id + " - reason=" +
+ aReason);
+
+ AddonManagerPrivate.callNoUpdateListeners(this, aListener);
+
+ if (aReason === AddonManager.UPDATE_WHEN_PERIODIC_UPDATE) {
+ if (!AddonManager.shouldAutoUpdate(this)) {
+ this._log.trace("findUpdates() - " + this._plugin.id +
+ " - no autoupdate");
+ return Promise.resolve(false);
+ }
+
+ let secSinceLastCheck =
+ Date.now() / 1000 - Preferences.get(GMPPrefs.KEY_UPDATE_LAST_CHECK, 0);
+ if (secSinceLastCheck <= SEC_IN_A_DAY) {
+ this._log.trace("findUpdates() - " + this._plugin.id +
+ " - last check was less then a day ago");
+ return Promise.resolve(false);
+ }
+ } else if (aReason !== AddonManager.UPDATE_WHEN_USER_REQUESTED) {
+ this._log.trace("findUpdates() - " + this._plugin.id +
+ " - the given reason to update is not supported");
+ return Promise.resolve(false);
+ }
+
+ if (this._updateTask !== null) {
+ this._log.trace("findUpdates() - " + this._plugin.id +
+ " - update task already running");
+ return this._updateTask;
+ }
+
+ this._updateTask = Task.spawn(function* GMPProvider_updateTask() {
+ this._log.trace("findUpdates() - updateTask");
+ try {
+ let installManager = new GMPInstallManager();
+ let gmpAddons = yield installManager.checkForAddons();
+ let update = gmpAddons.find(function(aAddon) {
+ return aAddon.id === this._plugin.id;
+ }, this);
+ if (update && update.isValid && !update.isInstalled) {
+ this._log.trace("findUpdates() - found update for " +
+ this._plugin.id + ", installing");
+ yield installManager.installAddon(update);
+ } else {
+ this._log.trace("findUpdates() - no updates for " + this._plugin.id);
+ }
+ this._log.info("findUpdates() - updateTask succeeded for " +
+ this._plugin.id);
+ } catch (e) {
+ this._log.error("findUpdates() - updateTask for " + this._plugin.id +
+ " threw", e);
+ throw e;
+ } finally {
+ this._updateTask = null;
+ return true;
+ }
+ }.bind(this));
+
+ return this._updateTask;
+ },
+
+ get pluginMimeTypes() { return []; },
+ get pluginLibraries() {
+ if (this.isInstalled) {
+ let path = this.version;
+ return [path];
+ }
+ return [];
+ },
+ get pluginFullpath() {
+ if (this.isInstalled) {
+ let path = OS.Path.join(OS.Constants.Path.profileDir,
+ this._plugin.id,
+ this.version);
+ return [path];
+ }
+ return [];
+ },
+
+ get isInstalled() {
+ return this.version && this.version.length > 0;
+ },
+
+ _handleEnabledChanged: function() {
+ AddonManagerPrivate.callAddonListeners(this.isActive ?
+ "onEnabling" : "onDisabling",
+ this, false);
+ if (this._gmpPath) {
+ if (this.isActive) {
+ this._log.info("onPrefEnabledChanged() - adding gmp directory " +
+ this._gmpPath);
+ gmpService.addPluginDirectory(this._gmpPath);
+ } else {
+ this._log.info("onPrefEnabledChanged() - removing gmp directory " +
+ this._gmpPath);
+ gmpService.removePluginDirectory(this._gmpPath);
+ }
+ }
+ AddonManagerPrivate.callAddonListeners(this.isActive ?
+ "onEnabled" : "onDisabled",
+ this);
+ },
+
+ onPrefEMEGlobalEnabledChanged: function() {
+ AddonManagerPrivate.callAddonListeners("onPropertyChanged", this,
+ ["appDisabled"]);
+ if (this.appDisabled) {
+ this.uninstallPlugin();
+ } else {
+ AddonManagerPrivate.callInstallListeners("onExternalInstall", null, this,
+ null, false);
+ AddonManagerPrivate.callAddonListeners("onInstalling", this, false);
+ AddonManagerPrivate.callAddonListeners("onInstalled", this);
+ this.checkForUpdates(GMP_CHECK_DELAY);
+ }
+ if (!this.userDisabled) {
+ this._handleEnabledChanged();
+ }
+ },
+
+ checkForUpdates: function(delay) {
+ if (this._isUpdateCheckPending) {
+ return;
+ }
+ this._isUpdateCheckPending = true;
+ GMPPrefs.reset(GMPPrefs.KEY_UPDATE_LAST_CHECK, null);
+ // Delay this in case the user changes his mind and doesn't want to
+ // enable EME after all.
+ setTimeout(() => {
+ if (!this.appDisabled) {
+ let gmpInstallManager = new GMPInstallManager();
+ // We don't really care about the results, if someone is interested
+ // they can check the log.
+ gmpInstallManager.simpleCheckAndInstall().then(null, () => {});
+ }
+ this._isUpdateCheckPending = false;
+ }, delay);
+ },
+
+ receiveMessage: function({target: browser, data: data}) {
+ this._log.trace("receiveMessage() data=" + data);
+ let parsedData;
+ try {
+ parsedData = JSON.parse(data);
+ } catch(ex) {
+ this._log.error("Malformed EME video message with data: " + data);
+ return;
+ }
+ let {status: status, keySystem: keySystem} = parsedData;
+ if (status == "cdm-not-installed" || status == "cdm-insufficient-version") {
+ this.checkForUpdates(0);
+ }
+ },
+
+ onPrefEnabledChanged: function() {
+ if (!this._plugin.isEME || !this.appDisabled) {
+ this._handleEnabledChanged();
+ }
+ },
+
+ onPrefVersionChanged: function() {
+ AddonManagerPrivate.callAddonListeners("onUninstalling", this, false);
+ if (this._gmpPath) {
+ this._log.info("onPrefVersionChanged() - unregistering gmp directory " +
+ this._gmpPath);
+ gmpService.removeAndDeletePluginDirectory(this._gmpPath, true /* can defer */);
+ }
+ AddonManagerPrivate.callAddonListeners("onUninstalled", this);
+
+ AddonManagerPrivate.callInstallListeners("onExternalInstall", null, this,
+ null, false);
+ AddonManagerPrivate.callAddonListeners("onInstalling", this, false);
+ this._gmpPath = null;
+ if (this.isInstalled) {
+ this._gmpPath = OS.Path.join(OS.Constants.Path.profileDir,
+ this._plugin.id,
+ GMPPrefs.get(GMPPrefs.KEY_PLUGIN_VERSION,
+ null, this._plugin.id));
+ }
+ if (this._gmpPath && this.isActive) {
+ this._log.info("onPrefVersionChanged() - registering gmp directory " +
+ this._gmpPath);
+ gmpService.addPluginDirectory(this._gmpPath);
+ }
+ AddonManagerPrivate.callAddonListeners("onInstalled", this);
+ },
+
+ uninstallPlugin: function() {
+ AddonManagerPrivate.callAddonListeners("onUninstalling", this, false);
+ if (this.gmpPath) {
+ this._log.info("uninstallPlugin() - unregistering gmp directory " +
+ this.gmpPath);
+ gmpService.removeAndDeletePluginDirectory(this.gmpPath);
+ }
+ GMPPrefs.reset(GMPPrefs.KEY_PLUGIN_VERSION, this.id);
+ AddonManagerPrivate.callAddonListeners("onUninstalled", this);
+ },
+
+ shutdown: function() {
+ Preferences.ignore(GMPPrefs.getPrefKey(GMPPrefs.KEY_PLUGIN_ENABLED,
+ this._plugin.id),
+ this.onPrefEnabledChanged, this);
+ Preferences.ignore(GMPPrefs.getPrefKey(GMPPrefs.KEY_PLUGIN_VERSION,
+ this._plugin.id),
+ this.onPrefVersionChanged, this);
+ if (this._plugin.isEME) {
+ Preferences.ignore(GMPPrefs.KEY_EME_ENABLED,
+ this.onPrefEMEGlobalEnabledChanged, this);
+ messageManager.removeMessageListener("EMEVideo:ContentMediaKeysRequest", this);
+ }
+ return this._updateTask;
+ },
+};
+
+let GMPProvider = {
+ get name() { return "GMPProvider"; },
+
+ _plugins: null,
+
+ startup: function() {
+ configureLogging();
+ this._log = Log.repository.getLoggerWithMessagePrefix("Toolkit.GMP",
+ "GMPProvider.");
+ let telemetry = {};
+ this.buildPluginList();
+ this.ensureProperCDMInstallState();
+
+ Preferences.observe(GMPPrefs.KEY_LOG_BASE, configureLogging);
+
+ for (let [id, plugin] of this._plugins) {
+ let wrapper = plugin.wrapper;
+ let gmpPath = wrapper.gmpPath;
+ let isEnabled = wrapper.isActive;
+ this._log.trace("startup - enabled=" + isEnabled + ", gmpPath=" +
+ gmpPath);
+
+ if (gmpPath && isEnabled) {
+ this._log.info("startup - adding gmp directory " + gmpPath);
+ try {
+ gmpService.addPluginDirectory(gmpPath);
+ } catch (e if e.name == 'NS_ERROR_NOT_AVAILABLE') {
+ this._log.warn("startup - adding gmp directory failed with " +
+ e.name + " - sandboxing not available?", e);
+ }
+ }
+
+ if (this.isEnabled) {
+ telemetry[id] = {
+ userDisabled: wrapper.userDisabled,
+ version: wrapper.version,
+ applyBackgroundUpdates: wrapper.applyBackgroundUpdates,
+ };
+ }
+ }
+
+ if (Preferences.get(GMPPrefs.KEY_EME_ENABLED, false)) {
+ try {
+ let greDir = Services.dirsvc.get(NS_GRE_DIR,
+ Ci.nsILocalFile);
+ let clearkeyPath = OS.Path.join(greDir.path,
+ CLEARKEY_PLUGIN_ID,
+ CLEARKEY_VERSION);
+ this._log.info("startup - adding clearkey CDM directory " +
+ clearkeyPath);
+ gmpService.addPluginDirectory(clearkeyPath);
+ } catch (e) {
+ this._log.warn("startup - adding clearkey CDM failed", e);
+ }
+ }
+
+ AddonManagerPrivate.setTelemetryDetails("GMP", telemetry);
+ },
+
+ shutdown: function() {
+ this._log.trace("shutdown");
+ Preferences.ignore(GMPPrefs.KEY_LOG_BASE, configureLogging);
+
+ let shutdownTask = Task.spawn(function* GMPProvider_shutdownTask() {
+ this._log.trace("shutdown - shutdownTask");
+ let shutdownSucceeded = true;
+
+ for (let plugin of this._plugins.values()) {
+ try {
+ yield plugin.wrapper.shutdown();
+ } catch (e) {
+ shutdownSucceeded = false;
+ }
+ }
+
+ this._plugins = null;
+
+ if (!shutdownSucceeded) {
+ throw new Error("Shutdown failed");
+ }
+ }.bind(this));
+
+ return shutdownTask;
+ },
+
+ getAddonByID: function(aId, aCallback) {
+ if (!this.isEnabled) {
+ aCallback(null);
+ return;
+ }
+
+ let plugin = this._plugins.get(aId);
+ if (plugin && !GMPUtils.isPluginHidden(plugin)) {
+ aCallback(plugin.wrapper);
+ } else {
+ aCallback(null);
+ }
+ },
+
+ getAddonsByTypes: function(aTypes, aCallback) {
+ if (!this.isEnabled ||
+ (aTypes && aTypes.indexOf("plugin") < 0)) {
+ aCallback([]);
+ return;
+ }
+
+ // Tycho:
+ // let results = [p.wrapper for ([id, p] of this._plugins)
+ // if (!GMPUtils.isPluginHidden(p))];
+ let results = [];
+ for (let [id, p] of this._plugins) {
+ if (!GMPUtils.isPluginHidden(p)) {
+ results.push(p.wrapper);
+ }
+ }
+
+ aCallback(results);
+ },
+
+ get isEnabled() {
+ return GMPPrefs.get(GMPPrefs.KEY_PROVIDER_ENABLED, false);
+ },
+
+ generateFullDescription: function(aLicenseURL, aLicenseInfo) {
+ return "<xhtml:a href=\"" + aLicenseURL + "\" target=\"_blank\">" +
+ aLicenseInfo + "</xhtml:a>."
+ },
+
+ buildPluginList: function() {
+ let licenseInfo = pluginsBundle.GetStringFromName(GMP_LICENSE_INFO);
+
+ this._plugins = new Map();
+ for (let aPlugin of GMP_PLUGINS) {
+ let plugin = {
+ id: aPlugin.id,
+ name: pluginsBundle.GetStringFromName(aPlugin.name),
+ description: pluginsBundle.GetStringFromName(aPlugin.description),
+ homepageURL: aPlugin.homepageURL,
+ optionsURL: aPlugin.optionsURL,
+ wrapper: null,
+ isEME: aPlugin.isEME,
+ };
+ if (aPlugin.licenseURL) {
+ plugin.fullDescription =
+ this.generateFullDescription(aPlugin.licenseURL, licenseInfo);
+ }
+ plugin.wrapper = new GMPWrapper(plugin);
+ this._plugins.set(plugin.id, plugin);
+ }
+ },
+
+ ensureProperCDMInstallState: function() {
+ if (!GMPPrefs.get(GMPPrefs.KEY_EME_ENABLED, true)) {
+ for (let [id, plugin] of this._plugins) {
+ if (plugin.isEME && plugin.wrapper.isInstalled) {
+ gmpService.addPluginDirectory(plugin.wrapper.gmpPath);
+ plugin.wrapper.uninstallPlugin();
+ }
+ }
+ }
+ },
+};
+
+AddonManagerPrivate.registerProvider(GMPProvider, [
+ new AddonManagerPrivate.AddonType("plugin", URI_EXTENSION_STRINGS,
+ STRING_TYPE_NAME,
+ AddonManager.VIEW_TYPE_LIST, 6000,
+ AddonManager.TYPE_SUPPORTS_ASK_TO_ACTIVATE)
+]);
diff --git a/toolkit/mozapps/extensions/internal/LightweightThemeImageOptimizer.jsm b/toolkit/mozapps/extensions/internal/LightweightThemeImageOptimizer.jsm
new file mode 100644
index 000000000..fccde9a81
--- /dev/null
+++ b/toolkit/mozapps/extensions/internal/LightweightThemeImageOptimizer.jsm
@@ -0,0 +1,198 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["LightweightThemeImageOptimizer"];
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+
+const ORIGIN_TOP_RIGHT = 1;
+const ORIGIN_BOTTOM_LEFT = 2;
+
+this.LightweightThemeImageOptimizer = {
+ optimize: function LWTIO_optimize(aThemeData, aScreen) {
+ let data = Utils.createCopy(aThemeData);
+ if (!data.headerURL) {
+ return data;
+ }
+
+ data.headerURL = ImageCropper.getCroppedImageURL(
+ data.headerURL, aScreen, ORIGIN_TOP_RIGHT);
+
+ if (data.footerURL) {
+ data.footerURL = ImageCropper.getCroppedImageURL(
+ data.footerURL, aScreen, ORIGIN_BOTTOM_LEFT);
+ }
+
+ return data;
+ },
+
+ purge: function LWTIO_purge() {
+ let dir = FileUtils.getDir("ProfD", ["lwtheme"]);
+ dir.followLinks = false;
+ try {
+ dir.remove(true);
+ } catch (e) {}
+ }
+};
+
+Object.freeze(LightweightThemeImageOptimizer);
+
+let ImageCropper = {
+ _inProgress: {},
+
+ getCroppedImageURL:
+ function ImageCropper_getCroppedImageURL(aImageURL, aScreen, aOrigin) {
+ // We can crop local files, only.
+ if (!aImageURL.startsWith("file://")) {
+ return aImageURL;
+ }
+
+ if (Services.prefs.getBoolPref("lightweightThemes.animation.enabled")) {
+ //Don't crop if animated
+ return aImageURL;
+ }
+
+ // Generate the cropped image's file name using its
+ // base name and the current screen size.
+ let uri = Services.io.newURI(aImageURL, null, null);
+ let file = uri.QueryInterface(Ci.nsIFileURL).file;
+
+ // Make sure the source file exists.
+ if (!file.exists()) {
+ return aImageURL;
+ }
+
+ let fileName = file.leafName + "-" + aScreen.width + "x" + aScreen.height;
+ let croppedFile = FileUtils.getFile("ProfD", ["lwtheme", fileName]);
+
+ // If we have a local file that is not in progress, return it.
+ if (croppedFile.exists() && !(croppedFile.path in this._inProgress)) {
+ let fileURI = Services.io.newFileURI(croppedFile);
+
+ // Copy the query part to avoid wrong caching.
+ fileURI.QueryInterface(Ci.nsIURL).query = uri.query;
+ return fileURI.spec;
+ }
+
+ // Crop the given image in the background.
+ this._crop(uri, croppedFile, aScreen, aOrigin);
+
+ // Return the original image while we're waiting for the cropped version
+ // to be written to disk.
+ return aImageURL;
+ },
+
+ _crop: function ImageCropper_crop(aURI, aTargetFile, aScreen, aOrigin) {
+ let inProgress = this._inProgress;
+ inProgress[aTargetFile.path] = true;
+
+ function resetInProgress() {
+ delete inProgress[aTargetFile.path];
+ }
+
+ ImageFile.read(aURI, function crop_readImageFile(aInputStream, aContentType) {
+ if (aInputStream && aContentType) {
+ let image = ImageTools.decode(aInputStream, aContentType);
+ if (image && image.width && image.height) {
+ let stream = ImageTools.encode(image, aScreen, aOrigin, aContentType);
+ if (stream) {
+ ImageFile.write(aTargetFile, stream, resetInProgress);
+ return;
+ }
+ }
+ }
+
+ resetInProgress();
+ });
+ }
+};
+
+let ImageFile = {
+ read: function ImageFile_read(aURI, aCallback) {
+ this._netUtil.asyncFetch2(
+ aURI,
+ function read_asyncFetch(aInputStream, aStatus, aRequest) {
+ if (Components.isSuccessCode(aStatus) && aRequest instanceof Ci.nsIChannel) {
+ let channel = aRequest.QueryInterface(Ci.nsIChannel);
+ aCallback(aInputStream, channel.contentType);
+ } else {
+ aCallback();
+ }
+ },
+ null, // aLoadingNode
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null, // aTriggeringPrincipal
+ Ci.nsILoadInfo.SEC_NORMAL,
+ Ci.nsIContentPolicy.TYPE_IMAGE);
+ },
+
+ write: function ImageFile_write(aFile, aInputStream, aCallback) {
+ let fos = FileUtils.openSafeFileOutputStream(aFile);
+ this._netUtil.asyncCopy(aInputStream, fos, function write_asyncCopy(aResult) {
+ FileUtils.closeSafeFileOutputStream(fos);
+
+ // Remove the file if writing was not successful.
+ if (!Components.isSuccessCode(aResult)) {
+ try {
+ aFile.remove(false);
+ } catch (e) {}
+ }
+
+ aCallback();
+ });
+ }
+};
+
+XPCOMUtils.defineLazyModuleGetter(ImageFile, "_netUtil",
+ "resource://gre/modules/NetUtil.jsm", "NetUtil");
+
+let ImageTools = {
+ decode: function ImageTools_decode(aInputStream, aContentType) {
+ let outParam = {value: null};
+
+ try {
+ this._imgTools.decodeImageData(aInputStream, aContentType, outParam);
+ } catch (e) {}
+
+ return outParam.value;
+ },
+
+ encode: function ImageTools_encode(aImage, aScreen, aOrigin, aContentType) {
+ let stream;
+ let width = Math.min(aImage.width, aScreen.width);
+ let height = Math.min(aImage.height, aScreen.height);
+ let x = aOrigin == ORIGIN_TOP_RIGHT ? aImage.width - width : 0;
+
+ try {
+ stream = this._imgTools.encodeCroppedImage(aImage, aContentType, x, 0,
+ width, height);
+ } catch (e) {}
+
+ return stream;
+ }
+};
+
+XPCOMUtils.defineLazyServiceGetter(ImageTools, "_imgTools",
+ "@mozilla.org/image/tools;1", "imgITools");
+
+let Utils = {
+ createCopy: function Utils_createCopy(aData) {
+ let copy = {};
+ for (let [k, v] in Iterator(aData)) {
+ copy[k] = v;
+ }
+ return copy;
+ }
+};
diff --git a/toolkit/mozapps/extensions/internal/PluginProvider.jsm b/toolkit/mozapps/extensions/internal/PluginProvider.jsm
new file mode 100644
index 000000000..04a4f9d7c
--- /dev/null
+++ b/toolkit/mozapps/extensions/internal/PluginProvider.jsm
@@ -0,0 +1,595 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+this.EXPORTED_SYMBOLS = [];
+
+Cu.import("resource://gre/modules/AddonManager.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+const URI_EXTENSION_STRINGS = "chrome://mozapps/locale/extensions/extensions.properties";
+const STRING_TYPE_NAME = "type.%ID%.name";
+const LIST_UPDATED_TOPIC = "plugins-list-updated";
+const FLASH_MIME_TYPE = "application/x-shockwave-flash";
+
+Cu.import("resource://gre/modules/Log.jsm");
+const LOGGER_ID = "addons.plugins";
+
+// Create a new logger for use by the Addons Plugin Provider
+// (Requires AddonManager.jsm)
+let logger = Log.repository.getLogger(LOGGER_ID);
+
+function getIDHashForString(aStr) {
+ // return the two-digit hexadecimal code for a byte
+ function toHexString(charCode)
+ ("0" + charCode.toString(16)).slice(-2);
+
+ let hasher = Cc["@mozilla.org/security/hash;1"].
+ createInstance(Ci.nsICryptoHash);
+ hasher.init(Ci.nsICryptoHash.MD5);
+ let stringStream = Cc["@mozilla.org/io/string-input-stream;1"].
+ createInstance(Ci.nsIStringInputStream);
+ stringStream.data = aStr ? aStr : "null";
+ hasher.updateFromStream(stringStream, -1);
+
+ // convert the binary hash data to a hex string.
+ let binary = hasher.finish(false);
+
+ // Tycho: let hash = [toHexString(binary.charCodeAt(i)) for (i in binary)].join("").toLowerCase();
+ let hash = [];
+
+ for (let i in binary) {
+ hash.push(toHexString(binary.charCodeAt(i)));
+ }
+
+ hash = hash.join("").toLowerCase();
+
+ return "{" + hash.substr(0, 8) + "-" +
+ hash.substr(8, 4) + "-" +
+ hash.substr(12, 4) + "-" +
+ hash.substr(16, 4) + "-" +
+ hash.substr(20) + "}";
+}
+
+var PluginProvider = {
+ get name() "PluginProvider",
+
+ // A dictionary mapping IDs to names and descriptions
+ plugins: null,
+
+ startup: function PL_startup() {
+ Services.obs.addObserver(this, LIST_UPDATED_TOPIC, false);
+ Services.obs.addObserver(this, AddonManager.OPTIONS_NOTIFICATION_DISPLAYED, false);
+ },
+
+ /**
+ * Called when the application is shutting down. Only necessary for tests
+ * to be able to simulate a shutdown.
+ */
+ shutdown: function PL_shutdown() {
+ this.plugins = null;
+ Services.obs.removeObserver(this, AddonManager.OPTIONS_NOTIFICATION_DISPLAYED);
+ Services.obs.removeObserver(this, LIST_UPDATED_TOPIC);
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case AddonManager.OPTIONS_NOTIFICATION_DISPLAYED:
+ this.getAddonByID(aData, function PL_displayPluginInfo(plugin) {
+ if (!plugin)
+ return;
+
+ let libLabel = aSubject.getElementById("pluginLibraries");
+ libLabel.textContent = plugin.pluginLibraries.join(", ");
+
+ let typeLabel = aSubject.getElementById("pluginMimeTypes"), types = [];
+ for (let type of plugin.pluginMimeTypes) {
+ let extras = [type.description.trim(), type.suffixes].
+ filter(function(x) x).join(": ");
+ types.push(type.type + (extras ? " (" + extras + ")" : ""));
+ }
+ typeLabel.textContent = types.join(",\n");
+ let showProtectedModePref = canDisableFlashProtectedMode(plugin);
+ aSubject.getElementById("pluginEnableProtectedMode")
+ .setAttribute("collapsed", showProtectedModePref ? "" : "true");
+ });
+ break;
+ case LIST_UPDATED_TOPIC:
+ if (this.plugins)
+ this.updatePluginList();
+ break;
+ }
+ },
+
+ /**
+ * Creates a PluginWrapper for a plugin object.
+ */
+ buildWrapper: function PL_buildWrapper(aPlugin) {
+ return new PluginWrapper(aPlugin.id,
+ aPlugin.name,
+ aPlugin.description,
+ aPlugin.tags);
+ },
+
+ /**
+ * Called to get an Addon with a particular ID.
+ *
+ * @param aId
+ * The ID of the add-on to retrieve
+ * @param aCallback
+ * A callback to pass the Addon to
+ */
+ getAddonByID: function PL_getAddon(aId, aCallback) {
+ if (!this.plugins)
+ this.buildPluginList();
+
+ if (aId in this.plugins)
+ aCallback(this.buildWrapper(this.plugins[aId]));
+ else
+ aCallback(null);
+ },
+
+ /**
+ * Called to get Addons of a particular type.
+ *
+ * @param aTypes
+ * An array of types to fetch. Can be null to get all types.
+ * @param callback
+ * A callback to pass an array of Addons to
+ */
+ getAddonsByTypes: function PL_getAddonsByTypes(aTypes, aCallback) {
+ if (aTypes && aTypes.indexOf("plugin") < 0) {
+ aCallback([]);
+ return;
+ }
+
+ if (!this.plugins)
+ this.buildPluginList();
+
+ let results = [];
+
+ for (let id in this.plugins) {
+ this.getAddonByID(id, function(aAddon) {
+ results.push(aAddon);
+ });
+ }
+
+ aCallback(results);
+ },
+
+ /**
+ * Called to get Addons that have pending operations.
+ *
+ * @param aTypes
+ * An array of types to fetch. Can be null to get all types
+ * @param aCallback
+ * A callback to pass an array of Addons to
+ */
+ getAddonsWithOperationsByTypes: function PL_getAddonsWithOperationsByTypes(aTypes, aCallback) {
+ aCallback([]);
+ },
+
+ /**
+ * Called to get the current AddonInstalls, optionally restricting by type.
+ *
+ * @param aTypes
+ * An array of types or null to get all types
+ * @param aCallback
+ * A callback to pass the array of AddonInstalls to
+ */
+ getInstallsByTypes: function PL_getInstallsByTypes(aTypes, aCallback) {
+ aCallback([]);
+ },
+
+ /**
+ * Builds a list of the current plugins reported by the plugin host
+ *
+ * @return a dictionary of plugins indexed by our generated ID
+ */
+ getPluginList: function PL_getPluginList() {
+ let tags = Cc["@mozilla.org/plugin/host;1"].
+ getService(Ci.nsIPluginHost).
+ getPluginTags({});
+
+ let list = {};
+ let seenPlugins = {};
+ for (let tag of tags) {
+ if (!(tag.name in seenPlugins))
+ seenPlugins[tag.name] = {};
+ if (!(tag.description in seenPlugins[tag.name])) {
+ let plugin = {
+ id: getIDHashForString(tag.name + tag.description),
+ // XXX Flash name substitution like in browser-plugins.js, aboutPermissions.js, permissions.js
+ name: tag.name == "Shockwave Flash" ? "Adobe Flash" : tag.name,
+ description: tag.description,
+ tags: [tag]
+ };
+
+ seenPlugins[tag.name][tag.description] = plugin;
+ list[plugin.id] = plugin;
+ }
+ else {
+ seenPlugins[tag.name][tag.description].tags.push(tag);
+ }
+ }
+
+ return list;
+ },
+
+ /**
+ * Builds the list of known plugins from the plugin host
+ */
+ buildPluginList: function PL_buildPluginList() {
+ this.plugins = this.getPluginList();
+ },
+
+ /**
+ * Updates the plugins from the plugin host by comparing the current plugins
+ * to the last known list sending out any necessary API notifications for
+ * changes.
+ */
+ updatePluginList: function PL_updatePluginList() {
+ let newList = this.getPluginList();
+
+ // Tycho:
+ // let lostPlugins = [this.buildWrapper(this.plugins[id])
+ // for each (id in Object.keys(this.plugins)) if (!(id in newList))];
+
+ // let newPlugins = [this.buildWrapper(newList[id])
+ // for each (id in Object.keys(newList)) if (!(id in this.plugins))];
+
+ // let matchedIDs = [id for each (id in Object.keys(newList)) if (id in this.plugins)];
+
+ let lostPlugins = [];
+ let newPlugins = [];
+ let matchedIDs = [];
+
+ // lostPlugins
+ for each(let id in Object.keys(this.plugins)) {
+ if (!(id in newList)) {
+ lostPlugins.push(this.buildWrapper(this.plugins[id]));
+ }
+ }
+
+ // newPlugins and matchedIDs
+ for each(let id in Object.keys(newList)) {
+ if (!(id in this.plugins)) {
+ newPlugins.push(this.buildWrapper(newList[id]));
+ }
+
+ if (id in this.plugins) {
+ matchedIDs.push(id);
+ }
+ }
+
+
+ // The plugin host generates new tags for every plugin after a scan and
+ // if the plugin's filename has changed then the disabled state won't have
+ // been carried across, send out notifications for anything that has
+ // changed (see bug 830267).
+ let changedWrappers = [];
+ for (let id of matchedIDs) {
+ let oldWrapper = this.buildWrapper(this.plugins[id]);
+ let newWrapper = this.buildWrapper(newList[id]);
+
+ if (newWrapper.isActive != oldWrapper.isActive) {
+ AddonManagerPrivate.callAddonListeners(newWrapper.isActive ?
+ "onEnabling" : "onDisabling",
+ newWrapper, false);
+ changedWrappers.push(newWrapper);
+ }
+ }
+
+ // Notify about new installs
+ for (let plugin of newPlugins) {
+ AddonManagerPrivate.callInstallListeners("onExternalInstall", null,
+ plugin, null, false);
+ AddonManagerPrivate.callAddonListeners("onInstalling", plugin, false);
+ }
+
+ // Notify for any plugins that have vanished.
+ for (let plugin of lostPlugins)
+ AddonManagerPrivate.callAddonListeners("onUninstalling", plugin, false);
+
+ this.plugins = newList;
+
+ // Signal that new installs are complete
+ for (let plugin of newPlugins)
+ AddonManagerPrivate.callAddonListeners("onInstalled", plugin);
+
+ // Signal that enables/disables are complete
+ for (let wrapper of changedWrappers) {
+ AddonManagerPrivate.callAddonListeners(wrapper.isActive ?
+ "onEnabled" : "onDisabled",
+ wrapper);
+ }
+
+ // Signal that uninstalls are complete
+ for (let plugin of lostPlugins)
+ AddonManagerPrivate.callAddonListeners("onUninstalled", plugin);
+ }
+};
+
+function isFlashPlugin(aPlugin) {
+ for (let type of aPlugin.pluginMimeTypes) {
+ if (type.type == FLASH_MIME_TYPE) {
+ return true;
+ }
+ }
+ return false;
+}
+// Protected mode is win32-only, not win64
+function canDisableFlashProtectedMode(aPlugin) {
+ return isFlashPlugin(aPlugin) && Services.appinfo.XPCOMABI == "x86-msvc";
+}
+
+/**
+ * The PluginWrapper wraps a set of nsIPluginTags to provide the data visible to
+ * public callers through the API.
+ */
+function PluginWrapper(aId, aName, aDescription, aTags) {
+ let safedesc = aDescription.replace(/<\/?[a-z][^>]*>/gi, " ");
+ let homepageURL = null;
+ if (/<A\s+HREF=[^>]*>/i.test(aDescription))
+ homepageURL = /<A\s+HREF=["']?([^>"'\s]*)/i.exec(aDescription)[1];
+
+ this.__defineGetter__("id", function() aId);
+ this.__defineGetter__("type", function() "plugin");
+ this.__defineGetter__("name", function() aName);
+ this.__defineGetter__("creator", function() null);
+ this.__defineGetter__("description", function() safedesc);
+ this.__defineGetter__("version", function() aTags[0].version);
+ this.__defineGetter__("homepageURL", function() homepageURL);
+
+ this.__defineGetter__("isActive", function() !aTags[0].blocklisted && !aTags[0].disabled);
+ this.__defineGetter__("appDisabled", function() aTags[0].blocklisted);
+
+ this.__defineGetter__("userDisabled", function() {
+ if (aTags[0].disabled)
+ return true;
+
+ if ((Services.prefs.getBoolPref("plugins.click_to_play") && aTags[0].clicktoplay) ||
+ this.blocklistState == Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE ||
+ this.blocklistState == Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE)
+ return AddonManager.STATE_ASK_TO_ACTIVATE;
+
+ return false;
+ });
+
+ this.__defineSetter__("userDisabled", function(aVal) {
+ let previousVal = this.userDisabled;
+ if (aVal === previousVal)
+ return aVal;
+
+ for (let tag of aTags) {
+ if (aVal === true)
+ tag.enabledState = Ci.nsIPluginTag.STATE_DISABLED;
+ else if (aVal === false)
+ tag.enabledState = Ci.nsIPluginTag.STATE_ENABLED;
+ else if (aVal == AddonManager.STATE_ASK_TO_ACTIVATE)
+ tag.enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY;
+ }
+
+ // If 'userDisabled' was 'true' and we're going to a state that's not
+ // that, we're enabling, so call those listeners.
+ if (previousVal === true && aVal !== true) {
+ AddonManagerPrivate.callAddonListeners("onEnabling", this, false);
+ AddonManagerPrivate.callAddonListeners("onEnabled", this);
+ }
+
+ // If 'userDisabled' was not 'true' and we're going to a state where
+ // it is, we're disabling, so call those listeners.
+ if (previousVal !== true && aVal === true) {
+ AddonManagerPrivate.callAddonListeners("onDisabling", this, false);
+ AddonManagerPrivate.callAddonListeners("onDisabled", this);
+ }
+
+ // If the 'userDisabled' value involved AddonManager.STATE_ASK_TO_ACTIVATE,
+ // call the onPropertyChanged listeners.
+ if (previousVal == AddonManager.STATE_ASK_TO_ACTIVATE ||
+ aVal == AddonManager.STATE_ASK_TO_ACTIVATE) {
+ AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, ["userDisabled"]);
+ }
+
+ return aVal;
+ });
+
+
+ this.__defineGetter__("blocklistState", function() {
+ let bs = Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(Ci.nsIBlocklistService);
+ return bs.getPluginBlocklistState(aTags[0]);
+ });
+
+ this.__defineGetter__("blocklistURL", function() {
+ let bs = Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(Ci.nsIBlocklistService);
+ return bs.getPluginBlocklistURL(aTags[0]);
+ });
+
+ this.__defineGetter__("size", function() {
+ function getDirectorySize(aFile) {
+ let size = 0;
+ let entries = aFile.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
+ let entry;
+ while ((entry = entries.nextFile)) {
+ if (entry.isSymlink() || !entry.isDirectory())
+ size += entry.fileSize;
+ else
+ size += getDirectorySize(entry);
+ }
+ entries.close();
+ return size;
+ }
+
+ let size = 0;
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ for (let tag of aTags) {
+ file.initWithPath(tag.fullpath);
+ if (file.isDirectory())
+ size += getDirectorySize(file);
+ else
+ size += file.fileSize;
+ }
+ return size;
+ });
+
+ this.__defineGetter__("pluginLibraries", function() {
+ let libs = [];
+ for (let tag of aTags)
+ libs.push(tag.filename);
+ return libs;
+ });
+
+ this.__defineGetter__("pluginFullpath", function() {
+ let paths = [];
+ for (let tag of aTags)
+ paths.push(tag.fullpath);
+ return paths;
+ })
+
+ this.__defineGetter__("pluginMimeTypes", function() {
+ let types = [];
+ for (let tag of aTags) {
+ let mimeTypes = tag.getMimeTypes({});
+ let mimeDescriptions = tag.getMimeDescriptions({});
+ let extensions = tag.getExtensions({});
+ for (let i = 0; i < mimeTypes.length; i++) {
+ let type = {};
+ type.type = mimeTypes[i];
+ type.description = mimeDescriptions[i];
+ type.suffixes = extensions[i];
+
+ types.push(type);
+ }
+ }
+ return types;
+ });
+
+ this.__defineGetter__("installDate", function() {
+ let date = 0;
+ for (let tag of aTags) {
+ date = Math.max(date, tag.lastModifiedTime);
+ }
+ return new Date(date);
+ });
+
+ this.__defineGetter__("scope", function() {
+ let path = aTags[0].fullpath;
+ // Plugins inside the application directory are in the application scope
+ let dir = Services.dirsvc.get("APlugns", Ci.nsIFile);
+ if (path.startsWith(dir.path))
+ return AddonManager.SCOPE_APPLICATION;
+
+ // Plugins inside the profile directory are in the profile scope
+ dir = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ if (path.startsWith(dir.path))
+ return AddonManager.SCOPE_PROFILE;
+
+ // Plugins anywhere else in the user's home are in the user scope,
+ // but not all platforms have a home directory.
+ try {
+ dir = Services.dirsvc.get("Home", Ci.nsIFile);
+ if (path.startsWith(dir.path))
+ return AddonManager.SCOPE_USER;
+ } catch (e if (e.result && e.result == Components.results.NS_ERROR_FAILURE)) {
+ // Do nothing: missing "Home".
+ }
+
+ // Any other locations are system scope
+ return AddonManager.SCOPE_SYSTEM;
+ });
+
+ this.__defineGetter__("pendingOperations", function() {
+ return AddonManager.PENDING_NONE;
+ });
+
+ this.__defineGetter__("operationsRequiringRestart", function() {
+ return AddonManager.OP_NEEDS_RESTART_NONE;
+ });
+
+ this.__defineGetter__("permissions", function() {
+ let permissions = 0;
+ if (aTags[0].isEnabledStateLocked) {
+ return permissions;
+ }
+ if (!this.appDisabled) {
+
+ if (this.userDisabled !== true)
+ permissions |= AddonManager.PERM_CAN_DISABLE;
+
+ let blocklistState = this.blocklistState;
+ let isCTPBlocklisted =
+ (blocklistState == Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE ||
+ blocklistState == Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE);
+
+ if (this.userDisabled !== AddonManager.STATE_ASK_TO_ACTIVATE &&
+ (Services.prefs.getBoolPref("plugins.click_to_play") ||
+ isCTPBlocklisted)) {
+ permissions |= AddonManager.PERM_CAN_ASK_TO_ACTIVATE;
+ }
+
+ if (this.userDisabled !== false && !isCTPBlocklisted) {
+ permissions |= AddonManager.PERM_CAN_ENABLE;
+ }
+ }
+ return permissions;
+ });
+
+ this.__defineGetter__("optionsType", function() {
+ if (canDisableFlashProtectedMode(this)) {
+ return AddonManager.OPTIONS_TYPE_INLINE;
+ }
+ return AddonManager.OPTIONS_TYPE_INLINE_INFO;
+ });
+}
+
+PluginWrapper.prototype = {
+ optionsURL: "chrome://mozapps/content/extensions/pluginPrefs.xul",
+
+ get updateDate() {
+ return this.installDate;
+ },
+
+ get isCompatible() {
+ return true;
+ },
+
+ get isPlatformCompatible() {
+ return true;
+ },
+
+ get providesUpdatesSecurely() {
+ return true;
+ },
+
+ get foreignInstall() {
+ return true;
+ },
+
+ isCompatibleWith: function(aAppVerison, aPlatformVersion) {
+ return true;
+ },
+
+ findUpdates: function(aListener, aReason, aAppVersion, aPlatformVersion) {
+ if ("onNoCompatibilityUpdateAvailable" in aListener)
+ aListener.onNoCompatibilityUpdateAvailable(this);
+ if ("onNoUpdateAvailable" in aListener)
+ aListener.onNoUpdateAvailable(this);
+ if ("onUpdateFinished" in aListener)
+ aListener.onUpdateFinished(this);
+ }
+};
+
+AddonManagerPrivate.registerProvider(PluginProvider, [
+ new AddonManagerPrivate.AddonType("plugin", URI_EXTENSION_STRINGS,
+ STRING_TYPE_NAME,
+ AddonManager.VIEW_TYPE_LIST, 6000,
+ AddonManager.TYPE_SUPPORTS_ASK_TO_ACTIVATE)
+]);
diff --git a/toolkit/mozapps/extensions/internal/SpellCheckDictionaryBootstrap.js b/toolkit/mozapps/extensions/internal/SpellCheckDictionaryBootstrap.js
new file mode 100644
index 000000000..f4f557fc2
--- /dev/null
+++ b/toolkit/mozapps/extensions/internal/SpellCheckDictionaryBootstrap.js
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var hunspell, dir;
+
+function startup(data) {
+ hunspell = Components.classes["@mozilla.org/spellchecker/engine;1"]
+ .getService(Components.interfaces.mozISpellCheckingEngine);
+ dir = data.installPath.clone();
+ dir.append("dictionaries");
+ hunspell.addDirectory(dir);
+}
+
+function shutdown() {
+ hunspell.removeDirectory(dir);
+}
diff --git a/toolkit/mozapps/extensions/internal/XPIProvider.jsm b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
new file mode 100644
index 000000000..975448fcc
--- /dev/null
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -0,0 +1,7875 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+this.EXPORTED_SYMBOLS = ["XPIProvider"];
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/AddonManager.jsm");
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository",
+ "resource://gre/modules/addons/AddonRepository.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ChromeManifestParser",
+ "resource://gre/modules/ChromeManifestParser.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
+ "resource://gre/modules/LightweightThemeManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ZipUtils",
+ "resource://gre/modules/ZipUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PermissionsUtils",
+ "resource://gre/modules/PermissionsUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "BrowserToolboxProcess",
+ "resource://gre/modules/devtools/ToolboxProcess.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ConsoleAPI",
+ "resource://gre/modules/devtools/Console.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "Blocklist",
+ "@mozilla.org/extensions/blocklist;1",
+ Ci.nsIBlocklistService);
+XPCOMUtils.defineLazyServiceGetter(this,
+ "ChromeRegistry",
+ "@mozilla.org/chrome/chrome-registry;1",
+ "nsIChromeRegistry");
+XPCOMUtils.defineLazyServiceGetter(this,
+ "ResProtocolHandler",
+ "@mozilla.org/network/protocol;1?name=resource",
+ "nsIResProtocolHandler");
+
+const nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile",
+ "initWithPath");
+
+const PREF_DB_SCHEMA = "extensions.databaseSchema";
+const PREF_INSTALL_CACHE = "extensions.installCache";
+const PREF_XPI_STATE = "extensions.xpiState";
+const PREF_BOOTSTRAP_ADDONS = "extensions.bootstrappedAddons";
+const PREF_PENDING_OPERATIONS = "extensions.pendingOperations";
+const PREF_MATCH_OS_LOCALE = "intl.locale.matchOS";
+const PREF_SELECTED_LOCALE = "general.useragent.locale";
+const PREF_EM_DSS_ENABLED = "extensions.dss.enabled";
+const PREF_DSS_SWITCHPENDING = "extensions.dss.switchPending";
+const PREF_DSS_SKIN_TO_SELECT = "extensions.lastSelectedSkin";
+const PREF_GENERAL_SKINS_SELECTEDSKIN = "general.skins.selectedSkin";
+const PREF_EM_UPDATE_URL = "extensions.update.url";
+const PREF_EM_UPDATE_BACKGROUND_URL = "extensions.update.background.url";
+const PREF_EM_ENABLED_ADDONS = "extensions.enabledAddons";
+const PREF_EM_EXTENSION_FORMAT = "extensions.";
+const PREF_EM_ENABLED_SCOPES = "extensions.enabledScopes";
+const PREF_EM_AUTO_DISABLED_SCOPES = "extensions.autoDisableScopes";
+const PREF_EM_SHOW_MISMATCH_UI = "extensions.showMismatchUI";
+const PREF_XPI_ENABLED = "xpinstall.enabled";
+const PREF_XPI_WHITELIST_REQUIRED = "xpinstall.whitelist.required";
+const PREF_XPI_DIRECT_WHITELISTED = "xpinstall.whitelist.directRequest";
+const PREF_XPI_FILE_WHITELISTED = "xpinstall.whitelist.fileRequest";
+const PREF_XPI_PERMISSIONS_BRANCH = "xpinstall.";
+const PREF_XPI_UNPACK = "extensions.alwaysUnpack";
+const PREF_INSTALL_REQUIREBUILTINCERTS = "extensions.install.requireBuiltInCerts";
+const PREF_INSTALL_REQUIRESECUREORIGIN = "extensions.install.requireSecureOrigin";
+const PREF_INSTALL_DISTRO_ADDONS = "extensions.installDistroAddons";
+const PREF_BRANCH_INSTALLED_ADDON = "extensions.installedDistroAddon.";
+const PREF_SHOWN_SELECTION_UI = "extensions.shownSelectionUI";
+const PREF_INTERPOSITION_ENABLED = "extensions.interposition.enabled";
+
+const PREF_EM_MIN_COMPAT_APP_VERSION = "extensions.minCompatibleAppVersion";
+const PREF_EM_MIN_COMPAT_PLATFORM_VERSION = "extensions.minCompatiblePlatformVersion";
+
+const PREF_CHECKCOMAT_THEMEOVERRIDE = "extensions.checkCompatibility.temporaryThemeOverride_minAppVersion";
+
+const URI_EXTENSION_SELECT_DIALOG = "chrome://mozapps/content/extensions/selectAddons.xul";
+const URI_EXTENSION_UPDATE_DIALOG = "chrome://mozapps/content/extensions/update.xul";
+const URI_EXTENSION_STRINGS = "chrome://mozapps/locale/extensions/extensions.properties";
+
+const STRING_TYPE_NAME = "type.%ID%.name";
+
+const DIR_EXTENSIONS = "extensions";
+const DIR_STAGE = "staged";
+const DIR_XPI_STAGE = "staged-xpis";
+const DIR_TRASH = "trash";
+
+const FILE_DATABASE = "extensions.json";
+const FILE_OLD_CACHE = "extensions.cache";
+const FILE_INSTALL_MANIFEST = "install.rdf";
+const FILE_WEBEXT_MANIFEST = "manifest.json";
+const FILE_XPI_ADDONS_LIST = "extensions.ini";
+
+const KEY_PROFILEDIR = "ProfD";
+const KEY_APPDIR = "XCurProcD";
+const KEY_TEMPDIR = "TmpD";
+const KEY_APP_DISTRIBUTION = "XREAppDist";
+
+const KEY_APP_PROFILE = "app-profile";
+const KEY_APP_GLOBAL = "app-global";
+const KEY_APP_SYSTEM_LOCAL = "app-system-local";
+const KEY_APP_SYSTEM_SHARE = "app-system-share";
+const KEY_APP_SYSTEM_USER = "app-system-user";
+
+const NOTIFICATION_FLUSH_PERMISSIONS = "flush-pending-permissions";
+const XPI_PERMISSION = "install";
+
+const RDFURI_INSTALL_MANIFEST_ROOT = "urn:mozilla:install-manifest";
+const PREFIX_NS_EM = "http://www.mozilla.org/2004/em-rdf#";
+
+const TOOLKIT_ID = "toolkit@mozilla.org";
+#ifdef MOZ_PHOENIX_EXTENSIONS
+const FIREFOX_ID = "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"
+const FIREFOX_APPCOMPATVERSION = "27.9"
+#endif
+
+// The value for this is in Makefile.in
+#expand const DB_SCHEMA = __MOZ_EXTENSIONS_DB_SCHEMA__;
+const NOTIFICATION_TOOLBOXPROCESS_LOADED = "ToolboxProcessLoaded";
+
+// Properties that exist in the install manifest
+const PROP_METADATA = ["id", "version", "type", "internalName", "updateURL",
+ "updateKey", "optionsURL", "optionsType", "aboutURL",
+ "iconURL", "icon64URL"];
+const PROP_LOCALE_SINGLE = ["name", "description", "creator", "homepageURL"];
+const PROP_LOCALE_MULTI = ["developers", "translators", "contributors"];
+const PROP_TARGETAPP = ["id", "minVersion", "maxVersion"];
+
+// Properties that should be migrated where possible from an old database. These
+// shouldn't include properties that can be read directly from install.rdf files
+// or calculated
+const DB_MIGRATE_METADATA= ["installDate", "userDisabled", "softDisabled",
+ "sourceURI", "applyBackgroundUpdates",
+ "releaseNotesURI", "foreignInstall", "syncGUID"];
+// Properties to cache and reload when an addon installation is pending
+const PENDING_INSTALL_METADATA =
+ ["syncGUID", "targetApplications", "userDisabled", "softDisabled",
+ "existingAddonID", "sourceURI", "releaseNotesURI", "installDate",
+ "updateDate", "applyBackgroundUpdates", "compatibilityOverrides"];
+
+// Note: When adding/changing/removing items here, remember to change the
+// DB schema version to ensure changes are picked up ASAP.
+const STATIC_BLOCKLIST_PATTERNS = [
+ { creator: "Mozilla Corp.",
+ level: Blocklist.STATE_BLOCKED,
+ blockID: "i162" },
+ { creator: "Mozilla.org",
+ level: Blocklist.STATE_BLOCKED,
+ blockID: "i162" }
+];
+
+
+const BOOTSTRAP_REASONS = {
+ APP_STARTUP : 1,
+ APP_SHUTDOWN : 2,
+ ADDON_ENABLE : 3,
+ ADDON_DISABLE : 4,
+ ADDON_INSTALL : 5,
+ ADDON_UNINSTALL : 6,
+ ADDON_UPGRADE : 7,
+ ADDON_DOWNGRADE : 8
+};
+
+// Map new string type identifiers to old style nsIUpdateItem types
+const TYPES = {
+ extension: 2,
+ theme: 4,
+ locale: 8,
+ multipackage: 32,
+ dictionary: 64,
+ experiment: 128,
+};
+
+const RESTARTLESS_TYPES = new Set([
+ "dictionary",
+ "experiment",
+ "locale",
+]);
+
+// Keep track of where we are in startup for telemetry
+// event happened during XPIDatabase.startup()
+const XPI_STARTING = "XPIStarting";
+// event happened after startup() but before the final-ui-startup event
+const XPI_BEFORE_UI_STARTUP = "BeforeFinalUIStartup";
+// event happened after final-ui-startup
+const XPI_AFTER_UI_STARTUP = "AfterFinalUIStartup";
+
+const COMPATIBLE_BY_DEFAULT_TYPES = {
+ extension: true,
+ dictionary: true
+};
+
+const MSG_JAR_FLUSH = "AddonJarFlush";
+
+var gGlobalScope = this;
+
+/**
+ * Valid IDs fit this pattern.
+ */
+var gIDTest = /^(\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}|[a-z0-9-\._]*\@[a-z0-9-\._]+)$/i;
+
+Cu.import("resource://gre/modules/Log.jsm");
+const LOGGER_ID = "addons.xpi";
+
+// Create a new logger for use by all objects in this Addons XPI Provider module
+// (Requires AddonManager.jsm)
+let logger = Log.repository.getLogger(LOGGER_ID);
+
+const LAZY_OBJECTS = ["XPIDatabase"];
+
+var gLazyObjectsLoaded = false;
+
+function loadLazyObjects() {
+ let scope = {};
+ scope.AddonInternal = AddonInternal;
+ scope.XPIProvider = XPIProvider;
+ scope.XPIStates = XPIStates;
+ Services.scriptloader.loadSubScript("resource://gre/modules/addons/XPIProviderUtils.js",
+ scope);
+
+ for (let name of LAZY_OBJECTS) {
+ delete gGlobalScope[name];
+ gGlobalScope[name] = scope[name];
+ }
+ gLazyObjectsLoaded = true;
+ return scope;
+}
+
+for (let name of LAZY_OBJECTS) {
+ Object.defineProperty(gGlobalScope, name, {
+ get: function lazyObjectGetter() {
+ let objs = loadLazyObjects();
+ return objs[name];
+ },
+ configurable: true
+ });
+}
+
+
+function findMatchingStaticBlocklistItem(aAddon) {
+ for (let item of STATIC_BLOCKLIST_PATTERNS) {
+ if ("creator" in item && typeof item.creator == "string") {
+ if ((aAddon.defaultLocale && aAddon.defaultLocale.creator == item.creator) ||
+ (aAddon.selectedLocale && aAddon.selectedLocale.creator == item.creator)) {
+ return item;
+ }
+ }
+ }
+ return null;
+}
+
+
+/**
+ * Sets permissions on a file
+ *
+ * @param aFile
+ * The file or directory to operate on.
+ * @param aPermissions
+ * The permisions to set
+ */
+function setFilePermissions(aFile, aPermissions) {
+ try {
+ aFile.permissions = aPermissions;
+ }
+ catch (e) {
+ logger.warn("Failed to set permissions " + aPermissions.toString(8) + " on " +
+ aFile.path, e);
+ }
+}
+
+/**
+ * A safe way to install a file or the contents of a directory to a new
+ * directory. The file or directory is moved or copied recursively and if
+ * anything fails an attempt is made to rollback the entire operation. The
+ * operation may also be rolled back to its original state after it has
+ * completed by calling the rollback method.
+ *
+ * Operations can be chained. Calling move or copy multiple times will remember
+ * the whole set and if one fails all of the operations will be rolled back.
+ */
+function SafeInstallOperation() {
+ this._installedFiles = [];
+ this._createdDirs = [];
+}
+
+SafeInstallOperation.prototype = {
+ _installedFiles: null,
+ _createdDirs: null,
+
+ _installFile: function SIO_installFile(aFile, aTargetDirectory, aCopy) {
+ let oldFile = aCopy ? null : aFile.clone();
+ let newFile = aFile.clone();
+ try {
+ if (aCopy)
+ newFile.copyTo(aTargetDirectory, null);
+ else
+ newFile.moveTo(aTargetDirectory, null);
+ }
+ catch (e) {
+ logger.error("Failed to " + (aCopy ? "copy" : "move") + " file " + aFile.path +
+ " to " + aTargetDirectory.path, e);
+ throw e;
+ }
+ this._installedFiles.push({ oldFile: oldFile, newFile: newFile });
+ },
+
+ _installDirectory: function SIO_installDirectory(aDirectory, aTargetDirectory, aCopy) {
+ let newDir = aTargetDirectory.clone();
+ newDir.append(aDirectory.leafName);
+ try {
+ newDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ }
+ catch (e) {
+ logger.error("Failed to create directory " + newDir.path, e);
+ throw e;
+ }
+ this._createdDirs.push(newDir);
+
+ // Use a snapshot of the directory contents to avoid possible issues with
+ // iterating over a directory while removing files from it (the YAFFS2
+ // embedded filesystem has this issue, see bug 772238), and to remove
+ // normal files before their resource forks on OSX (see bug 733436).
+ let entries = getDirectoryEntries(aDirectory, true);
+ entries.forEach(function(aEntry) {
+ try {
+ this._installDirEntry(aEntry, newDir, aCopy);
+ }
+ catch (e) {
+ logger.error("Failed to " + (aCopy ? "copy" : "move") + " entry " +
+ aEntry.path, e);
+ throw e;
+ }
+ }, this);
+
+ // If this is only a copy operation then there is nothing else to do
+ if (aCopy)
+ return;
+
+ // The directory should be empty by this point. If it isn't this will throw
+ // and all of the operations will be rolled back
+ try {
+ setFilePermissions(aDirectory, FileUtils.PERMS_DIRECTORY);
+ aDirectory.remove(false);
+ }
+ catch (e) {
+ logger.error("Failed to remove directory " + aDirectory.path, e);
+ throw e;
+ }
+
+ // Note we put the directory move in after all the file moves so the
+ // directory is recreated before all the files are moved back
+ this._installedFiles.push({ oldFile: aDirectory, newFile: newDir });
+ },
+
+ _installDirEntry: function SIO_installDirEntry(aDirEntry, aTargetDirectory, aCopy) {
+ let isDir = null;
+
+ try {
+ isDir = aDirEntry.isDirectory();
+ }
+ catch (e) {
+ // If the file has already gone away then don't worry about it, this can
+ // happen on OSX where the resource fork is automatically moved with the
+ // data fork for the file. See bug 733436.
+ if (e.result == Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST)
+ return;
+
+ logger.error("Failure " + (aCopy ? "copying" : "moving") + " " + aDirEntry.path +
+ " to " + aTargetDirectory.path);
+ throw e;
+ }
+
+ try {
+ if (isDir)
+ this._installDirectory(aDirEntry, aTargetDirectory, aCopy);
+ else
+ this._installFile(aDirEntry, aTargetDirectory, aCopy);
+ }
+ catch (e) {
+ logger.error("Failure " + (aCopy ? "copying" : "moving") + " " + aDirEntry.path +
+ " to " + aTargetDirectory.path);
+ throw e;
+ }
+ },
+
+ /**
+ * Moves a file or directory into a new directory. If an error occurs then all
+ * files that have been moved will be moved back to their original location.
+ *
+ * @param aFile
+ * The file or directory to be moved.
+ * @param aTargetDirectory
+ * The directory to move into, this is expected to be an empty
+ * directory.
+ */
+ moveUnder: function SIO_move(aFile, aTargetDirectory) {
+ try {
+ this._installDirEntry(aFile, aTargetDirectory, false);
+ }
+ catch (e) {
+ this.rollback();
+ throw e;
+ }
+ },
+
+ /**
+ * Renames a file to a new location. If an error occurs then all
+ * files that have been moved will be moved back to their original location.
+ *
+ * @param aOldLocation
+ * The old location of the file.
+ * @param aNewLocation
+ * The new location of the file.
+ */
+ moveTo: function(aOldLocation, aNewLocation) {
+ try {
+ let oldFile = aOldLocation.clone(), newFile = aNewLocation.clone();
+ oldFile.moveTo(newFile.parent, newFile.leafName);
+ this._installedFiles.push({ oldFile: oldFile, newFile: newFile, isMoveTo: true});
+ }
+ catch(e) {
+ this.rollback();
+ throw e;
+ }
+ },
+
+ /**
+ * Copies a file or directory into a new directory. If an error occurs then
+ * all new files that have been created will be removed.
+ *
+ * @param aFile
+ * The file or directory to be copied.
+ * @param aTargetDirectory
+ * The directory to copy into, this is expected to be an empty
+ * directory.
+ */
+ copy: function SIO_copy(aFile, aTargetDirectory) {
+ try {
+ this._installDirEntry(aFile, aTargetDirectory, true);
+ }
+ catch (e) {
+ this.rollback();
+ throw e;
+ }
+ },
+
+ /**
+ * Rolls back all the moves that this operation performed. If an exception
+ * occurs here then both old and new directories are left in an indeterminate
+ * state
+ */
+ rollback: function SIO_rollback() {
+ while (this._installedFiles.length > 0) {
+ let move = this._installedFiles.pop();
+ if (move.isMoveTo) {
+ move.newFile.moveTo(oldDir.parent, oldDir.leafName);
+ }
+ else if (move.newFile.isDirectory()) {
+ let oldDir = move.oldFile.parent.clone();
+ oldDir.append(move.oldFile.leafName);
+ oldDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ }
+ else if (!move.oldFile) {
+ // No old file means this was a copied file
+ move.newFile.remove(true);
+ }
+ else {
+ move.newFile.moveTo(move.oldFile.parent, null);
+ }
+ }
+
+ while (this._createdDirs.length > 0)
+ recursiveRemove(this._createdDirs.pop());
+ }
+};
+
+/**
+ * Gets the currently selected locale for display.
+ * @return the selected locale or "en-US" if none is selected
+ */
+function getLocale() {
+ if (Preferences.get(PREF_MATCH_OS_LOCALE, false))
+ return Services.locale.getLocaleComponentForUserAgent();
+ try {
+ let locale = Preferences.get(PREF_SELECTED_LOCALE, null, Ci.nsIPrefLocalizedString);
+ if (locale)
+ return locale;
+ }
+ catch (e) {}
+ return Preferences.get(PREF_SELECTED_LOCALE, "en-US");
+}
+
+/**
+ * Selects the closest matching locale from a list of locales.
+ *
+ * @param aLocales
+ * An array of locales
+ * @return the best match for the currently selected locale
+ */
+function findClosestLocale(aLocales) {
+ let appLocale = getLocale();
+
+ // Holds the best matching localized resource
+ var bestmatch = null;
+ // The number of locale parts it matched with
+ var bestmatchcount = 0;
+ // The number of locale parts in the match
+ var bestpartcount = 0;
+
+ var matchLocales = [appLocale.toLowerCase()];
+ /* If the current locale is English then it will find a match if there is
+ a valid match for en-US so no point searching that locale too. */
+ if (matchLocales[0].substring(0, 3) != "en-")
+ matchLocales.push("en-us");
+
+ for each (var locale in matchLocales) {
+ var lparts = locale.split("-");
+ for each (var localized in aLocales) {
+ for each (let found in localized.locales) {
+ found = found.toLowerCase();
+ // Exact match is returned immediately
+ if (locale == found)
+ return localized;
+
+ var fparts = found.split("-");
+ /* If we have found a possible match and this one isn't any longer
+ then we dont need to check further. */
+ if (bestmatch && fparts.length < bestmatchcount)
+ continue;
+
+ // Count the number of parts that match
+ var maxmatchcount = Math.min(fparts.length, lparts.length);
+ var matchcount = 0;
+ while (matchcount < maxmatchcount &&
+ fparts[matchcount] == lparts[matchcount])
+ matchcount++;
+
+ /* If we matched more than the last best match or matched the same and
+ this locale is less specific than the last best match. */
+ if (matchcount > bestmatchcount ||
+ (matchcount == bestmatchcount && fparts.length < bestpartcount)) {
+ bestmatch = localized;
+ bestmatchcount = matchcount;
+ bestpartcount = fparts.length;
+ }
+ }
+ }
+ // If we found a valid match for this locale return it
+ if (bestmatch)
+ return bestmatch;
+ }
+ return null;
+}
+
+/**
+ * Sets the userDisabled and softDisabled properties of an add-on based on what
+ * values those properties had for a previous instance of the add-on. The
+ * previous instance may be a previous install or in the case of an application
+ * version change the same add-on.
+ *
+ * NOTE: this may modify aNewAddon in place; callers should save the database if
+ * necessary
+ *
+ * @param aOldAddon
+ * The previous instance of the add-on
+ * @param aNewAddon
+ * The new instance of the add-on
+ * @param aAppVersion
+ * The optional application version to use when checking the blocklist
+ * or undefined to use the current application
+ * @param aPlatformVersion
+ * The optional platform version to use when checking the blocklist or
+ * undefined to use the current platform
+ */
+function applyBlocklistChanges(aOldAddon, aNewAddon, aOldAppVersion,
+ aOldPlatformVersion) {
+ // Copy the properties by default
+ aNewAddon.userDisabled = aOldAddon.userDisabled;
+ aNewAddon.softDisabled = aOldAddon.softDisabled;
+
+ let oldBlocklistState = Blocklist.getAddonBlocklistState(createWrapper(aOldAddon),
+ aOldAppVersion,
+ aOldPlatformVersion);
+ let newBlocklistState = Blocklist.getAddonBlocklistState(createWrapper(aNewAddon));
+
+ // If the blocklist state hasn't changed then the properties don't need to
+ // change
+ if (newBlocklistState == oldBlocklistState)
+ return;
+
+ if (newBlocklistState == Blocklist.STATE_SOFTBLOCKED) {
+ if (aNewAddon.type != "theme") {
+ // The add-on has become softblocked, set softDisabled if it isn't already
+ // userDisabled
+ aNewAddon.softDisabled = !aNewAddon.userDisabled;
+ }
+ else {
+ // Themes just get userDisabled to switch back to the default theme
+ aNewAddon.userDisabled = true;
+ }
+ }
+ else {
+ // If the new add-on is not softblocked then it cannot be softDisabled
+ aNewAddon.softDisabled = false;
+ }
+}
+
+/**
+ * Calculates whether an add-on should be appDisabled or not.
+ *
+ * @param aAddon
+ * The add-on to check
+ * @return true if the add-on should not be appDisabled
+ */
+function isUsableAddon(aAddon) {
+ // Hack to ensure the default theme is always usable
+ if (aAddon.type == "theme" && aAddon.internalName == XPIProvider.defaultSkin)
+ return true;
+
+ if (aAddon.jetsdk)
+ return false;
+
+ if (aAddon.blocklistState == Blocklist.STATE_BLOCKED)
+ return false;
+
+ if (AddonManager.checkUpdateSecurity && !aAddon.providesUpdatesSecurely)
+ return false;
+
+ if (!aAddon.isPlatformCompatible)
+ return false;
+
+ if (AddonManager.checkCompatibility) {
+ if (!aAddon.isCompatible)
+ return false;
+ }
+ else {
+ let app = aAddon.matchingTargetApplication;
+ if (!app)
+ return false;
+
+ // XXX Temporary solution to let applications opt-in to make themes safer
+ // following significant UI changes even if checkCompatibility=false has
+ // been set, until we get bug 962001.
+ if (aAddon.type == "theme" && app.id == Services.appinfo.ID) {
+ try {
+ let minCompatVersion = Services.prefs.getCharPref(PREF_CHECKCOMAT_THEMEOVERRIDE);
+ if (minCompatVersion &&
+ Services.vc.compare(minCompatVersion, app.maxVersion) > 0) {
+ return false;
+ }
+ } catch (e) {}
+ }
+ }
+
+ return true;
+}
+
+XPCOMUtils.defineLazyServiceGetter(this, "gRDF", "@mozilla.org/rdf/rdf-service;1",
+ Ci.nsIRDFService);
+
+function EM_R(aProperty) {
+ return gRDF.GetResource(PREFIX_NS_EM + aProperty);
+}
+
+function createAddonDetails(id, aAddon) {
+ return {
+ id: id || aAddon.id,
+ type: aAddon.type,
+ version: aAddon.version
+ };
+}
+
+/**
+ * Converts an RDF literal, resource or integer into a string.
+ *
+ * @param aLiteral
+ * The RDF object to convert
+ * @return a string if the object could be converted or null
+ */
+function getRDFValue(aLiteral) {
+ if (aLiteral instanceof Ci.nsIRDFLiteral)
+ return aLiteral.Value;
+ if (aLiteral instanceof Ci.nsIRDFResource)
+ return aLiteral.Value;
+ if (aLiteral instanceof Ci.nsIRDFInt)
+ return aLiteral.Value;
+ return null;
+}
+
+/**
+ * Gets an RDF property as a string
+ *
+ * @param aDs
+ * The RDF datasource to read the property from
+ * @param aResource
+ * The RDF resource to read the property from
+ * @param aProperty
+ * The property to read
+ * @return a string if the property existed or null
+ */
+function getRDFProperty(aDs, aResource, aProperty) {
+ return getRDFValue(aDs.GetTarget(aResource, EM_R(aProperty), true));
+}
+
+/**
+ * Reads an AddonInternal object from an RDF stream.
+ *
+ * @param aUri
+ * The URI that the manifest is being read from
+ * @param aStream
+ * An open stream to read the RDF from
+ * @return an AddonInternal object
+ * @throws if the install manifest in the RDF stream is corrupt or could not
+ * be read
+ */
+function loadManifestFromRDF(aUri, aStream) {
+ function getPropertyArray(aDs, aSource, aProperty) {
+ let values = [];
+ let targets = aDs.GetTargets(aSource, EM_R(aProperty), true);
+ while (targets.hasMoreElements())
+ values.push(getRDFValue(targets.getNext()));
+
+ return values;
+ }
+
+ /**
+ * Reads locale properties from either the main install manifest root or
+ * an em:localized section in the install manifest.
+ *
+ * @param aDs
+ * The nsIRDFDatasource to read from
+ * @param aSource
+ * The nsIRDFResource to read the properties from
+ * @param isDefault
+ * True if the locale is to be read from the main install manifest
+ * root
+ * @param aSeenLocales
+ * An array of locale names already seen for this install manifest.
+ * Any locale names seen as a part of this function will be added to
+ * this array
+ * @return an object containing the locale properties
+ */
+ function readLocale(aDs, aSource, isDefault, aSeenLocales) {
+ let locale = { };
+ if (!isDefault) {
+ locale.locales = [];
+ let targets = ds.GetTargets(aSource, EM_R("locale"), true);
+ while (targets.hasMoreElements()) {
+ let localeName = getRDFValue(targets.getNext());
+ if (!localeName) {
+ logger.warn("Ignoring empty locale in localized properties");
+ continue;
+ }
+ if (aSeenLocales.indexOf(localeName) != -1) {
+ logger.warn("Ignoring duplicate locale in localized properties");
+ continue;
+ }
+ aSeenLocales.push(localeName);
+ locale.locales.push(localeName);
+ }
+
+ if (locale.locales.length == 0) {
+ logger.warn("Ignoring localized properties with no listed locales");
+ return null;
+ }
+ }
+
+ PROP_LOCALE_SINGLE.forEach(function(aProp) {
+ locale[aProp] = getRDFProperty(aDs, aSource, aProp);
+ });
+
+ PROP_LOCALE_MULTI.forEach(function(aProp) {
+ // Don't store empty arrays
+ let props = getPropertyArray(aDs, aSource,
+ aProp.substring(0, aProp.length - 1));
+ if (props.length > 0)
+ locale[aProp] = props;
+ });
+
+ return locale;
+ }
+
+ let rdfParser = Cc["@mozilla.org/rdf/xml-parser;1"].
+ createInstance(Ci.nsIRDFXMLParser)
+ let ds = Cc["@mozilla.org/rdf/datasource;1?name=in-memory-datasource"].
+ createInstance(Ci.nsIRDFDataSource);
+ let listener = rdfParser.parseAsync(ds, aUri);
+ let channel = Cc["@mozilla.org/network/input-stream-channel;1"].
+ createInstance(Ci.nsIInputStreamChannel);
+ channel.setURI(aUri);
+ channel.contentStream = aStream;
+ channel.QueryInterface(Ci.nsIChannel);
+ channel.contentType = "text/xml";
+
+ listener.onStartRequest(channel, null);
+
+ try {
+ let pos = 0;
+ let count = aStream.available();
+ while (count > 0) {
+ listener.onDataAvailable(channel, null, aStream, pos, count);
+ pos += count;
+ count = aStream.available();
+ }
+ listener.onStopRequest(channel, null, Components.results.NS_OK);
+ }
+ catch (e) {
+ listener.onStopRequest(channel, null, e.result);
+ throw e;
+ }
+
+ let root = gRDF.GetResource(RDFURI_INSTALL_MANIFEST_ROOT);
+ let addon = new AddonInternal();
+ PROP_METADATA.forEach(function(aProp) {
+ addon[aProp] = getRDFProperty(ds, root, aProp);
+ });
+ addon.unpack = getRDFProperty(ds, root, "unpack") == "true";
+
+ if (!addon.type) {
+ addon.type = addon.internalName ? "theme" : "extension";
+ }
+ else {
+ let type = addon.type;
+ addon.type = null;
+ for (let name in TYPES) {
+ if (TYPES[name] == type) {
+ addon.type = name;
+ break;
+ }
+ }
+ }
+
+ if (!(addon.type in TYPES))
+ throw new Error("Install manifest specifies unknown type: " + addon.type);
+
+ if (addon.type != "multipackage") {
+ if (!addon.id)
+ throw new Error("No ID in install manifest");
+ if (!gIDTest.test(addon.id))
+ throw new Error("Illegal add-on ID " + addon.id);
+ if (!addon.version)
+ throw new Error("No version in install manifest");
+ }
+
+ addon.strictCompatibility = !(addon.type in COMPATIBLE_BY_DEFAULT_TYPES) ||
+ getRDFProperty(ds, root, "strictCompatibility") == "true";
+
+ // Only read these properties for extensions.
+ if (addon.type == "extension") {
+ addon.bootstrap = getRDFProperty(ds, root, "bootstrap") == "true";
+ addon.multiprocessCompatible = getRDFProperty(ds, root, "multiprocessCompatible") == "true";
+ if (addon.optionsType &&
+ addon.optionsType != AddonManager.OPTIONS_TYPE_DIALOG &&
+ addon.optionsType != AddonManager.OPTIONS_TYPE_INLINE &&
+ addon.optionsType != AddonManager.OPTIONS_TYPE_TAB &&
+ addon.optionsType != AddonManager.OPTIONS_TYPE_INLINE_INFO) {
+ throw new Error("Install manifest specifies unknown type: " + addon.optionsType);
+ }
+ }
+ else {
+ // Some add-on types are always restartless.
+ if (RESTARTLESS_TYPES.has(addon.type)) {
+ addon.bootstrap = true;
+ }
+
+ // Only extensions are allowed to provide an optionsURL, optionsType or aboutURL. For
+ // all other types they are silently ignored
+ addon.optionsURL = null;
+ addon.optionsType = null;
+ addon.aboutURL = null;
+
+ if (addon.type == "theme") {
+ if (!addon.internalName)
+ throw new Error("Themes must include an internalName property");
+ addon.skinnable = getRDFProperty(ds, root, "skinnable") == "true";
+ }
+ }
+
+ addon.defaultLocale = readLocale(ds, root, true);
+
+ let seenLocales = [];
+ addon.locales = [];
+ let targets = ds.GetTargets(root, EM_R("localized"), true);
+ while (targets.hasMoreElements()) {
+ let target = targets.getNext().QueryInterface(Ci.nsIRDFResource);
+ let locale = readLocale(ds, target, false, seenLocales);
+ if (locale)
+ addon.locales.push(locale);
+ }
+
+ let seenApplications = [];
+ addon.targetApplications = [];
+ targets = ds.GetTargets(root, EM_R("targetApplication"), true);
+ while (targets.hasMoreElements()) {
+ let target = targets.getNext().QueryInterface(Ci.nsIRDFResource);
+ let targetAppInfo = {};
+ PROP_TARGETAPP.forEach(function(aProp) {
+ targetAppInfo[aProp] = getRDFProperty(ds, target, aProp);
+ });
+ if (!targetAppInfo.id || !targetAppInfo.minVersion ||
+ !targetAppInfo.maxVersion) {
+ logger.warn("Ignoring invalid targetApplication entry in install manifest");
+ continue;
+ }
+ if (seenApplications.indexOf(targetAppInfo.id) != -1) {
+ logger.warn("Ignoring duplicate targetApplication entry for " + targetAppInfo.id +
+ " in install manifest");
+ continue;
+ }
+ seenApplications.push(targetAppInfo.id);
+ addon.targetApplications.push(targetAppInfo);
+ }
+
+ // Note that we don't need to check for duplicate targetPlatform entries since
+ // the RDF service coalesces them for us.
+ let targetPlatforms = getPropertyArray(ds, root, "targetPlatform");
+ addon.targetPlatforms = [];
+ targetPlatforms.forEach(function(aPlatform) {
+ let platform = {
+ os: null,
+ abi: null
+ };
+
+ let pos = aPlatform.indexOf("_");
+ if (pos != -1) {
+ platform.os = aPlatform.substring(0, pos);
+ platform.abi = aPlatform.substring(pos + 1);
+ }
+ else {
+ platform.os = aPlatform;
+ }
+
+ addon.targetPlatforms.push(platform);
+ });
+
+ // A theme's userDisabled value is true if the theme is not the selected skin
+ // or if there is an active lightweight theme. We ignore whether softblocking
+ // is in effect since it would change the active theme.
+ if (addon.type == "theme") {
+ addon.userDisabled = !!LightweightThemeManager.currentTheme ||
+ addon.internalName != XPIProvider.selectedSkin;
+ }
+ // Experiments are disabled by default. It is up to the Experiments Manager
+ // to enable them (it drives installation).
+ else if (addon.type == "experiment") {
+ addon.userDisabled = true;
+ }
+ else {
+ addon.userDisabled = false;
+ addon.softDisabled = addon.blocklistState == Blocklist.STATE_SOFTBLOCKED;
+ }
+
+ addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT;
+
+ // Experiments are managed and updated through an external "experiments
+ // manager." So disable some built-in mechanisms.
+ if (addon.type == "experiment") {
+ addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DISABLE;
+ addon.updateURL = null;
+ addon.updateKey = null;
+
+ addon.targetApplications = [];
+ addon.targetPlatforms = [];
+ }
+
+ // Load the storage service before NSS (nsIRandomGenerator),
+ // to avoid a SQLite initialization error (bug 717904).
+ let storage = Services.storage;
+
+ // Generate random GUID used for Sync.
+ // This was lifted from util.js:makeGUID() from services-sync.
+ let guid = Cc["@mozilla.org/uuid-generator;1"]
+ .getService(Ci.nsIUUIDGenerator)
+ .generateUUID().toString();
+ addon.syncGUID = guid;
+
+ return addon;
+}
+
+/**
+ * Loads an AddonInternal object from an add-on extracted in a directory.
+ *
+ * @param aDir
+ * The nsIFile directory holding the add-on
+ * @return an AddonInternal object
+ * @throws if the directory does not contain a valid install manifest
+ */
+function loadManifestFromDir(aDir) {
+ function getFileSize(aFile) {
+ if (aFile.isSymlink())
+ return 0;
+
+ if (!aFile.isDirectory())
+ return aFile.fileSize;
+
+ let size = 0;
+ let entries = aFile.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
+ let entry;
+ while ((entry = entries.nextFile))
+ size += getFileSize(entry);
+ entries.close();
+ return size;
+ }
+
+ let file = aDir.clone();
+ file.append(FILE_INSTALL_MANIFEST);
+ if (!file.exists() || !file.isFile())
+ throw new Error("Directory " + aDir.path + " does not contain a valid " +
+ "install manifest");
+
+ let fis = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+ fis.init(file, -1, -1, false);
+ let bis = Cc["@mozilla.org/network/buffered-input-stream;1"].
+ createInstance(Ci.nsIBufferedInputStream);
+ bis.init(fis, 4096);
+
+ try {
+ let addon = loadManifestFromRDF(Services.io.newFileURI(file), bis);
+ addon._sourceBundle = aDir.clone();
+ addon.size = getFileSize(aDir);
+
+ file = aDir.clone();
+ file.append("chrome.manifest");
+ let chromeManifest = ChromeManifestParser.parseSync(Services.io.newFileURI(file));
+ addon.hasBinaryComponents = ChromeManifestParser.hasType(chromeManifest,
+ "binary-component");
+
+ addon.appDisabled = !isUsableAddon(addon);
+ return addon;
+ }
+ finally {
+ bis.close();
+ fis.close();
+ }
+}
+
+/**
+ * Loads an AddonInternal object from an nsIZipReader for an add-on.
+ *
+ * @param aZipReader
+ * An open nsIZipReader for the add-on's files
+ * @return an AddonInternal object
+ * @throws if the XPI file does not contain a valid install manifest.
+ * Throws with |webext:true| if a WebExtension manifest was found
+ * to distinguish between WebExtensions and corrupt files.
+ */
+function loadManifestFromZipReader(aZipReader) {
+ let zis;
+ try {
+ zis = aZipReader.getInputStream(FILE_INSTALL_MANIFEST);
+ } catch (e) {
+ // We're going to throw here, but depending on whether we have a
+ // WebExtension manifest in the XPI, we'll throw with the webext flag.
+ try {
+ let zws = aZipReader.getInputStream(FILE_WEBEXT_MANIFEST);
+ zws.close();
+ } catch(e2) {
+ // We have neither an install manifest nor a WebExtension manifest;
+ // this means the extension file has a structural problem.
+ // Just pass the original error up the chain in that case.
+ throw {
+ name: e.name,
+ message: e.message
+ };
+ }
+ // If we get here, we have a WebExtension manifest but no install
+ // manifest. Pass the error up the chain with the webext flag.
+ throw {
+ name: e.name,
+ message: e.message,
+ webext: true
+ };
+ }
+
+ // We found an install manifest, so it's either a regular or hybrid
+ // extension. Continue processing.
+ let bis = Cc["@mozilla.org/network/buffered-input-stream;1"].
+ createInstance(Ci.nsIBufferedInputStream);
+ bis.init(zis, 4096);
+
+ try {
+ let uri = buildJarURI(aZipReader.file, FILE_INSTALL_MANIFEST);
+ let addon = loadManifestFromRDF(uri, bis);
+ addon._sourceBundle = aZipReader.file;
+
+ addon.size = 0;
+ let entries = aZipReader.findEntries(null);
+ while (entries.hasMore())
+ addon.size += aZipReader.getEntry(entries.getNext()).realSize;
+
+ // Binary components can only be loaded from unpacked addons.
+ if (addon.unpack) {
+ uri = buildJarURI(aZipReader.file, "chrome.manifest");
+ let chromeManifest = ChromeManifestParser.parseSync(uri);
+ addon.hasBinaryComponents = ChromeManifestParser.hasType(chromeManifest,
+ "binary-component");
+ } else {
+ addon.hasBinaryComponents = false;
+ }
+
+ // Set a boolean value whether the .xpi archive contains file related to old
+ // Mozilla Add-on SDK or contains file related to PMkit (or new Mozilla SDK),
+ // but extension is not directly targeting Pale Moon
+ if (aZipReader.hasEntry("harness-options.json")) {
+ addon.jetsdk = true;
+ } else if (aZipReader.hasEntry("package.json")) {
+ let app = addon.matchingTargetApplication;
+ if (app && app.id == Services.appinfo.ID) {
+ addon.jetsdk = false;
+ } else {
+ addon.jetsdk = true;
+ }
+ } else {
+ addon.jetsdk = false;
+ }
+
+ addon.appDisabled = !isUsableAddon(addon);
+ return addon;
+ }
+ finally {
+ bis.close();
+ zis.close();
+ }
+}
+
+/**
+ * Loads an AddonInternal object from an add-on in an XPI file.
+ *
+ * @param aXPIFile
+ * An nsIFile pointing to the add-on's XPI file
+ * @return an AddonInternal object
+ * @throws if the XPI file does not contain a valid install manifest
+ */
+function loadManifestFromZipFile(aXPIFile) {
+ let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].
+ createInstance(Ci.nsIZipReader);
+ try {
+ zipReader.open(aXPIFile);
+
+ return loadManifestFromZipReader(zipReader);
+ }
+ finally {
+ zipReader.close();
+ }
+}
+
+function loadManifestFromFile(aFile) {
+ if (aFile.isFile())
+ return loadManifestFromZipFile(aFile);
+ else
+ return loadManifestFromDir(aFile);
+}
+
+/**
+ * Gets an nsIURI for a file within another file, either a directory or an XPI
+ * file. If aFile is a directory then this will return a file: URI, if it is an
+ * XPI file then it will return a jar: URI.
+ *
+ * @param aFile
+ * The file containing the resources, must be either a directory or an
+ * XPI file
+ * @param aPath
+ * The path to find the resource at, "/" separated. If aPath is empty
+ * then the uri to the root of the contained files will be returned
+ * @return an nsIURI pointing at the resource
+ */
+function getURIForResourceInFile(aFile, aPath) {
+ if (aFile.isDirectory()) {
+ let resource = aFile.clone();
+ if (aPath) {
+ aPath.split("/").forEach(function(aPart) {
+ resource.append(aPart);
+ });
+ }
+ return NetUtil.newURI(resource);
+ }
+
+ return buildJarURI(aFile, aPath);
+}
+
+/**
+ * Creates a jar: URI for a file inside a ZIP file.
+ *
+ * @param aJarfile
+ * The ZIP file as an nsIFile
+ * @param aPath
+ * The path inside the ZIP file
+ * @return an nsIURI for the file
+ */
+function buildJarURI(aJarfile, aPath) {
+ let uri = Services.io.newFileURI(aJarfile);
+ uri = "jar:" + uri.spec + "!/" + aPath;
+ return NetUtil.newURI(uri);
+}
+
+/**
+ * Sends local and remote notifications to flush a JAR file cache entry
+ *
+ * @param aJarFile
+ * The ZIP/XPI/JAR file as a nsIFile
+ */
+function flushJarCache(aJarFile) {
+ Services.obs.notifyObservers(aJarFile, "flush-cache-entry", null);
+ Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageBroadcaster)
+ .broadcastAsyncMessage(MSG_JAR_FLUSH, aJarFile.path);
+}
+
+function flushStartupCache() {
+ // Init this, so it will get the notification.
+ Services.obs.notifyObservers(null, "startupcache-invalidate", null);
+}
+
+/**
+ * Creates and returns a new unique temporary file. The caller should delete
+ * the file when it is no longer needed.
+ *
+ * @return an nsIFile that points to a randomly named, initially empty file in
+ * the OS temporary files directory
+ */
+function getTemporaryFile() {
+ let file = FileUtils.getDir(KEY_TEMPDIR, []);
+ let random = Math.random().toString(36).replace(/0./, '').substr(-3);
+ file.append("tmp-" + random + ".xpi");
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+
+ return file;
+}
+
+/**
+ * Verifies that a zip file's contents are all signed by the same principal.
+ * Directory entries and anything in the META-INF directory are not checked.
+ *
+ * @param aZip
+ * A nsIZipReader to check
+ * @param aCertificate
+ * The nsIX509Cert to compare against
+ * @return true if all the contents that should be signed were signed by the
+ * principal
+ */
+function verifyZipSigning(aZip, aCertificate) {
+ var count = 0;
+ var entries = aZip.findEntries(null);
+ while (entries.hasMore()) {
+ var entry = entries.getNext();
+ // Nothing in META-INF is in the manifest.
+ if (entry.substr(0, 9) == "META-INF/")
+ continue;
+ // Directory entries aren't in the manifest.
+ if (entry.substr(-1) == "/")
+ continue;
+ count++;
+ var entryCertificate = aZip.getSigningCert(entry);
+ if (!entryCertificate || !aCertificate.equals(entryCertificate)) {
+ return false;
+ }
+ }
+ return aZip.manifestEntriesCount == count;
+}
+
+/**
+ * Replaces %...% strings in an addon url (update and updateInfo) with
+ * appropriate values.
+ *
+ * @param aAddon
+ * The AddonInternal representing the add-on
+ * @param aUri
+ * The uri to escape
+ * @param aUpdateType
+ * An optional number representing the type of update, only applicable
+ * when creating a url for retrieving an update manifest
+ * @param aAppVersion
+ * The optional application version to use for %APP_VERSION%
+ * @return the appropriately escaped uri.
+ */
+function escapeAddonURI(aAddon, aUri, aUpdateType, aAppVersion)
+{
+ let uri = AddonManager.escapeAddonURI(aAddon, aUri, aAppVersion);
+
+ // If there is an updateType then replace the UPDATE_TYPE string
+ if (aUpdateType)
+ uri = uri.replace(/%UPDATE_TYPE%/g, aUpdateType);
+
+ // If this add-on has compatibility information for either the current
+ // application or toolkit then replace the ITEM_MAXAPPVERSION with the
+ // maxVersion
+ let app = aAddon.matchingTargetApplication;
+ if (app)
+ var maxVersion = app.maxVersion;
+ else
+ maxVersion = "";
+ uri = uri.replace(/%ITEM_MAXAPPVERSION%/g, maxVersion);
+
+ let compatMode = "normal";
+ if (!AddonManager.checkCompatibility)
+ compatMode = "ignore";
+ else if (AddonManager.strictCompatibility)
+ compatMode = "strict";
+ uri = uri.replace(/%COMPATIBILITY_MODE%/g, compatMode);
+
+ return uri;
+}
+
+function removeAsync(aFile) {
+ return Task.spawn(function () {
+ let info = null;
+ try {
+ info = yield OS.File.stat(aFile.path);
+ if (info.isDir)
+ yield OS.File.removeDir(aFile.path);
+ else
+ yield OS.File.remove(aFile.path);
+ }
+ catch (e if e instanceof OS.File.Error && e.becauseNoSuchFile) {
+ // The file has already gone away
+ return;
+ }
+ });
+}
+
+/**
+ * Recursively removes a directory or file fixing permissions when necessary.
+ *
+ * @param aFile
+ * The nsIFile to remove
+ */
+function recursiveRemove(aFile) {
+ let isDir = null;
+
+ try {
+ isDir = aFile.isDirectory();
+ }
+ catch (e) {
+ // If the file has already gone away then don't worry about it, this can
+ // happen on OSX where the resource fork is automatically moved with the
+ // data fork for the file. See bug 733436.
+ if (e.result == Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST)
+ return;
+ if (e.result == Cr.NS_ERROR_FILE_NOT_FOUND)
+ return;
+
+ throw e;
+ }
+
+ setFilePermissions(aFile, isDir ? FileUtils.PERMS_DIRECTORY
+ : FileUtils.PERMS_FILE);
+
+ try {
+ aFile.remove(true);
+ return;
+ }
+ catch (e) {
+ if (!aFile.isDirectory()) {
+ logger.error("Failed to remove file " + aFile.path, e);
+ throw e;
+ }
+ }
+
+ // Use a snapshot of the directory contents to avoid possible issues with
+ // iterating over a directory while removing files from it (the YAFFS2
+ // embedded filesystem has this issue, see bug 772238), and to remove
+ // normal files before their resource forks on OSX (see bug 733436).
+ let entries = getDirectoryEntries(aFile, true);
+ entries.forEach(recursiveRemove);
+
+ try {
+ aFile.remove(true);
+ }
+ catch (e) {
+ logger.error("Failed to remove empty directory " + aFile.path, e);
+ throw e;
+ }
+}
+
+/**
+ * Returns the timestamp and leaf file name of the most recently modified
+ * entry in a directory,
+ * or simply the file's own timestamp if it is not a directory.
+ * Also returns the total number of items (directories and files) visited in the scan
+ *
+ * @param aFile
+ * A non-null nsIFile object
+ * @return [File Name, Epoch time, items visited], as described above.
+ */
+function recursiveLastModifiedTime(aFile) {
+ try {
+ let modTime = aFile.lastModifiedTime;
+ let fileName = aFile.leafName;
+ if (aFile.isFile())
+ return [fileName, modTime, 1];
+
+ if (aFile.isDirectory()) {
+ let entries = aFile.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
+ let entry;
+ let totalItems = 1;
+ while ((entry = entries.nextFile)) {
+ let [subName, subTime, items] = recursiveLastModifiedTime(entry);
+ totalItems += items;
+ if (subTime > modTime) {
+ modTime = subTime;
+ fileName = subName;
+ }
+ }
+ entries.close();
+ return [fileName, modTime, totalItems];
+ }
+ }
+ catch (e) {
+ logger.warn("Problem getting last modified time for " + aFile.path, e);
+ }
+
+ // If the file is something else, just ignore it.
+ return ["", 0, 0];
+}
+
+/**
+ * Gets a snapshot of directory entries.
+ *
+ * @param aDir
+ * Directory to look at
+ * @param aSortEntries
+ * True to sort entries by filename
+ * @return An array of nsIFile, or an empty array if aDir is not a readable directory
+ */
+function getDirectoryEntries(aDir, aSortEntries) {
+ let dirEnum;
+ try {
+ dirEnum = aDir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
+ let entries = [];
+ while (dirEnum.hasMoreElements())
+ entries.push(dirEnum.nextFile);
+
+ if (aSortEntries) {
+ entries.sort(function sortDirEntries(a, b) {
+ return a.path > b.path ? -1 : 1;
+ });
+ }
+
+ return entries
+ }
+ catch (e) {
+ logger.warn("Can't iterate directory " + aDir.path, e);
+ return [];
+ }
+ finally {
+ if (dirEnum) {
+ dirEnum.close();
+ }
+ }
+}
+
+/**
+ * Wraps a function in an exception handler to protect against exceptions inside callbacks
+ * @param aFunction function(args...)
+ * @return function(args...), a function that takes the same arguments as aFunction
+ * and returns the same result unless aFunction throws, in which case it logs
+ * a warning and returns undefined.
+ */
+function makeSafe(aFunction) {
+ return function(...aArgs) {
+ try {
+ return aFunction(...aArgs);
+ }
+ catch(ex) {
+ logger.warn("XPIProvider callback failed", ex);
+ }
+ return undefined;
+ }
+}
+
+/**
+ * The on-disk state of an individual XPI, created from an Object
+ * as stored in the 'extensions.xpiState' pref.
+ */
+function XPIState(saved) {
+ for (let [short, long] of XPIState.prototype.fields) {
+ if (short in saved) {
+ this[long] = saved[short];
+ }
+ }
+}
+
+XPIState.prototype = {
+ fields: [['d', 'descriptor'],
+ ['e', 'enabled'],
+ ['v', 'version'],
+ ['st', 'scanTime'],
+ ['mt', 'manifestTime']],
+ /**
+ * Return the last modified time, based on enabled/disabled
+ */
+ get mtime() {
+ if (!this.enabled && ('manifestTime' in this) && this.manifestTime > this.scanTime) {
+ return this.manifestTime;
+ }
+ return this.scanTime;
+ },
+
+ toJSON() {
+ let json = {};
+ for (let [short, long] of XPIState.prototype.fields) {
+ if (long in this) {
+ json[short] = this[long];
+ }
+ }
+ return json;
+ },
+
+ /**
+ * Update the last modified time for an add-on on disk.
+ * @param aFile: nsIFile path of the add-on.
+ * @param aId: The add-on ID.
+ * @return True if the time stamp has changed.
+ */
+ getModTime(aFile, aId) {
+ let changed = false;
+ let scanStarted = Cu.now();
+ // For an unknown or enabled add-on, we do a full recursive scan.
+ if (!('scanTime' in this) || this.enabled) {
+ logger.debug('getModTime: Recursive scan of ' + aId);
+ let [modFile, modTime, items] = recursiveLastModifiedTime(aFile);
+ XPIProvider._mostRecentlyModifiedFile[aId] = modFile;
+ if (modTime != this.scanTime) {
+ this.scanTime = modTime;
+ changed = true;
+ }
+ }
+ // if the add-on is disabled, modified time is the install.rdf time, if any.
+ // If {path}/install.rdf doesn't exist, we assume this is a packed .xpi and use
+ // the time stamp of {path}
+ try {
+ // Get the install.rdf update time, if any.
+ // XXX This will eventually also need to check for package.json or whatever
+ // the new manifest is named.
+ let maniFile = aFile.clone();
+ maniFile.append(FILE_INSTALL_MANIFEST);
+ if (!(aId in XPIProvider._mostRecentlyModifiedFile)) {
+ XPIProvider._mostRecentlyModifiedFile[aId] = maniFile.leafName;
+ }
+ let maniTime = maniFile.lastModifiedTime;
+ if (maniTime != this.manifestTime) {
+ this.manifestTime = maniTime;
+ changed = true;
+ }
+ } catch (e) {
+ // No manifest
+ delete this.manifestTime;
+ try {
+ let dtime = aFile.lastModifiedTime;
+ if (dtime != this.scanTime) {
+ changed = true;
+ this.scanTime = dtime;
+ }
+ } catch (e) {
+ logger.warn("Can't get modified time of ${file}: ${e}", {file: aFile.path, e: e});
+ changed = true;
+ this.scanTime = 0;
+ }
+ }
+ return changed;
+ },
+
+ /**
+ * Update the XPIState to match an XPIDatabase entry; if 'enabled' is changed to true,
+ * update the last-modified time. This should probably be made async, but for now we
+ * don't want to maintain parallel sync and async versions of the scan.
+ * Caller is responsible for doing XPIStates.save() if necessary.
+ * @param aDBAddon The DBAddonInternal for this add-on.
+ * @param aUpdated The add-on was updated, so we must record new modified time.
+ */
+ syncWithDB(aDBAddon, aUpdated = false) {
+ logger.debug("Updating XPIState for " + JSON.stringify(aDBAddon));
+ // If the add-on changes from disabled to enabled, we should re-check the modified time.
+ // If this is a newly found add-on, it won't have an 'enabled' field but we
+ // did a full recursive scan in that case, so we don't need to do it again.
+ // We don't use aDBAddon.active here because it's not updated until after restart.
+ let mustGetMod = (aDBAddon.visible && !aDBAddon.disabled && !this.enabled);
+ this.enabled = (aDBAddon.visible && !aDBAddon.disabled);
+ this.version = aDBAddon.version;
+ // XXX Eventually also copy bootstrap, etc.
+ if (aUpdated || mustGetMod) {
+ this.getModTime(new nsIFile(this.descriptor), aDBAddon.id);
+ if (this.scanTime != aDBAddon.updateDate) {
+ aDBAddon.updateDate = this.scanTime;
+ XPIDatabase.saveChanges();
+ }
+ }
+ },
+};
+
+// Constructor for an ES6 Map that knows how to convert itself into a
+// regular object for toJSON().
+function SerializableMap() {
+ let m = new Map();
+ m.toJSON = function() {
+ let out = {}
+ for (let [key, val] of m) {
+ out[key] = val;
+ }
+ return out;
+ };
+ return m;
+}
+
+/**
+ * Keeps track of the state of XPI add-ons on the file system.
+ */
+this.XPIStates = {
+ // Map(location name -> Map(add-on ID -> XPIState))
+ db: null,
+
+ get size() {
+ if (!this.db) {
+ return 0;
+ }
+ let count = 0;
+ for (let location of this.db.values()) {
+ count += location.size;
+ }
+ return count;
+ },
+
+ /**
+ * Load extension state data from preferences.
+ */
+ loadExtensionState() {
+ let state = {};
+
+ // Clear out old directory state cache.
+ Preferences.reset(PREF_INSTALL_CACHE);
+
+ let cache = Preferences.get(PREF_XPI_STATE, "{}");
+ try {
+ state = JSON.parse(cache);
+ } catch (e) {
+ logger.warn("Error parsing extensions.xpiState ${state}: ${error}",
+ {state: cache, error: e});
+ }
+ logger.debug("Loaded add-on state from prefs: ${}", state);
+ return state;
+ },
+
+ /**
+ * Walk through all install locations, highest priority first,
+ * comparing the on-disk state of extensions to what is stored in prefs.
+ * @return true if anything has changed.
+ */
+ getInstallState() {
+ let oldState = this.loadExtensionState();
+ let changed = false;
+ this.db = new SerializableMap();
+
+ for (let location of XPIProvider.installLocations) {
+ // The list of add-on like file/directory names in the install location.
+ let addons = location.addonLocations;
+ // The results of scanning this location.
+ let foundAddons = new SerializableMap();
+
+ // What our old state thinks should be in this location.
+ let locState = {};
+ if (location.name in oldState) {
+ locState = oldState[location.name];
+ // We've seen this location.
+ delete oldState[location.name];
+ }
+
+ for (let file of addons) {
+ let id = location.getIDForLocation(file);
+
+ if (!(id in locState)) {
+ logger.debug("New add-on ${id} in ${location}", {id: id, location: location.name});
+ let xpiState = new XPIState({d: file.persistentDescriptor});
+ changed = xpiState.getModTime(file, id) || changed;
+ foundAddons.set(id, xpiState);
+ } else {
+ let xpiState = new XPIState(locState[id]);
+ // We found this add-on in the file system
+ delete locState[id];
+
+ changed = xpiState.getModTime(file, id) || changed;
+
+ if (file.persistentDescriptor != xpiState.descriptor) {
+ xpiState.descriptor = file.persistentDescriptor;
+ changed = true;
+ }
+ if (changed) {
+ logger.debug("Changed add-on ${id} in ${location}", {id: id, location: location.name});
+ }
+ foundAddons.set(id, xpiState);
+ }
+ }
+
+ // Anything left behind in oldState was removed from the file system.
+ for (let id in locState) {
+ changed = true;
+ break;
+ }
+ // If we found anything, add this location to our database.
+ if (foundAddons.size != 0) {
+ this.db.set(location.name, foundAddons);
+ }
+ }
+
+ // If there's anything left in oldState, an install location that held add-ons
+ // was removed from the browser configuration.
+ for (let location in oldState) {
+ changed = true;
+ break;
+ }
+
+ logger.debug("getInstallState changed: ${rv}, state: ${state}",
+ {rv: changed, state: this.db});
+ return changed;
+ },
+
+ /**
+ * Get the Map of XPI states for a particular location.
+ * @param aLocation The name of the install location.
+ * @return Map (id -> XPIState) or null if there are no add-ons in the location.
+ */
+ getLocation(aLocation) {
+ return this.db.get(aLocation);
+ },
+
+ /**
+ * Get the XPI state for a specific add-on in a location.
+ * If the state is not in our cache, return null.
+ * @param aLocation The name of the location where the add-on is installed.
+ * @param aId The add-on ID
+ * @return The XPIState entry for the add-on, or null.
+ */
+ getAddon(aLocation, aId) {
+ let location = this.db.get(aLocation);
+ if (!location) {
+ return null;
+ }
+ return location.get(aId);
+ },
+
+ /**
+ * Find the highest priority location of an add-on by ID and return the
+ * location and the XPIState.
+ * @param aId The add-on ID
+ * @return [locationName, XPIState] if the add-on is found, [undefined, undefined]
+ * if the add-on is not found.
+ */
+ findAddon(aId) {
+ // Fortunately the Map iterator returns in order of insertion, which is
+ // also our highest -> lowest priority order.
+ for (let [name, location] of this.db) {
+ if (location.has(aId)) {
+ return [name, location.get(aId)];
+ }
+ }
+ return [undefined, undefined];
+ },
+
+ /**
+ * Add a new XPIState for an add-on and synchronize it with the DBAddonInternal.
+ * @param aAddon DBAddonInternal for the new add-on.
+ */
+ addAddon(aAddon) {
+ let location = this.db.get(aAddon.location);
+ if (!location) {
+ // First add-on in this location.
+ location = new SerializableMap();
+ this.db.set(aAddon.location, location);
+ }
+ logger.debug("XPIStates adding add-on ${id} in ${location}: ${descriptor}", aAddon);
+ let xpiState = new XPIState({d: aAddon.descriptor});
+ location.set(aAddon.id, xpiState);
+ xpiState.syncWithDB(aAddon, true);
+ },
+
+ /**
+ * Save the current state of installed add-ons.
+ * XXX this *totally* should be a .json file using DeferredSave...
+ */
+ save() {
+ let cache = JSON.stringify(this.db);
+ Services.prefs.setCharPref(PREF_XPI_STATE, cache);
+ },
+
+ /**
+ * Remove the XPIState for an add-on and save the new state.
+ * @param aLocation The name of the add-on location.
+ * @param aId The ID of the add-on.
+ */
+ removeAddon(aLocation, aId) {
+ logger.debug("Removing XPIState for " + aLocation + ":" + aId);
+ let location = this.db.get(aLocation);
+ if (!location) {
+ return;
+ }
+ location.delete(aId);
+ if (location.size == 0) {
+ this.db.delete(aLocation);
+ }
+ this.save();
+ },
+};
+
+this.XPIProvider = {
+ get name() "XPIProvider",
+
+ // An array of known install locations
+ installLocations: null,
+ // A dictionary of known install locations by name
+ installLocationsByName: null,
+ // An array of currently active AddonInstalls
+ installs: null,
+ // The default skin for the application
+ defaultSkin: "classic/1.0",
+ // The current skin used by the application
+ currentSkin: null,
+ // The selected skin to be used by the application when it is restarted. This
+ // will be the same as currentSkin when it is the skin to be used when the
+ // application is restarted
+ selectedSkin: null,
+ // The value of the minCompatibleAppVersion preference
+ minCompatibleAppVersion: null,
+ // The value of the minCompatiblePlatformVersion preference
+ minCompatiblePlatformVersion: null,
+ // A dictionary of the file descriptors for bootstrappable add-ons by ID
+ bootstrappedAddons: {},
+ // A dictionary of JS scopes of loaded bootstrappable add-ons by ID
+ bootstrapScopes: {},
+ // True if the platform could have activated extensions
+ extensionsActive: false,
+ // True if all of the add-ons found during startup were installed in the
+ // application install location
+ allAppGlobal: true,
+ // A string listing the enabled add-ons for annotating crash reports
+ enabledAddons: null,
+ // Keep track of startup phases for telemetry
+ runPhase: XPI_STARTING,
+ // Keep track of the newest file in each add-on, in case we want to
+ // report it to telemetry.
+ _mostRecentlyModifiedFile: {},
+ // Per-addon telemetry information
+ _telemetryDetails: {},
+ // Experiments are disabled by default. Track ones that are locally enabled.
+ _enabledExperiments: null,
+ // A Map from an add-on install to its ID
+ _addonFileMap: new Map(),
+ // Flag to know if ToolboxProcess.jsm has already been loaded by someone or not
+ _toolboxProcessLoaded: false,
+ // Have we started shutting down bootstrap add-ons?
+ _closing: false,
+
+ // Keep track of in-progress operations that support cancel()
+ _inProgress: [],
+
+ doing: function XPI_doing(aCancellable) {
+ this._inProgress.push(aCancellable);
+ },
+
+ done: function XPI_done(aCancellable) {
+ let i = this._inProgress.indexOf(aCancellable);
+ if (i != -1) {
+ this._inProgress.splice(i, 1);
+ return true;
+ }
+ return false;
+ },
+
+ cancelAll: function XPI_cancelAll() {
+ // Cancelling one may alter _inProgress, so don't use a simple iterator
+ while (this._inProgress.length > 0) {
+ let c = this._inProgress.shift();
+ try {
+ c.cancel();
+ }
+ catch (e) {
+ logger.warn("Cancel failed", e);
+ }
+ }
+ },
+
+ /**
+ * Adds or updates a URI mapping for an Addon.id.
+ *
+ * Mappings should not be removed at any point. This is so that the mappings
+ * will be still valid after an add-on gets disabled or uninstalled, as
+ * consumers may still have URIs of (leaked) resources they want to map.
+ */
+ _addURIMapping: function XPI__addURIMapping(aID, aFile) {
+ logger.info("Mapping " + aID + " to " + aFile.path);
+ this._addonFileMap.set(aID, aFile.path);
+
+ let service = Cc["@mozilla.org/addon-path-service;1"].getService(Ci.amIAddonPathService);
+ service.insertPath(aFile.path, aID);
+ },
+
+ /**
+ * Resolve a URI back to physical file.
+ *
+ * Of course, this works only for URIs pointing to local resources.
+ *
+ * @param aURI
+ * URI to resolve
+ * @return
+ * resolved nsIFileURL
+ */
+ _resolveURIToFile: function XPI__resolveURIToFile(aURI) {
+ switch (aURI.scheme) {
+ case "jar":
+ case "file":
+ if (aURI instanceof Ci.nsIJARURI) {
+ return this._resolveURIToFile(aURI.JARFile);
+ }
+ return aURI;
+
+ case "chrome":
+ aURI = ChromeRegistry.convertChromeURL(aURI);
+ return this._resolveURIToFile(aURI);
+
+ case "resource":
+ aURI = Services.io.newURI(ResProtocolHandler.resolveURI(aURI), null,
+ null);
+ return this._resolveURIToFile(aURI);
+
+ case "view-source":
+ aURI = Services.io.newURI(aURI.path, null, null);
+ return this._resolveURIToFile(aURI);
+
+ case "about":
+ if (aURI.spec == "about:blank") {
+ // Do not attempt to map about:blank
+ return null;
+ }
+
+ let chan;
+ try {
+ chan = Services.io.newChannelFromURI2(aURI,
+ null, // aLoadingNode
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null, // aTriggeringPrincipal
+ Ci.nsILoadInfo.SEC_NORMAL,
+ Ci.nsIContentPolicy.TYPE_OTHER);
+ }
+ catch (ex) {
+ return null;
+ }
+ // Avoid looping
+ if (chan.URI.equals(aURI)) {
+ return null;
+ }
+ // We want to clone the channel URI to avoid accidentially keeping
+ // unnecessary references to the channel or implementation details
+ // around.
+ return this._resolveURIToFile(chan.URI.clone());
+
+ default:
+ return null;
+ }
+ },
+
+ /**
+ * Starts the XPI provider initializes the install locations and prefs.
+ *
+ * @param aAppChanged
+ * A tri-state value. Undefined means the current profile was created
+ * for this session, true means the profile already existed but was
+ * last used with an application with a different version number,
+ * false means that the profile was last used by this version of the
+ * application.
+ * @param aOldAppVersion
+ * The version of the application last run with this profile or null
+ * if it is a new profile or the version is unknown
+ * @param aOldPlatformVersion
+ * The version of the platform last run with this profile or null
+ * if it is a new profile or the version is unknown
+ */
+ startup: function XPI_startup(aAppChanged, aOldAppVersion, aOldPlatformVersion) {
+ function addDirectoryInstallLocation(aName, aKey, aPaths, aScope, aLocked) {
+ try {
+ var dir = FileUtils.getDir(aKey, aPaths);
+ }
+ catch (e) {
+ // Some directories aren't defined on some platforms, ignore them
+ logger.debug("Skipping unavailable install location " + aName);
+ return;
+ }
+
+ try {
+ var location = new DirectoryInstallLocation(aName, dir, aScope, aLocked);
+ }
+ catch (e) {
+ logger.warn("Failed to add directory install location " + aName, e);
+ return;
+ }
+
+ XPIProvider.installLocations.push(location);
+ XPIProvider.installLocationsByName[location.name] = location;
+ }
+
+ function addRegistryInstallLocation(aName, aRootkey, aScope) {
+ try {
+ var location = new WinRegInstallLocation(aName, aRootkey, aScope);
+ }
+ catch (e) {
+ logger.warn("Failed to add registry install location " + aName, e);
+ return;
+ }
+
+ XPIProvider.installLocations.push(location);
+ XPIProvider.installLocationsByName[location.name] = location;
+ }
+
+ try {
+ AddonManagerPrivate.recordTimestamp("XPI_startup_begin");
+
+ logger.debug("startup");
+ this.runPhase = XPI_STARTING;
+ this.installs = [];
+ this.installLocations = [];
+ this.installLocationsByName = {};
+ // Hook for tests to detect when saving database at shutdown time fails
+ this._shutdownError = null;
+ // Clear this at startup for xpcshell test restarts
+ this._telemetryDetails = {};
+ // Clear the set of enabled experiments (experiments disabled by default).
+ this._enabledExperiments = new Set();
+
+ let hasRegistry = ("nsIWindowsRegKey" in Ci);
+
+ let enabledScopes = Preferences.get(PREF_EM_ENABLED_SCOPES,
+ AddonManager.SCOPE_ALL);
+
+ // These must be in order of priority, highest to lowest,
+ // for processFileChanges etc. to work
+ // The profile location is always enabled
+ addDirectoryInstallLocation(KEY_APP_PROFILE, KEY_PROFILEDIR,
+ [DIR_EXTENSIONS],
+ AddonManager.SCOPE_PROFILE, false);
+
+ if (enabledScopes & AddonManager.SCOPE_USER) {
+ addDirectoryInstallLocation(KEY_APP_SYSTEM_USER, "XREUSysExt",
+ [Services.appinfo.ID],
+ AddonManager.SCOPE_USER, true);
+ if (hasRegistry) {
+ addRegistryInstallLocation("winreg-app-user",
+ Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ AddonManager.SCOPE_USER);
+ }
+ }
+
+ if (enabledScopes & AddonManager.SCOPE_APPLICATION) {
+ addDirectoryInstallLocation(KEY_APP_GLOBAL, KEY_APPDIR,
+ [DIR_EXTENSIONS],
+ AddonManager.SCOPE_APPLICATION, true);
+ }
+
+ if (enabledScopes & AddonManager.SCOPE_SYSTEM) {
+ addDirectoryInstallLocation(KEY_APP_SYSTEM_SHARE, "XRESysSExtPD",
+ [Services.appinfo.ID],
+ AddonManager.SCOPE_SYSTEM, true);
+ addDirectoryInstallLocation(KEY_APP_SYSTEM_LOCAL, "XRESysLExtPD",
+ [Services.appinfo.ID],
+ AddonManager.SCOPE_SYSTEM, true);
+ if (hasRegistry) {
+ addRegistryInstallLocation("winreg-app-global",
+ Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
+ AddonManager.SCOPE_SYSTEM);
+ }
+ }
+
+ let defaultPrefs = new Preferences({ defaultBranch: true });
+ this.defaultSkin = defaultPrefs.get(PREF_GENERAL_SKINS_SELECTEDSKIN,
+ "classic/1.0");
+ this.currentSkin = Preferences.get(PREF_GENERAL_SKINS_SELECTEDSKIN,
+ this.defaultSkin);
+ this.selectedSkin = this.currentSkin;
+ this.applyThemeChange();
+
+ this.minCompatibleAppVersion = Preferences.get(PREF_EM_MIN_COMPAT_APP_VERSION,
+ null);
+ this.minCompatiblePlatformVersion = Preferences.get(PREF_EM_MIN_COMPAT_PLATFORM_VERSION,
+ null);
+ this.enabledAddons = "";
+
+ Services.prefs.addObserver(PREF_EM_MIN_COMPAT_APP_VERSION, this, false);
+ Services.prefs.addObserver(PREF_EM_MIN_COMPAT_PLATFORM_VERSION, this, false);
+ Services.obs.addObserver(this, NOTIFICATION_FLUSH_PERMISSIONS, false);
+ if (Cu.isModuleLoaded("resource://gre/modules/devtools/ToolboxProcess.jsm")) {
+ // If BrowserToolboxProcess is already loaded, set the boolean to true
+ // and do whatever is needed
+ this._toolboxProcessLoaded = true;
+ BrowserToolboxProcess.on("connectionchange",
+ this.onDebugConnectionChange.bind(this));
+ }
+ else {
+ // Else, wait for it to load
+ Services.obs.addObserver(this, NOTIFICATION_TOOLBOXPROCESS_LOADED, false);
+ }
+
+ let flushCaches = this.checkForChanges(aAppChanged, aOldAppVersion,
+ aOldPlatformVersion);
+
+ // Changes to installed extensions may have changed which theme is selected
+ this.applyThemeChange();
+
+ AddonManagerPrivate.markProviderSafe(this);
+
+ if (aAppChanged === undefined) {
+ // For new profiles we will never need to show the add-on selection UI
+ Services.prefs.setBoolPref(PREF_SHOWN_SELECTION_UI, true);
+ }
+ else if (aAppChanged && !this.allAppGlobal &&
+ Preferences.get(PREF_EM_SHOW_MISMATCH_UI, true)) {
+ if (!Preferences.get(PREF_SHOWN_SELECTION_UI, false)) {
+ // Flip a flag to indicate that we interrupted startup with an interactive prompt
+ Services.startup.interrupted = true;
+ // This *must* be modal as it has to block startup.
+ var features = "chrome,centerscreen,dialog,titlebar,modal";
+ Services.ww.openWindow(null, URI_EXTENSION_SELECT_DIALOG, "", features, null);
+ Services.prefs.setBoolPref(PREF_SHOWN_SELECTION_UI, true);
+ // Ensure any changes to the add-ons list are flushed to disk
+ Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS,
+ !XPIDatabase.writeAddonsList());
+ }
+ else {
+ let addonsToUpdate = this.shouldForceUpdateCheck(aAppChanged);
+ if (addonsToUpdate) {
+ this.showUpgradeUI(addonsToUpdate);
+ flushCaches = true;
+ }
+ }
+ }
+
+ if (flushCaches) {
+ flushStartupCache();
+ // UI displayed early in startup (like the compatibility UI) may have
+ // caused us to cache parts of the skin or locale in memory. These must
+ // be flushed to allow extension provided skins and locales to take full
+ // effect
+ Services.obs.notifyObservers(null, "chrome-flush-skin-caches", null);
+ Services.obs.notifyObservers(null, "chrome-flush-caches", null);
+ }
+
+ this.enabledAddons = Preferences.get(PREF_EM_ENABLED_ADDONS, "");
+
+ if ("nsICrashReporter" in Ci &&
+ Services.appinfo instanceof Ci.nsICrashReporter) {
+ // Annotate the crash report with relevant add-on information.
+ try {
+ Services.appinfo.annotateCrashReport("Theme", this.currentSkin);
+ } catch (e) { }
+ try {
+ Services.appinfo.annotateCrashReport("EMCheckCompatibility",
+ AddonManager.checkCompatibility);
+ } catch (e) { }
+ this.addAddonsToCrashReporter();
+ }
+
+ try {
+ AddonManagerPrivate.recordTimestamp("XPI_bootstrap_addons_begin");
+ for (let id in this.bootstrappedAddons) {
+ try {
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.persistentDescriptor = this.bootstrappedAddons[id].descriptor;
+ let reason = BOOTSTRAP_REASONS.APP_STARTUP;
+ // Eventually set INSTALLED reason when a bootstrap addon
+ // is dropped in profile folder and automatically installed
+ if (AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_INSTALLED)
+ .indexOf(id) !== -1)
+ reason = BOOTSTRAP_REASONS.ADDON_INSTALL;
+ this.callBootstrapMethod(createAddonDetails(id, this.bootstrappedAddons[id]),
+ file, "startup", reason);
+ }
+ catch (e) {
+ logger.error("Failed to load bootstrap addon " + id + " from " +
+ this.bootstrappedAddons[id].descriptor, e);
+ }
+ }
+ AddonManagerPrivate.recordTimestamp("XPI_bootstrap_addons_end");
+ }
+ catch (e) {
+ logger.error("bootstrap startup failed", e);
+ AddonManagerPrivate.recordException("XPI-BOOTSTRAP", "startup failed", e);
+ }
+
+ // Let these shutdown a little earlier when they still have access to most
+ // of XPCOM
+ Services.obs.addObserver({
+ observe: function shutdownObserver(aSubject, aTopic, aData) {
+ XPIProvider._closing = true;
+ for (let id in XPIProvider.bootstrappedAddons) {
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.persistentDescriptor = XPIProvider.bootstrappedAddons[id].descriptor;
+ let addon = createAddonDetails(id, XPIProvider.bootstrappedAddons[id]);
+ XPIProvider.callBootstrapMethod(addon, file, "shutdown",
+ BOOTSTRAP_REASONS.APP_SHUTDOWN);
+ }
+ Services.obs.removeObserver(this, "quit-application-granted");
+ }
+ }, "quit-application-granted", false);
+
+ // Detect final-ui-startup for telemetry reporting
+ Services.obs.addObserver({
+ observe: function uiStartupObserver(aSubject, aTopic, aData) {
+ AddonManagerPrivate.recordTimestamp("XPI_finalUIStartup");
+ XPIProvider.runPhase = XPI_AFTER_UI_STARTUP;
+ Services.obs.removeObserver(this, "final-ui-startup");
+ }
+ }, "final-ui-startup", false);
+
+ AddonManagerPrivate.recordTimestamp("XPI_startup_end");
+
+ this.extensionsActive = true;
+ this.runPhase = XPI_BEFORE_UI_STARTUP;
+ }
+ catch (e) {
+ logger.error("startup failed", e);
+ AddonManagerPrivate.recordException("XPI", "startup failed", e);
+ }
+ },
+
+ /**
+ * Shuts down the database and releases all references.
+ * Return: Promise{integer} resolves / rejects with the result of
+ * flushing the XPI Database if it was loaded,
+ * 0 otherwise.
+ */
+ shutdown: function XPI_shutdown() {
+ logger.debug("shutdown");
+
+ // Stop anything we were doing asynchronously
+ this.cancelAll();
+
+ this.bootstrappedAddons = {};
+ this.bootstrapScopes = {};
+ this.enabledAddons = null;
+ this.allAppGlobal = true;
+
+ // If there are pending operations then we must update the list of active
+ // add-ons
+ if (Preferences.get(PREF_PENDING_OPERATIONS, false)) {
+ AddonManagerPrivate.recordSimpleMeasure("XPIDB_pending_ops", 1);
+ XPIDatabase.updateActiveAddons();
+ Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS,
+ !XPIDatabase.writeAddonsList());
+ }
+
+ this.installs = null;
+ this.installLocations = null;
+ this.installLocationsByName = null;
+
+ // This is needed to allow xpcshell tests to simulate a restart
+ this.extensionsActive = false;
+ this._addonFileMap.clear();
+
+ if (gLazyObjectsLoaded) {
+ let done = XPIDatabase.shutdown();
+ done.then(
+ ret => {
+ logger.debug("Notifying XPI shutdown observers");
+ Services.obs.notifyObservers(null, "xpi-provider-shutdown", null);
+ },
+ err => {
+ logger.debug("Notifying XPI shutdown observers");
+ this._shutdownError = err;
+ Services.obs.notifyObservers(null, "xpi-provider-shutdown", err);
+ }
+ );
+ return done;
+ }
+ else {
+ logger.debug("Notifying XPI shutdown observers");
+ Services.obs.notifyObservers(null, "xpi-provider-shutdown", null);
+ }
+ },
+
+ /**
+ * Applies any pending theme change to the preferences.
+ */
+ applyThemeChange: function XPI_applyThemeChange() {
+ if (!Preferences.get(PREF_DSS_SWITCHPENDING, false))
+ return;
+
+ // Tell the Chrome Registry which Skin to select
+ try {
+ this.selectedSkin = Preferences.get(PREF_DSS_SKIN_TO_SELECT);
+ Services.prefs.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN,
+ this.selectedSkin);
+ Services.prefs.clearUserPref(PREF_DSS_SKIN_TO_SELECT);
+ logger.debug("Changed skin to " + this.selectedSkin);
+ this.currentSkin = this.selectedSkin;
+ }
+ catch (e) {
+ logger.error("Error applying theme change", e);
+ }
+ Services.prefs.clearUserPref(PREF_DSS_SWITCHPENDING);
+ },
+
+ /**
+ * If the application has been upgraded and there are add-ons outside the
+ * application directory then we may need to synchronize compatibility
+ * information but only if the mismatch UI isn't disabled.
+ *
+ * @returns False if no update check is needed, otherwise an array of add-on
+ * IDs to check for updates. Array may be empty if no add-ons can be/need
+ * to be updated, but the metadata check needs to be performed.
+ */
+ shouldForceUpdateCheck: function XPI_shouldForceUpdateCheck(aAppChanged) {
+ AddonManagerPrivate.recordSimpleMeasure("XPIDB_metadata_age", AddonRepository.metadataAge());
+
+ let startupChanges = AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_DISABLED);
+ logger.debug("shouldForceUpdateCheck startupChanges: " + startupChanges.toSource());
+ AddonManagerPrivate.recordSimpleMeasure("XPIDB_startup_disabled", startupChanges.length);
+
+ let forceUpdate = [];
+ if (startupChanges.length > 0) {
+ let addons = XPIDatabase.getAddons();
+ for (let addon of addons) {
+ if ((startupChanges.indexOf(addon.id) != -1) &&
+ (addon.permissions() & AddonManager.PERM_CAN_UPGRADE)) {
+ logger.debug("shouldForceUpdateCheck: can upgrade disabled add-on " + addon.id);
+ forceUpdate.push(addon.id);
+ }
+ }
+ }
+
+ if (AddonRepository.isMetadataStale()) {
+ logger.debug("shouldForceUpdateCheck: metadata is stale");
+ return forceUpdate;
+ }
+ if (forceUpdate.length > 0) {
+ return forceUpdate;
+ }
+
+ return false;
+ },
+
+ /**
+ * Shows the "Compatibility Updates" UI.
+ *
+ * @param aAddonIDs
+ * Array opf addon IDs that were disabled by the application update, and
+ * should therefore be checked for updates.
+ */
+ showUpgradeUI: function XPI_showUpgradeUI(aAddonIDs) {
+ logger.debug("XPI_showUpgradeUI: " + aAddonIDs.toSource());
+ // Flip a flag to indicate that we interrupted startup with an interactive prompt
+ Services.startup.interrupted = true;
+
+ var variant = Cc["@mozilla.org/variant;1"].
+ createInstance(Ci.nsIWritableVariant);
+ variant.setFromVariant(aAddonIDs);
+
+ // This *must* be modal as it has to block startup.
+ var features = "chrome,centerscreen,dialog,titlebar,modal";
+ var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].
+ getService(Ci.nsIWindowWatcher);
+ ww.openWindow(null, URI_EXTENSION_UPDATE_DIALOG, "", features, variant);
+
+ // Ensure any changes to the add-ons list are flushed to disk
+ Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS,
+ !XPIDatabase.writeAddonsList());
+ },
+
+ /**
+ * Persists changes to XPIProvider.bootstrappedAddons to its store (a pref).
+ */
+ persistBootstrappedAddons: function XPI_persistBootstrappedAddons() {
+ // Experiments are disabled upon app load, so don't persist references.
+ let filtered = {};
+ for (let id in this.bootstrappedAddons) {
+ let entry = this.bootstrappedAddons[id];
+ if (entry.type == "experiment") {
+ continue;
+ }
+
+ filtered[id] = entry;
+ }
+
+ Services.prefs.setCharPref(PREF_BOOTSTRAP_ADDONS,
+ JSON.stringify(filtered));
+ },
+
+ /**
+ * Adds a list of currently active add-ons to the next crash report.
+ */
+ addAddonsToCrashReporter: function XPI_addAddonsToCrashReporter() {
+ if (!("nsICrashReporter" in Ci) ||
+ !(Services.appinfo instanceof Ci.nsICrashReporter))
+ return;
+
+ // In safe mode no add-ons are loaded so we should not include them in the
+ // crash report
+ if (Services.appinfo.inSafeMode)
+ return;
+
+ let data = this.enabledAddons;
+ for (let id in this.bootstrappedAddons) {
+ data += (data ? "," : "") + encodeURIComponent(id) + ":" +
+ encodeURIComponent(this.bootstrappedAddons[id].version);
+ }
+
+ try {
+ Services.appinfo.annotateCrashReport("Add-ons", data);
+ }
+ catch (e) { }
+
+ let TelemetrySession =
+ Cu.import("resource://gre/modules/TelemetrySession.jsm", {}).TelemetrySession;
+ TelemetrySession.setAddOns(data);
+ },
+
+ /**
+ * Check the staging directories of install locations for any add-ons to be
+ * installed or add-ons to be uninstalled.
+ *
+ * @param aManifests
+ * A dictionary to add detected install manifests to for the purpose
+ * of passing through updated compatibility information
+ * @return true if an add-on was installed or uninstalled
+ */
+ processPendingFileChanges: function XPI_processPendingFileChanges(aManifests) {
+ let changed = false;
+ this.installLocations.forEach(function(aLocation) {
+ aManifests[aLocation.name] = {};
+ // We can't install or uninstall anything in locked locations
+ if (aLocation.locked)
+ return;
+
+ let stagedXPIDir = aLocation.getXPIStagingDir();
+ let stagingDir = aLocation.getStagingDir();
+
+ if (stagedXPIDir.exists() && stagedXPIDir.isDirectory()) {
+ let entries = stagedXPIDir.directoryEntries
+ .QueryInterface(Ci.nsIDirectoryEnumerator);
+ while (entries.hasMoreElements()) {
+ let stageDirEntry = entries.nextFile;
+
+ if (!stageDirEntry.isDirectory()) {
+ logger.warn("Ignoring file in XPI staging directory: " + stageDirEntry.path);
+ continue;
+ }
+
+ // Find the last added XPI file in the directory
+ let stagedXPI = null;
+ var xpiEntries = stageDirEntry.directoryEntries
+ .QueryInterface(Ci.nsIDirectoryEnumerator);
+ while (xpiEntries.hasMoreElements()) {
+ let file = xpiEntries.nextFile;
+ if (file.isDirectory())
+ continue;
+
+ let extension = file.leafName;
+ extension = extension.substring(extension.length - 4);
+
+ if (extension != ".xpi" && extension != ".jar")
+ continue;
+
+ stagedXPI = file;
+ }
+ xpiEntries.close();
+
+ if (!stagedXPI)
+ continue;
+
+ let addon = null;
+ try {
+ addon = loadManifestFromZipFile(stagedXPI);
+ }
+ catch (e) {
+ logger.error("Unable to read add-on manifest from " + stagedXPI.path, e);
+ continue;
+ }
+
+ logger.debug("Migrating staged install of " + addon.id + " in " + aLocation.name);
+
+ if (addon.unpack || Preferences.get(PREF_XPI_UNPACK, false)) {
+ let targetDir = stagingDir.clone();
+ targetDir.append(addon.id);
+ try {
+ targetDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ }
+ catch (e) {
+ logger.error("Failed to create staging directory for add-on " + addon.id, e);
+ continue;
+ }
+
+ try {
+ ZipUtils.extractFiles(stagedXPI, targetDir);
+ }
+ catch (e) {
+ logger.error("Failed to extract staged XPI for add-on " + addon.id + " in " +
+ aLocation.name, e);
+ }
+ }
+ else {
+ try {
+ stagedXPI.moveTo(stagingDir, addon.id + ".xpi");
+ }
+ catch (e) {
+ logger.error("Failed to move staged XPI for add-on " + addon.id + " in " +
+ aLocation.name, e);
+ }
+ }
+ }
+ entries.close();
+ }
+
+ if (stagedXPIDir.exists()) {
+ try {
+ recursiveRemove(stagedXPIDir);
+ }
+ catch (e) {
+ // Non-critical, just saves some perf on startup if we clean this up.
+ logger.debug("Error removing XPI staging dir " + stagedXPIDir.path, e);
+ }
+ }
+
+ try {
+ if (!stagingDir || !stagingDir.exists() || !stagingDir.isDirectory())
+ return;
+ }
+ catch (e) {
+ logger.warn("Failed to find staging directory", e);
+ return;
+ }
+
+ let seenFiles = [];
+ // Use a snapshot of the directory contents to avoid possible issues with
+ // iterating over a directory while removing files from it (the YAFFS2
+ // embedded filesystem has this issue, see bug 772238), and to remove
+ // normal files before their resource forks on OSX (see bug 733436).
+ let stagingDirEntries = getDirectoryEntries(stagingDir, true);
+ for (let stageDirEntry of stagingDirEntries) {
+ let id = stageDirEntry.leafName;
+
+ let isDir;
+ try {
+ isDir = stageDirEntry.isDirectory();
+ }
+ catch (e if e.result == Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) {
+ // If the file has already gone away then don't worry about it, this
+ // can happen on OSX where the resource fork is automatically moved
+ // with the data fork for the file. See bug 733436.
+ continue;
+ }
+
+ if (!isDir) {
+ if (id.substring(id.length - 4).toLowerCase() == ".xpi") {
+ id = id.substring(0, id.length - 4);
+ }
+ else {
+ if (id.substring(id.length - 5).toLowerCase() != ".json") {
+ logger.warn("Ignoring file: " + stageDirEntry.path);
+ seenFiles.push(stageDirEntry.leafName);
+ }
+ continue;
+ }
+ }
+
+ // Check that the directory's name is a valid ID.
+ if (!gIDTest.test(id)) {
+ logger.warn("Ignoring directory whose name is not a valid add-on ID: " +
+ stageDirEntry.path);
+ seenFiles.push(stageDirEntry.leafName);
+ continue;
+ }
+
+ changed = true;
+
+ if (isDir) {
+ // Check if the directory contains an install manifest.
+ let manifest = stageDirEntry.clone();
+ manifest.append(FILE_INSTALL_MANIFEST);
+
+ // If the install manifest doesn't exist uninstall this add-on in this
+ // install location.
+ if (!manifest.exists()) {
+ logger.debug("Processing uninstall of " + id + " in " + aLocation.name);
+
+ try {
+ let addonFile = aLocation.getLocationForID(id);
+ let addonToUninstall = loadManifestFromFile(addonFile, aLocation);
+ if (addonToUninstall.bootstrap) {
+ this.callBootstrapMethod(addonToUninstall, addonToUninstall._sourceBundle,
+ "uninstall", BOOTSTRAP_REASONS.ADDON_UNINSTALL);
+ }
+ }
+ catch (e) {
+ logger.warn("Failed to call uninstall for " + id, e);
+ }
+
+ try {
+ aLocation.uninstallAddon(id);
+ seenFiles.push(stageDirEntry.leafName);
+ }
+ catch (e) {
+ logger.error("Failed to uninstall add-on " + id + " in " + aLocation.name, e);
+ }
+ // The file check later will spot the removal and cleanup the database
+ continue;
+ }
+ }
+
+ aManifests[aLocation.name][id] = null;
+ let existingAddonID = id;
+
+ let jsonfile = stagingDir.clone();
+ jsonfile.append(id + ".json");
+
+ try {
+ aManifests[aLocation.name][id] = loadManifestFromFile(stageDirEntry);
+ }
+ catch (e) {
+ logger.error("Unable to read add-on manifest from " + stageDirEntry.path, e);
+ // This add-on can't be installed so just remove it now
+ seenFiles.push(stageDirEntry.leafName);
+ seenFiles.push(jsonfile.leafName);
+ continue;
+ }
+
+ // Check for a cached metadata for this add-on, it may contain updated
+ // compatibility information
+ if (jsonfile.exists()) {
+ logger.debug("Found updated metadata for " + id + " in " + aLocation.name);
+ let fis = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+ let json = Cc["@mozilla.org/dom/json;1"].
+ createInstance(Ci.nsIJSON);
+
+ try {
+ fis.init(jsonfile, -1, 0, 0);
+ let metadata = json.decodeFromStream(fis, jsonfile.fileSize);
+ aManifests[aLocation.name][id].importMetadata(metadata);
+ }
+ catch (e) {
+ // If some data can't be recovered from the cached metadata then it
+ // is unlikely to be a problem big enough to justify throwing away
+ // the install, just log and error and continue
+ logger.error("Unable to read metadata from " + jsonfile.path, e);
+ }
+ finally {
+ fis.close();
+ }
+ }
+ seenFiles.push(jsonfile.leafName);
+
+ existingAddonID = aManifests[aLocation.name][id].existingAddonID || id;
+
+ var oldBootstrap = null;
+ logger.debug("Processing install of " + id + " in " + aLocation.name);
+ if (existingAddonID in this.bootstrappedAddons) {
+ try {
+ var existingAddon = aLocation.getLocationForID(existingAddonID);
+ if (this.bootstrappedAddons[existingAddonID].descriptor ==
+ existingAddon.persistentDescriptor) {
+ oldBootstrap = this.bootstrappedAddons[existingAddonID];
+
+ // We'll be replacing a currently active bootstrapped add-on so
+ // call its uninstall method
+ let newVersion = aManifests[aLocation.name][id].version;
+ let oldVersion = oldBootstrap.version;
+ let uninstallReason = Services.vc.compare(oldVersion, newVersion) < 0 ?
+ BOOTSTRAP_REASONS.ADDON_UPGRADE :
+ BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
+
+ this.callBootstrapMethod(createAddonDetails(existingAddonID, oldBootstrap),
+ existingAddon, "uninstall", uninstallReason,
+ { newVersion: newVersion });
+ this.unloadBootstrapScope(existingAddonID);
+ flushStartupCache();
+ }
+ }
+ catch (e) {
+ }
+ }
+
+ try {
+ var addonInstallLocation = aLocation.installAddon(id, stageDirEntry,
+ existingAddonID);
+ if (aManifests[aLocation.name][id])
+ aManifests[aLocation.name][id]._sourceBundle = addonInstallLocation;
+ }
+ catch (e) {
+ logger.error("Failed to install staged add-on " + id + " in " + aLocation.name,
+ e);
+ // Re-create the staged install
+ AddonInstall.createStagedInstall(aLocation, stageDirEntry,
+ aManifests[aLocation.name][id]);
+ // Make sure not to delete the cached manifest json file
+ seenFiles.pop();
+
+ delete aManifests[aLocation.name][id];
+
+ if (oldBootstrap) {
+ // Re-install the old add-on
+ this.callBootstrapMethod(createAddonDetails(existingAddonID, oldBootstrap),
+ existingAddon, "install",
+ BOOTSTRAP_REASONS.ADDON_INSTALL);
+ }
+ continue;
+ }
+ }
+
+ try {
+ aLocation.cleanStagingDir(seenFiles);
+ }
+ catch (e) {
+ // Non-critical, just saves some perf on startup if we clean this up.
+ logger.debug("Error cleaning staging dir " + stagingDir.path, e);
+ }
+ }, this);
+ return changed;
+ },
+
+ /**
+ * Installs any add-ons located in the extensions directory of the
+ * application's distribution specific directory into the profile unless a
+ * newer version already exists or the user has previously uninstalled the
+ * distributed add-on.
+ *
+ * @param aManifests
+ * A dictionary to add new install manifests to to save having to
+ * reload them later
+ * @return true if any new add-ons were installed
+ */
+ installDistributionAddons: function XPI_installDistributionAddons(aManifests) {
+ let distroDir;
+ try {
+ distroDir = FileUtils.getDir(KEY_APP_DISTRIBUTION, [DIR_EXTENSIONS]);
+ }
+ catch (e) {
+ return false;
+ }
+
+ if (!distroDir.exists())
+ return false;
+
+ if (!distroDir.isDirectory())
+ return false;
+
+ let changed = false;
+ let profileLocation = this.installLocationsByName[KEY_APP_PROFILE];
+
+ let entries = distroDir.directoryEntries
+ .QueryInterface(Ci.nsIDirectoryEnumerator);
+ let entry;
+ while ((entry = entries.nextFile)) {
+
+ let id = entry.leafName;
+
+ if (entry.isFile()) {
+ if (id.substring(id.length - 4).toLowerCase() == ".xpi") {
+ id = id.substring(0, id.length - 4);
+ }
+ else {
+ logger.debug("Ignoring distribution add-on that isn't an XPI: " + entry.path);
+ continue;
+ }
+ }
+ else if (!entry.isDirectory()) {
+ logger.debug("Ignoring distribution add-on that isn't a file or directory: " +
+ entry.path);
+ continue;
+ }
+
+ if (!gIDTest.test(id)) {
+ logger.debug("Ignoring distribution add-on whose name is not a valid add-on ID: " +
+ entry.path);
+ continue;
+ }
+
+ let addon;
+ try {
+ addon = loadManifestFromFile(entry);
+ }
+ catch (e) {
+ logger.warn("File entry " + entry.path + " contains an invalid add-on", e);
+ continue;
+ }
+
+ if (addon.id != id) {
+ logger.warn("File entry " + entry.path + " contains an add-on with an " +
+ "incorrect ID")
+ continue;
+ }
+
+ let existingEntry = null;
+ try {
+ existingEntry = profileLocation.getLocationForID(id);
+ }
+ catch (e) {
+ }
+
+ if (existingEntry) {
+ let existingAddon;
+ try {
+ existingAddon = loadManifestFromFile(existingEntry);
+
+ if (Services.vc.compare(addon.version, existingAddon.version) <= 0)
+ continue;
+ }
+ catch (e) {
+ // Bad add-on in the profile so just proceed and install over the top
+ logger.warn("Profile contains an add-on with a bad or missing install " +
+ "manifest at " + existingEntry.path + ", overwriting", e);
+ }
+ }
+ else if (Preferences.get(PREF_BRANCH_INSTALLED_ADDON + id, false)) {
+ continue;
+ }
+
+ // Install the add-on
+ try {
+ profileLocation.installAddon(id, entry, null, true);
+ logger.debug("Installed distribution add-on " + id);
+
+ Services.prefs.setBoolPref(PREF_BRANCH_INSTALLED_ADDON + id, true)
+
+ // aManifests may contain a copy of a newly installed add-on's manifest
+ // and we'll have overwritten that so instead cache our install manifest
+ // which will later be put into the database in processFileChanges
+ if (!(KEY_APP_PROFILE in aManifests))
+ aManifests[KEY_APP_PROFILE] = {};
+ aManifests[KEY_APP_PROFILE][id] = addon;
+ changed = true;
+ }
+ catch (e) {
+ logger.error("Failed to install distribution add-on " + entry.path, e);
+ }
+ }
+
+ entries.close();
+
+ return changed;
+ },
+
+ /**
+ * Compares the add-ons that are currently installed to those that were
+ * known to be installed when the application last ran and applies any
+ * changes found to the database. Also sends "startupcache-invalidate" signal to
+ * observerservice if it detects that data may have changed.
+ * Always called after XPIProviderUtils.js and extensions.json have been loaded.
+ *
+ * @param aManifests
+ * A dictionary of cached AddonInstalls for add-ons that have been
+ * installed
+ * @param aUpdateCompatibility
+ * true to update add-ons appDisabled property when the application
+ * version has changed
+ * @param aOldAppVersion
+ * The version of the application last run with this profile or null
+ * if it is a new profile or the version is unknown
+ * @param aOldPlatformVersion
+ * The version of the platform last run with this profile or null
+ * if it is a new profile or the version is unknown
+ * @return a boolean indicating if a change requiring flushing the caches was
+ * detected
+ */
+ processFileChanges: function XPI_processFileChanges(aManifests,
+ aUpdateCompatibility,
+ aOldAppVersion,
+ aOldPlatformVersion) {
+ let visibleAddons = {};
+ let oldBootstrappedAddons = this.bootstrappedAddons;
+ this.bootstrappedAddons = {};
+
+ /**
+ * Updates an add-on's metadata and determines if a restart of the
+ * application is necessary. This is called when either the add-on's
+ * install directory path or last modified time has changed.
+ *
+ * @param aInstallLocation
+ * The install location containing the add-on
+ * @param aOldAddon
+ * The AddonInternal as it appeared the last time the application
+ * ran
+ * @param aAddonState
+ * The new state of the add-on
+ * @return a boolean indicating if flushing caches is required to complete
+ * changing this add-on
+ */
+ function updateMetadata(aInstallLocation, aOldAddon, aAddonState) {
+ logger.debug("Add-on " + aOldAddon.id + " modified in " + aInstallLocation.name);
+
+ // Check if there is an updated install manifest for this add-on
+ let newAddon = aManifests[aInstallLocation.name][aOldAddon.id];
+
+ try {
+ // If not load it
+ if (!newAddon) {
+ let file = aInstallLocation.getLocationForID(aOldAddon.id);
+ newAddon = loadManifestFromFile(file);
+ applyBlocklistChanges(aOldAddon, newAddon);
+
+ // Carry over any pendingUninstall state to add-ons modified directly
+ // in the profile. This is important when the attempt to remove the
+ // add-on in processPendingFileChanges failed and caused an mtime
+ // change to the add-ons files.
+ newAddon.pendingUninstall = aOldAddon.pendingUninstall;
+ }
+
+ // The ID in the manifest that was loaded must match the ID of the old
+ // add-on.
+ if (newAddon.id != aOldAddon.id)
+ throw new Error("Incorrect id in install manifest for existing add-on " + aOldAddon.id);
+ }
+ catch (e) {
+ logger.warn("updateMetadata: Add-on " + aOldAddon.id + " is invalid", e);
+ XPIDatabase.removeAddonMetadata(aOldAddon);
+ XPIStates.removeAddon(aOldAddon.location, aOldAddon.id);
+ if (!aInstallLocation.locked)
+ aInstallLocation.uninstallAddon(aOldAddon.id);
+ else
+ logger.warn("Could not uninstall invalid item from locked install location");
+ // If this was an active add-on then we must force a restart
+ if (aOldAddon.active)
+ return true;
+
+ return false;
+ }
+
+ // Set the additional properties on the new AddonInternal
+ newAddon._installLocation = aInstallLocation;
+ newAddon.updateDate = aAddonState.mtime;
+ newAddon.visible = !(newAddon.id in visibleAddons);
+
+ // Update the database
+ let newDBAddon = XPIDatabase.updateAddonMetadata(aOldAddon, newAddon,
+ aAddonState.descriptor);
+ if (newDBAddon.visible) {
+ visibleAddons[newDBAddon.id] = newDBAddon;
+ // Remember add-ons that were changed during startup
+ AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED,
+ newDBAddon.id);
+
+ // If this was the active theme and it is now disabled then enable the
+ // default theme
+ if (aOldAddon.active && newDBAddon.disabled)
+ XPIProvider.enableDefaultTheme();
+
+ // If the new add-on is bootstrapped and active then call its install method
+ if (newDBAddon.active && newDBAddon.bootstrap) {
+ // Startup cache must be flushed before calling the bootstrap script
+ flushStartupCache();
+
+ let installReason = Services.vc.compare(aOldAddon.version, newDBAddon.version) < 0 ?
+ BOOTSTRAP_REASONS.ADDON_UPGRADE :
+ BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
+
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.persistentDescriptor = aAddonState.descriptor;
+ XPIProvider.callBootstrapMethod(newDBAddon, file, "install",
+ installReason, { oldVersion: aOldAddon.version });
+ return false;
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Updates an add-on's descriptor for when the add-on has moved in the
+ * filesystem but hasn't changed in any other way.
+ *
+ * @param aInstallLocation
+ * The install location containing the add-on
+ * @param aOldAddon
+ * The AddonInternal as it appeared the last time the application
+ * ran
+ * @param aAddonState
+ * The new state of the add-on
+ * @return a boolean indicating if flushing caches is required to complete
+ * changing this add-on
+ */
+ function updateDescriptor(aInstallLocation, aOldAddon, aAddonState) {
+ logger.debug("Add-on " + aOldAddon.id + " moved to " + aAddonState.descriptor);
+
+ aOldAddon.descriptor = aAddonState.descriptor;
+ aOldAddon.visible = !(aOldAddon.id in visibleAddons);
+ XPIDatabase.saveChanges();
+
+ if (aOldAddon.visible) {
+ visibleAddons[aOldAddon.id] = aOldAddon;
+
+ if (aOldAddon.bootstrap && aOldAddon.active) {
+ let bootstrap = oldBootstrappedAddons[aOldAddon.id];
+ bootstrap.descriptor = aAddonState.descriptor;
+ XPIProvider.bootstrappedAddons[aOldAddon.id] = bootstrap;
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Called when no change has been detected for an add-on's metadata. The
+ * add-on may have become visible due to other add-ons being removed or
+ * the add-on may need to be updated when the application version has
+ * changed.
+ *
+ * @param aInstallLocation
+ * The install location containing the add-on
+ * @param aOldAddon
+ * The AddonInternal as it appeared the last time the application
+ * ran
+ * @param aAddonState
+ * The new state of the add-on
+ * @return a boolean indicating if flushing caches is required to complete
+ * changing this add-on
+ */
+ function updateVisibilityAndCompatibility(aInstallLocation, aOldAddon,
+ aAddonState) {
+ let changed = false;
+
+ // This add-ons metadata has not changed but it may have become visible
+ if (!(aOldAddon.id in visibleAddons)) {
+ visibleAddons[aOldAddon.id] = aOldAddon;
+
+ if (!aOldAddon.visible) {
+ // Remember add-ons that were changed during startup.
+ AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED,
+ aOldAddon.id);
+ XPIDatabase.makeAddonVisible(aOldAddon);
+
+ if (aOldAddon.bootstrap) {
+ // The add-on is bootstrappable so call its install script
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.persistentDescriptor = aAddonState.descriptor;
+ XPIProvider.callBootstrapMethod(aOldAddon, file,
+ "install",
+ BOOTSTRAP_REASONS.ADDON_INSTALL);
+
+ // If it should be active then mark it as active otherwise unload
+ // its scope
+ if (!aOldAddon.disabled) {
+ XPIDatabase.updateAddonActive(aOldAddon, true);
+ }
+ else {
+ XPIProvider.unloadBootstrapScope(newAddon.id);
+ }
+ }
+ else {
+ // Otherwise a restart is necessary
+ changed = true;
+ }
+ }
+ }
+
+ // App version changed, we may need to update the appDisabled property.
+ if (aUpdateCompatibility) {
+ let wasDisabled = aOldAddon.disabled;
+ let wasAppDisabled = aOldAddon.appDisabled;
+ let wasUserDisabled = aOldAddon.userDisabled;
+ let wasSoftDisabled = aOldAddon.softDisabled;
+
+ // This updates the addon's JSON cached data in place
+ applyBlocklistChanges(aOldAddon, aOldAddon, aOldAppVersion,
+ aOldPlatformVersion);
+ aOldAddon.appDisabled = !isUsableAddon(aOldAddon);
+
+ let isDisabled = aOldAddon.disabled;
+
+ // If either property has changed update the database.
+ if (wasAppDisabled != aOldAddon.appDisabled ||
+ wasUserDisabled != aOldAddon.userDisabled ||
+ wasSoftDisabled != aOldAddon.softDisabled) {
+ logger.debug("Add-on " + aOldAddon.id + " changed appDisabled state to " +
+ aOldAddon.appDisabled + ", userDisabled state to " +
+ aOldAddon.userDisabled + " and softDisabled state to " +
+ aOldAddon.softDisabled);
+ XPIDatabase.saveChanges();
+ }
+
+ // If this is a visible add-on and it has changed disabled state then we
+ // may need a restart or to update the bootstrap list.
+ if (aOldAddon.visible && wasDisabled != isDisabled) {
+ // Remember add-ons that became disabled or enabled by the application
+ // change
+ let change = isDisabled ? AddonManager.STARTUP_CHANGE_DISABLED
+ : AddonManager.STARTUP_CHANGE_ENABLED;
+ AddonManagerPrivate.addStartupChange(change, aOldAddon.id);
+ if (aOldAddon.bootstrap) {
+ // Update the add-ons active state
+ XPIDatabase.updateAddonActive(aOldAddon, !isDisabled);
+ }
+ else {
+ changed = true;
+ }
+ }
+ }
+
+ if (aOldAddon.visible && aOldAddon.active && aOldAddon.bootstrap) {
+ XPIProvider.bootstrappedAddons[aOldAddon.id] = {
+ version: aOldAddon.version,
+ type: aOldAddon.type,
+ descriptor: aAddonState.descriptor,
+ multiprocessCompatible: aOldAddon.multiprocessCompatible
+ };
+ }
+
+ return changed;
+ }
+
+ /**
+ * Called when an add-on has been removed.
+ *
+ * @param aOldAddon
+ * The AddonInternal as it appeared the last time the application
+ * ran
+ * @return a boolean indicating if flushing caches is required to complete
+ * changing this add-on
+ */
+ function removeMetadata(aOldAddon) {
+ // This add-on has disappeared
+ logger.debug("Add-on " + aOldAddon.id + " removed from " + aOldAddon.location);
+ XPIDatabase.removeAddonMetadata(aOldAddon);
+
+ // Remember add-ons that were uninstalled during startup
+ if (aOldAddon.visible) {
+ AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_UNINSTALLED,
+ aOldAddon.id);
+ }
+ else if (AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_INSTALLED)
+ .indexOf(aOldAddon.id) != -1) {
+ AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED,
+ aOldAddon.id);
+ }
+
+ if (aOldAddon.active) {
+ // Enable the default theme if the previously active theme has been
+ // removed
+ if (aOldAddon.type == "theme")
+ XPIProvider.enableDefaultTheme();
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Called to add the metadata for an add-on in one of the install locations
+ * to the database. This can be called in three different cases. Either an
+ * add-on has been dropped into the location from outside of Firefox, or
+ * an add-on has been installed through the application, or the database
+ * has been upgraded or become corrupt and add-on data has to be reloaded
+ * into it.
+ *
+ * @param aInstallLocation
+ * The install location containing the add-on
+ * @param aId
+ * The ID of the add-on
+ * @param aAddonState
+ * The new state of the add-on
+ * @param aMigrateData
+ * If during startup the database had to be upgraded this will
+ * contain data that used to be held about this add-on
+ * @return a boolean indicating if flushing caches is required to complete
+ * changing this add-on
+ */
+ function addMetadata(aInstallLocation, aId, aAddonState, aMigrateData) {
+ logger.debug("New add-on " + aId + " installed in " + aInstallLocation.name);
+
+ let newAddon = null;
+ let sameVersion = false;
+ // Check the updated manifests lists for the install location, If there
+ // is no manifest for the add-on ID then newAddon will be undefined
+ if (aInstallLocation.name in aManifests)
+ newAddon = aManifests[aInstallLocation.name][aId];
+
+ // If we had staged data for this add-on or we aren't recovering from a
+ // corrupt database and we don't have migration data for this add-on then
+ // this must be a new install.
+ let isNewInstall = (!!newAddon) || (!XPIDatabase.activeBundles && !aMigrateData);
+
+ // If it's a new install and we haven't yet loaded the manifest then it
+ // must be something dropped directly into the install location
+ let isDetectedInstall = isNewInstall && !newAddon;
+
+ // Load the manifest if necessary and sanity check the add-on ID
+ try {
+ if (!newAddon) {
+ // Load the manifest from the add-on.
+ let file = aInstallLocation.getLocationForID(aId);
+ newAddon = loadManifestFromFile(file);
+ }
+ // The add-on in the manifest should match the add-on ID.
+ if (newAddon.id != aId) {
+ throw new Error("Invalid addon ID: expected addon ID " + aId +
+ ", found " + newAddon.id + " in manifest");
+ }
+ }
+ catch (e) {
+ logger.warn("addMetadata: Add-on " + aId + " is invalid", e);
+
+ // Remove the invalid add-on from the install location if the install
+ // location isn't locked, no restart will be necessary
+ if (!aInstallLocation.locked)
+ aInstallLocation.uninstallAddon(aId);
+ else
+ logger.warn("Could not uninstall invalid item from locked install location");
+ return false;
+ }
+
+ // Update the AddonInternal properties.
+ newAddon._installLocation = aInstallLocation;
+ newAddon.visible = !(newAddon.id in visibleAddons);
+ newAddon.installDate = aAddonState.mtime;
+ newAddon.updateDate = aAddonState.mtime;
+ newAddon.foreignInstall = isDetectedInstall;
+
+ if (aMigrateData) {
+ // If there is migration data then apply it.
+ logger.debug("Migrating data from old database");
+
+ DB_MIGRATE_METADATA.forEach(function(aProp) {
+ // A theme's disabled state is determined by the selected theme
+ // preference which is read in loadManifestFromRDF
+ if (aProp == "userDisabled" && newAddon.type == "theme")
+ return;
+
+ if (aProp in aMigrateData)
+ newAddon[aProp] = aMigrateData[aProp];
+ });
+
+ // Force all non-profile add-ons to be foreignInstalls since they can't
+ // have been installed through the API
+ newAddon.foreignInstall |= aInstallLocation.name != KEY_APP_PROFILE;
+
+ // Some properties should only be migrated if the add-on hasn't changed.
+ // The version property isn't a perfect check for this but covers the
+ // vast majority of cases.
+ if (aMigrateData.version == newAddon.version) {
+ logger.debug("Migrating compatibility info");
+ sameVersion = true;
+ if ("targetApplications" in aMigrateData)
+ newAddon.applyCompatibilityUpdate(aMigrateData, true);
+ }
+
+ // Since the DB schema has changed make sure softDisabled is correct
+ applyBlocklistChanges(newAddon, newAddon, aOldAppVersion,
+ aOldPlatformVersion);
+ }
+
+ // The default theme is never a foreign install
+ if (newAddon.type == "theme" && newAddon.internalName == XPIProvider.defaultSkin)
+ newAddon.foreignInstall = false;
+
+ if (isDetectedInstall && newAddon.foreignInstall) {
+ // If the add-on is a foreign install and is in a scope where add-ons
+ // that were dropped in should default to disabled then disable it
+ let disablingScopes = Preferences.get(PREF_EM_AUTO_DISABLED_SCOPES, 0);
+ if (aInstallLocation.scope & disablingScopes) {
+ logger.warn("Disabling foreign installed add-on " + newAddon.id + " in "
+ + aInstallLocation.name);
+ newAddon.userDisabled = true;
+ }
+ }
+
+ // If we have a list of what add-ons should be marked as active then use
+ // it to guess at migration data.
+ if (!isNewInstall && XPIDatabase.activeBundles) {
+ // For themes we know which is active by the current skin setting
+ if (newAddon.type == "theme")
+ newAddon.active = newAddon.internalName == XPIProvider.currentSkin;
+ else
+ newAddon.active = XPIDatabase.activeBundles.indexOf(aAddonState.descriptor) != -1;
+
+ // If the add-on wasn't active and it isn't already disabled in some way
+ // then it was probably either softDisabled or userDisabled
+ if (!newAddon.active && newAddon.visible && !newAddon.disabled) {
+ // If the add-on is softblocked then assume it is softDisabled
+ if (newAddon.blocklistState == Blocklist.STATE_SOFTBLOCKED)
+ newAddon.softDisabled = true;
+ else
+ newAddon.userDisabled = true;
+ }
+ }
+ else {
+ newAddon.active = (newAddon.visible && !newAddon.disabled);
+ }
+
+ let newDBAddon = XPIDatabase.addAddonMetadata(newAddon, aAddonState.descriptor);
+
+ if (newDBAddon.visible) {
+ // Remember add-ons that were first detected during startup.
+ if (isDetectedInstall) {
+ // If a copy from a higher priority location was removed then this
+ // add-on has changed
+ if (AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_UNINSTALLED)
+ .indexOf(newDBAddon.id) != -1) {
+ AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED,
+ newDBAddon.id);
+ }
+ else {
+ AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_INSTALLED,
+ newDBAddon.id);
+ }
+ }
+
+ // Note if any visible add-on is not in the application install location
+ if (newDBAddon._installLocation.name != KEY_APP_GLOBAL)
+ XPIProvider.allAppGlobal = false;
+
+ visibleAddons[newDBAddon.id] = newDBAddon;
+
+ let installReason = BOOTSTRAP_REASONS.ADDON_INSTALL;
+ let extraParams = {};
+
+ // Copy add-on details (enabled, bootstrap, version, etc) to XPIState.
+ aAddonState.syncWithDB(newDBAddon);
+
+ // If we're hiding a bootstrapped add-on then call its uninstall method
+ if (newDBAddon.id in oldBootstrappedAddons) {
+ let oldBootstrap = oldBootstrappedAddons[newDBAddon.id];
+ extraParams.oldVersion = oldBootstrap.version;
+ XPIProvider.bootstrappedAddons[newDBAddon.id] = oldBootstrap;
+
+ // If the old version is the same as the new version, or we're
+ // recovering from a corrupt DB, don't call uninstall and install
+ // methods.
+ if (sameVersion || !isNewInstall) {
+ logger.debug("addMetadata: early return, sameVersion " + sameVersion
+ + ", isNewInstall " + isNewInstall);
+ return false;
+ }
+
+ installReason = Services.vc.compare(oldBootstrap.version, newDBAddon.version) < 0 ?
+ BOOTSTRAP_REASONS.ADDON_UPGRADE :
+ BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
+
+ let oldAddonFile = Cc["@mozilla.org/file/local;1"].
+ createInstance(Ci.nsIFile);
+ oldAddonFile.persistentDescriptor = oldBootstrap.descriptor;
+
+ XPIProvider.callBootstrapMethod(createAddonDetails(newDBAddon.id, oldBootstrap),
+ oldAddonFile, "uninstall", installReason,
+ { newVersion: newDBAddon.version });
+
+ XPIProvider.unloadBootstrapScope(newDBAddon.id);
+
+ // If the new add-on is bootstrapped then we must flush the caches
+ // before calling the new bootstrap script
+ if (newDBAddon.bootstrap)
+ flushStartupCache();
+ }
+
+ if (!newDBAddon.bootstrap)
+ return true;
+
+ // Visible bootstrapped add-ons need to have their install method called
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.persistentDescriptor = aAddonState.descriptor;
+ XPIProvider.callBootstrapMethod(newDBAddon, file,
+ "install", installReason, extraParams);
+ if (!newDBAddon.active)
+ XPIProvider.unloadBootstrapScope(newDBAddon.id);
+ }
+
+ return false;
+ }
+
+ let changed = false;
+
+ // Get all the add-ons in the existing DB and Map them into Sets by install location
+ let allDBAddons = new Map();
+ for (let a of XPIDatabase.getAddons()) {
+ let locationSet = allDBAddons.get(a.location);
+ if (!locationSet) {
+ locationSet = new Set();
+ allDBAddons.set(a.location, locationSet);
+ }
+ locationSet.add(a);
+ }
+
+ for (let installLocation of this.installLocations) {
+ // Get all the on-disk XPI states for this location, and keep track of which
+ // ones we see in the database.
+ let states = XPIStates.getLocation(installLocation.name);
+ let seen = new Set();
+ // Iterate through the add-ons installed the last time the application
+ // ran
+ let dbAddons = allDBAddons.get(installLocation.name);
+ if (dbAddons) {
+ // we've processed this location
+ allDBAddons.delete(installLocation.name);
+
+ logger.debug("processFileChanges reconciling DB for location ${l} state ${s} db ${d}",
+ {l: installLocation.name, s: states, d: [for (a of dbAddons) a.id]});
+ for (let aOldAddon of dbAddons) {
+ // If a version of this add-on has been installed in an higher
+ // priority install location then count it as changed
+ if (AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_INSTALLED)
+ .indexOf(aOldAddon.id) != -1) {
+ AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED,
+ aOldAddon.id);
+ }
+
+ // Check if the add-on is still installed
+ let xpiState = states && states.get(aOldAddon.id);
+ if (xpiState) {
+ // in this block, the add-on is in both XPIStates and the DB
+ seen.add(xpiState);
+
+ // The add-on has changed if the modification time has changed, or
+ // we have an updated manifest for it. Also reload the metadata for
+ // add-ons in the application directory when the application version
+ // has changed
+ if (aOldAddon.id in aManifests[installLocation.name] ||
+ aOldAddon.updateDate != xpiState.mtime ||
+ (aUpdateCompatibility && installLocation.name == KEY_APP_GLOBAL)) {
+ changed = updateMetadata(installLocation, aOldAddon, xpiState) ||
+ changed;
+ }
+ else if (aOldAddon.descriptor != xpiState.descriptor) {
+ changed = updateDescriptor(installLocation, aOldAddon, xpiState) ||
+ changed;
+ }
+ else {
+ changed = updateMetadata(installLocation, aOldAddon, xpiState) ||
+ changed;
+
+ changed = updateVisibilityAndCompatibility(installLocation,
+ aOldAddon, xpiState) ||
+ changed;
+ }
+ if (aOldAddon.visible && aOldAddon._installLocation.name != KEY_APP_GLOBAL)
+ XPIProvider.allAppGlobal = false;
+ // Copy add-on details (enabled, bootstrap, version, etc) to XPIState.
+ xpiState.syncWithDB(aOldAddon);
+ }
+ else {
+ // The add-on is in the DB, but not in xpiState (and thus not on disk).
+ changed = removeMetadata(aOldAddon) || changed;
+ }
+ }
+ }
+
+ // Any add-on in our current location that we haven't seen needs to
+ // be added to the database.
+ // Get the migration data for this install location so we can include that as
+ // we add, in case this is a database upgrade or rebuild.
+ let locMigrateData = {};
+ if (XPIDatabase.migrateData && installLocation.name in XPIDatabase.migrateData)
+ locMigrateData = XPIDatabase.migrateData[installLocation.name];
+ if (states) {
+ for (let [id, xpiState] of states) {
+ if (!seen.has(xpiState)) {
+ changed = addMetadata(installLocation, id, xpiState,
+ (locMigrateData[id] || null)) || changed;
+ }
+ }
+ }
+ }
+
+ // Anything left in allDBAddons is a location where the database contains add-ons,
+ // but the browser is no longer configured to use that location. The metadata for those
+ // add-ons must be removed from the database.
+ for (let [locationName, addons] of allDBAddons) {
+ logger.debug("Removing orphaned DB add-on entries from " + locationName);
+ for (let a of addons) {
+ logger.debug("Remove ${location}:${id}", a);
+ changed = removeMetadata(a) || changed;
+ }
+ }
+
+ XPIStates.save();
+ this.persistBootstrappedAddons();
+
+ // Clear out any cached migration data.
+ XPIDatabase.migrateData = null;
+
+ return changed;
+ },
+
+ /**
+ * Imports the xpinstall permissions from preferences into the permissions
+ * manager for the user to change later.
+ */
+ importPermissions: function XPI_importPermissions() {
+ PermissionsUtils.importFromPrefs(PREF_XPI_PERMISSIONS_BRANCH,
+ XPI_PERMISSION);
+ },
+
+ /**
+ * Checks for any changes that have occurred since the last time the
+ * application was launched.
+ *
+ * @param aAppChanged
+ * A tri-state value. Undefined means the current profile was created
+ * for this session, true means the profile already existed but was
+ * last used with an application with a different version number,
+ * false means that the profile was last used by this version of the
+ * application.
+ * @param aOldAppVersion
+ * The version of the application last run with this profile or null
+ * if it is a new profile or the version is unknown
+ * @param aOldPlatformVersion
+ * The version of the platform last run with this profile or null
+ * if it is a new profile or the version is unknown
+ * @return true if a change requiring a restart was detected
+ */
+ checkForChanges: function XPI_checkForChanges(aAppChanged, aOldAppVersion,
+ aOldPlatformVersion) {
+ logger.debug("checkForChanges");
+
+ // Keep track of whether and why we need to open and update the database at
+ // startup time.
+ let updateReasons = [];
+ if (aAppChanged) {
+ updateReasons.push("appChanged");
+ }
+
+ // Load the list of bootstrapped add-ons first so processFileChanges can
+ // modify it
+ // XXX eventually get rid of bootstrappedAddons
+ try {
+ this.bootstrappedAddons = JSON.parse(Preferences.get(PREF_BOOTSTRAP_ADDONS,
+ "{}"));
+ } catch (e) {
+ logger.warn("Error parsing enabled bootstrapped extensions cache", e);
+ }
+
+ // First install any new add-ons into the locations, if there are any
+ // changes then we must update the database with the information in the
+ // install locations
+ let manifests = {};
+ let updated = this.processPendingFileChanges(manifests);
+ if (updated) {
+ updateReasons.push("pendingFileChanges");
+ }
+
+ // This will be true if the previous session made changes that affect the
+ // active state of add-ons but didn't commit them properly (normally due
+ // to the application crashing)
+ let hasPendingChanges = Preferences.get(PREF_PENDING_OPERATIONS);
+ if (hasPendingChanges) {
+ updateReasons.push("hasPendingChanges");
+ }
+
+ // If the application has changed then check for new distribution add-ons
+ if (aAppChanged !== false &&
+ Preferences.get(PREF_INSTALL_DISTRO_ADDONS, true))
+ {
+ updated = this.installDistributionAddons(manifests);
+ if (updated) {
+ updateReasons.push("installDistributionAddons");
+ }
+ }
+
+ // Telemetry probe added around getInstallState() to check perf
+ let telemetryCaptureTime = Cu.now();
+ let installChanged = XPIStates.getInstallState();
+ let telemetry = Services.telemetry;
+ telemetry.getHistogramById("CHECK_ADDONS_MODIFIED_MS").add(Math.round(Cu.now() - telemetryCaptureTime));
+ if (installChanged) {
+ updateReasons.push("directoryState");
+ }
+
+ let haveAnyAddons = (XPIStates.size > 0);
+
+ // If the schema appears to have changed then we should update the database
+ if (DB_SCHEMA != Preferences.get(PREF_DB_SCHEMA, 0)) {
+ // If we don't have any add-ons, just update the pref, since we don't need to
+ // write the database
+ if (!haveAnyAddons) {
+ logger.debug("Empty XPI database, setting schema version preference to " + DB_SCHEMA);
+ Services.prefs.setIntPref(PREF_DB_SCHEMA, DB_SCHEMA);
+ }
+ else {
+ updateReasons.push("schemaChanged");
+ }
+ }
+
+ // If the database doesn't exist and there are add-ons installed then we
+ // must update the database however if there are no add-ons then there is
+ // no need to update the database.
+ let dbFile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_DATABASE], true);
+ if (!dbFile.exists() && haveAnyAddons) {
+ updateReasons.push("needNewDatabase");
+ }
+
+ // XXX This will go away when we fold bootstrappedAddons into XPIStates.
+ if (updateReasons.length == 0) {
+ let bootstrapDescriptors = new Set([for (b of Object.keys(this.bootstrappedAddons))
+ this.bootstrappedAddons[b].descriptor]);
+
+ for (let location of XPIStates.db.values()) {
+ for (let state of location.values()) {
+ bootstrapDescriptors.delete(state.descriptor);
+ }
+ }
+
+ if (bootstrapDescriptors.size > 0) {
+ logger.warn("Bootstrap state is invalid (missing add-ons: "
+ + [for (b of bootstrapDescriptors) b] + ")");
+ updateReasons.push("missingBootstrapAddon");
+ }
+ }
+
+ // Catch and log any errors during the main startup
+ try {
+ let extensionListChanged = false;
+ // If the database needs to be updated then open it and then update it
+ // from the filesystem
+ if (updateReasons.length > 0) {
+ AddonManagerPrivate.recordSimpleMeasure("XPIDB_startup_load_reasons", updateReasons);
+ XPIDatabase.syncLoadDB(false);
+ try {
+ extensionListChanged = this.processFileChanges(manifests,
+ aAppChanged,
+ aOldAppVersion,
+ aOldPlatformVersion);
+ }
+ catch (e) {
+ logger.error("Failed to process extension changes at startup", e);
+ }
+ }
+
+ if (aAppChanged) {
+ // When upgrading the app and using a custom skin make sure it is still
+ // compatible otherwise switch back the default
+ if (this.currentSkin != this.defaultSkin) {
+ let oldSkin = XPIDatabase.getVisibleAddonForInternalName(this.currentSkin);
+ if (!oldSkin || oldSkin.disabled)
+ this.enableDefaultTheme();
+ }
+
+ // When upgrading remove the old extensions cache to force older
+ // versions to rescan the entire list of extensions
+ try {
+ let oldCache = FileUtils.getFile(KEY_PROFILEDIR, [FILE_OLD_CACHE], true);
+ if (oldCache.exists())
+ oldCache.remove(true);
+ }
+ catch (e) {
+ logger.warn("Unable to remove old extension cache " + oldCache.path, e);
+ }
+ }
+
+ // If the application crashed before completing any pending operations then
+ // we should perform them now.
+ if (extensionListChanged || hasPendingChanges) {
+ logger.debug("Updating database with changes to installed add-ons");
+ XPIDatabase.updateActiveAddons();
+ Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS,
+ !XPIDatabase.writeAddonsList());
+ Services.prefs.setCharPref(PREF_BOOTSTRAP_ADDONS,
+ JSON.stringify(this.bootstrappedAddons));
+ return true;
+ }
+
+ logger.debug("No changes found");
+ }
+ catch (e) {
+ logger.error("Error during startup file checks", e);
+ }
+
+ // Check that the add-ons list still exists
+ let addonsList = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST],
+ true);
+ // the addons list file should exist if and only if we have add-ons installed
+ if (addonsList.exists() != haveAnyAddons) {
+ logger.debug("Add-ons list is invalid, rebuilding");
+ XPIDatabase.writeAddonsList();
+ }
+
+ return false;
+ },
+
+ /**
+ * Called to test whether this provider supports installing a particular
+ * mimetype.
+ *
+ * @param aMimetype
+ * The mimetype to check for
+ * @return true if the mimetype is application/x-xpinstall
+ */
+ supportsMimetype: function XPI_supportsMimetype(aMimetype) {
+ return aMimetype == "application/x-xpinstall";
+ },
+
+ /**
+ * Called to test whether installing XPI add-ons is enabled.
+ *
+ * @return true if installing is enabled
+ */
+ isInstallEnabled: function XPI_isInstallEnabled() {
+ // Default to enabled if the preference does not exist
+ return Preferences.get(PREF_XPI_ENABLED, true);
+ },
+
+ /**
+ * Called to test whether installing XPI add-ons by direct URL requests is
+ * whitelisted.
+ *
+ * @return true if installing by direct requests is whitelisted
+ */
+ isDirectRequestWhitelisted: function XPI_isDirectRequestWhitelisted() {
+ // Default to whitelisted if the preference does not exist.
+ return Preferences.get(PREF_XPI_DIRECT_WHITELISTED, true);
+ },
+
+ /**
+ * Called to test whether installing XPI add-ons from file referrers is
+ * whitelisted.
+ *
+ * @return true if installing from file referrers is whitelisted
+ */
+ isFileRequestWhitelisted: function XPI_isFileRequestWhitelisted() {
+ // Default to whitelisted if the preference does not exist.
+ return Preferences.get(PREF_XPI_FILE_WHITELISTED, true);
+ },
+
+ /**
+ * Called to test whether installing XPI add-ons from a URI is allowed.
+ *
+ * @param aInstallingPrincipal
+ * The nsIPrincipal that initiated the install
+ * @return true if installing is allowed
+ */
+ isInstallAllowed: function XPI_isInstallAllowed(aInstallingPrincipal) {
+ if (!this.isInstallEnabled())
+ return false;
+
+ let uri = aInstallingPrincipal.URI;
+
+ // Direct requests without a referrer are either whitelisted or blocked.
+ if (!uri)
+ return this.isDirectRequestWhitelisted();
+
+ // Local referrers can be whitelisted.
+ if (this.isFileRequestWhitelisted() &&
+ (uri.schemeIs("chrome") || uri.schemeIs("file")))
+ return true;
+
+ this.importPermissions();
+
+ let permission = Services.perms.testPermissionFromPrincipal(aInstallingPrincipal, XPI_PERMISSION);
+ if (permission == Ci.nsIPermissionManager.DENY_ACTION)
+ return false;
+
+ let requireWhitelist = Preferences.get(PREF_XPI_WHITELIST_REQUIRED, true);
+ if (requireWhitelist && (permission != Ci.nsIPermissionManager.ALLOW_ACTION))
+ return false;
+
+ let requireSecureOrigin = Preferences.get(PREF_INSTALL_REQUIRESECUREORIGIN, true);
+ let safeSchemes = ["https", "chrome", "file"];
+ if (requireSecureOrigin && safeSchemes.indexOf(uri.scheme) == -1)
+ return false;
+
+ return true;
+ },
+
+ /**
+ * Called to get an AddonInstall to download and install an add-on from a URL.
+ *
+ * @param aUrl
+ * The URL to be installed
+ * @param aHash
+ * A hash for the install
+ * @param aName
+ * A name for the install
+ * @param aIcons
+ * Icon URLs for the install
+ * @param aVersion
+ * A version for the install
+ * @param aBrowser
+ * The browser performing the install
+ * @param aCallback
+ * A callback to pass the AddonInstall to
+ */
+ getInstallForURL: function XPI_getInstallForURL(aUrl, aHash, aName, aIcons,
+ aVersion, aBrowser, aCallback) {
+ AddonInstall.createDownload(function getInstallForURL_createDownload(aInstall) {
+ aCallback(aInstall.wrapper);
+ }, aUrl, aHash, aName, aIcons, aVersion, aBrowser);
+ },
+
+ /**
+ * Called to get an AddonInstall to install an add-on from a local file.
+ *
+ * @param aFile
+ * The file to be installed
+ * @param aCallback
+ * A callback to pass the AddonInstall to
+ */
+ getInstallForFile: function XPI_getInstallForFile(aFile, aCallback) {
+ AddonInstall.createInstall(function getInstallForFile_createInstall(aInstall) {
+ if (aInstall)
+ aCallback(aInstall.wrapper);
+ else
+ aCallback(null);
+ }, aFile);
+ },
+
+ /**
+ * Removes an AddonInstall from the list of active installs.
+ *
+ * @param install
+ * The AddonInstall to remove
+ */
+ removeActiveInstall: function XPI_removeActiveInstall(aInstall) {
+ let where = this.installs.indexOf(aInstall);
+ if (where == -1) {
+ logger.warn("removeActiveInstall: could not find active install for "
+ + aInstall.sourceURI.spec);
+ return;
+ }
+ this.installs.splice(where, 1);
+ },
+
+ /**
+ * Called to get an Addon with a particular ID.
+ *
+ * @param aId
+ * The ID of the add-on to retrieve
+ * @param aCallback
+ * A callback to pass the Addon to
+ */
+ getAddonByID: function XPI_getAddonByID(aId, aCallback) {
+ XPIDatabase.getVisibleAddonForID (aId, function getAddonByID_getVisibleAddonForID(aAddon) {
+ aCallback(createWrapper(aAddon));
+ });
+ },
+
+ /**
+ * Called to get Addons of a particular type.
+ *
+ * @param aTypes
+ * An array of types to fetch. Can be null to get all types.
+ * @param aCallback
+ * A callback to pass an array of Addons to
+ */
+ getAddonsByTypes: function XPI_getAddonsByTypes(aTypes, aCallback) {
+ XPIDatabase.getVisibleAddons(aTypes, function getAddonsByTypes_getVisibleAddons(aAddons) {
+ // Tycho: aCallback([createWrapper(a) for each (a in aAddons)]);
+ let result = [];
+ for each(let a in aAddons) {
+ result.push(createWrapper(a));
+ }
+ aCallback(result);
+ });
+ },
+
+ /**
+ * Obtain an Addon having the specified Sync GUID.
+ *
+ * @param aGUID
+ * String GUID of add-on to retrieve
+ * @param aCallback
+ * A callback to pass the Addon to. Receives null if not found.
+ */
+ getAddonBySyncGUID: function XPI_getAddonBySyncGUID(aGUID, aCallback) {
+ XPIDatabase.getAddonBySyncGUID(aGUID, function getAddonBySyncGUID_getAddonBySyncGUID(aAddon) {
+ aCallback(createWrapper(aAddon));
+ });
+ },
+
+ /**
+ * Called to get Addons that have pending operations.
+ *
+ * @param aTypes
+ * An array of types to fetch. Can be null to get all types
+ * @param aCallback
+ * A callback to pass an array of Addons to
+ */
+ getAddonsWithOperationsByTypes:
+ function XPI_getAddonsWithOperationsByTypes(aTypes, aCallback) {
+ XPIDatabase.getVisibleAddonsWithPendingOperations(aTypes,
+ function getAddonsWithOpsByTypes_getVisibleAddonsWithPendingOps(aAddons) {
+ // Tycho: let results = [createWrapper(a) for each (a in aAddons)];
+ let results = [];
+ for each(let a in aAddons) {
+ results.push(createWrapper(a));
+ }
+
+ XPIProvider.installs.forEach(function(aInstall) {
+ if (aInstall.state == AddonManager.STATE_INSTALLED &&
+ !(aInstall.addon.inDatabase))
+ results.push(createWrapper(aInstall.addon));
+ });
+ aCallback(results);
+ });
+ },
+
+ /**
+ * Called to get the current AddonInstalls, optionally limiting to a list of
+ * types.
+ *
+ * @param aTypes
+ * An array of types or null to get all types
+ * @param aCallback
+ * A callback to pass the array of AddonInstalls to
+ */
+ getInstallsByTypes: function XPI_getInstallsByTypes(aTypes, aCallback) {
+ let results = [];
+ this.installs.forEach(function(aInstall) {
+ if (!aTypes || aTypes.indexOf(aInstall.type) >= 0)
+ results.push(aInstall.wrapper);
+ });
+ aCallback(results);
+ },
+
+ /**
+ * Synchronously map a URI to the corresponding Addon ID.
+ *
+ * Mappable URIs are limited to in-application resources belonging to the
+ * add-on, such as Javascript compartments, XUL windows, XBL bindings, etc.
+ * but do not include URIs from meta data, such as the add-on homepage.
+ *
+ * @param aURI
+ * nsIURI to map or null
+ * @return string containing the Addon ID
+ * @see AddonManager.mapURIToAddonID
+ * @see amIAddonManager.mapURIToAddonID
+ */
+ mapURIToAddonID: function XPI_mapURIToAddonID(aURI) {
+ let resolved = this._resolveURIToFile(aURI);
+ if (!resolved || !(resolved instanceof Ci.nsIFileURL))
+ return null;
+
+ for (let [id, path] of this._addonFileMap) {
+ if (resolved.file.path.startsWith(path))
+ return id;
+ }
+
+ return null;
+ },
+
+ /**
+ * Called when a new add-on has been enabled when only one add-on of that type
+ * can be enabled.
+ *
+ * @param aId
+ * The ID of the newly enabled add-on
+ * @param aType
+ * The type of the newly enabled add-on
+ * @param aPendingRestart
+ * true if the newly enabled add-on will only become enabled after a
+ * restart
+ */
+ addonChanged: function XPI_addonChanged(aId, aType, aPendingRestart) {
+ // We only care about themes in this provider
+ if (aType != "theme")
+ return;
+
+ if (!aId) {
+ // Fallback to the default theme when no theme was enabled
+ this.enableDefaultTheme();
+ return;
+ }
+
+ // Look for the previously enabled theme and find the internalName of the
+ // currently selected theme
+ let previousTheme = null;
+ let newSkin = this.defaultSkin;
+ let addons = XPIDatabase.getAddonsByType("theme");
+ addons.forEach(function(aTheme) {
+ if (!aTheme.visible)
+ return;
+ if (aTheme.id == aId)
+ newSkin = aTheme.internalName;
+ else if (aTheme.userDisabled == false && !aTheme.pendingUninstall)
+ previousTheme = aTheme;
+ }, this);
+
+ if (aPendingRestart) {
+ Services.prefs.setBoolPref(PREF_DSS_SWITCHPENDING, true);
+ Services.prefs.setCharPref(PREF_DSS_SKIN_TO_SELECT, newSkin);
+ }
+ else if (newSkin == this.currentSkin) {
+ try {
+ Services.prefs.clearUserPref(PREF_DSS_SWITCHPENDING);
+ }
+ catch (e) { }
+ try {
+ Services.prefs.clearUserPref(PREF_DSS_SKIN_TO_SELECT);
+ }
+ catch (e) { }
+ }
+ else {
+ Services.prefs.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN, newSkin);
+ this.currentSkin = newSkin;
+ }
+ this.selectedSkin = newSkin;
+
+ // Flush the preferences to disk so they don't get out of sync with the
+ // database
+ Services.prefs.savePrefFile(null);
+
+ // Mark the previous theme as disabled. This won't cause recursion since
+ // only enabled calls notifyAddonChanged.
+ if (previousTheme)
+ this.updateAddonDisabledState(previousTheme, true);
+ },
+
+ /**
+ * Update the appDisabled property for all add-ons.
+ */
+ updateAddonAppDisabledStates: function XPI_updateAddonAppDisabledStates() {
+ let addons = XPIDatabase.getAddons();
+ addons.forEach(function(aAddon) {
+ this.updateAddonDisabledState(aAddon);
+ }, this);
+ },
+
+ /**
+ * Update the repositoryAddon property for all add-ons.
+ *
+ * @param aCallback
+ * Function to call when operation is complete.
+ */
+ updateAddonRepositoryData: function XPI_updateAddonRepositoryData(aCallback) {
+ let self = this;
+ XPIDatabase.getVisibleAddons(null, function UARD_getVisibleAddonsCallback(aAddons) {
+ let pending = aAddons.length;
+ logger.debug("updateAddonRepositoryData found " + pending + " visible add-ons");
+ if (pending == 0) {
+ aCallback();
+ return;
+ }
+
+ function notifyComplete() {
+ if (--pending == 0)
+ aCallback();
+ }
+
+ for (let addon of aAddons) {
+ AddonRepository.getCachedAddonByID(addon.id,
+ function UARD_getCachedAddonCallback(aRepoAddon) {
+ if (aRepoAddon) {
+ logger.debug("updateAddonRepositoryData got info for " + addon.id);
+ addon._repositoryAddon = aRepoAddon;
+ addon.compatibilityOverrides = aRepoAddon.compatibilityOverrides;
+ self.updateAddonDisabledState(addon);
+ }
+
+ notifyComplete();
+ });
+ };
+ });
+ },
+
+ /**
+ * When the previously selected theme is removed this method will be called
+ * to enable the default theme.
+ */
+ enableDefaultTheme: function XPI_enableDefaultTheme() {
+ logger.debug("Activating default theme");
+ let addon = XPIDatabase.getVisibleAddonForInternalName(this.defaultSkin);
+ if (addon) {
+ if (addon.userDisabled) {
+ this.updateAddonDisabledState(addon, false);
+ }
+ else if (!this.extensionsActive) {
+ // During startup we may end up trying to enable the default theme when
+ // the database thinks it is already enabled (see f.e. bug 638847). In
+ // this case just force the theme preferences to be correct
+ Services.prefs.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN,
+ addon.internalName);
+ this.currentSkin = this.selectedSkin = addon.internalName;
+ Preferences.reset(PREF_DSS_SKIN_TO_SELECT);
+ Preferences.reset(PREF_DSS_SWITCHPENDING);
+ }
+ else {
+ logger.warn("Attempting to activate an already active default theme");
+ }
+ }
+ else {
+ logger.warn("Unable to activate the default theme");
+ }
+ },
+
+ onDebugConnectionChange: function(aEvent, aWhat, aConnection) {
+ if (aWhat != "opened")
+ return;
+
+ for (let id of Object.keys(this.bootstrapScopes)) {
+ aConnection.setAddonOptions(id, { global: this.bootstrapScopes[id] });
+ }
+ },
+
+ /**
+ * Notified when a preference we're interested in has changed.
+ *
+ * @see nsIObserver
+ */
+ observe: function XPI_observe(aSubject, aTopic, aData) {
+ if (aTopic == NOTIFICATION_FLUSH_PERMISSIONS) {
+ if (!aData || aData == XPI_PERMISSION) {
+ this.importPermissions();
+ }
+ return;
+ }
+ else if (aTopic == NOTIFICATION_TOOLBOXPROCESS_LOADED) {
+ Services.obs.removeObserver(this, NOTIFICATION_TOOLBOXPROCESS_LOADED, false);
+ this._toolboxProcessLoaded = true;
+ BrowserToolboxProcess.on("connectionchange",
+ this.onDebugConnectionChange.bind(this));
+ }
+
+ if (aTopic == "nsPref:changed") {
+ switch (aData) {
+ case PREF_EM_MIN_COMPAT_APP_VERSION:
+ case PREF_EM_MIN_COMPAT_PLATFORM_VERSION:
+ this.minCompatibleAppVersion = Preferences.get(PREF_EM_MIN_COMPAT_APP_VERSION,
+ null);
+ this.minCompatiblePlatformVersion = Preferences.get(PREF_EM_MIN_COMPAT_PLATFORM_VERSION,
+ null);
+ this.updateAddonAppDisabledStates();
+ break;
+ }
+ }
+ },
+
+ /**
+ * Tests whether enabling an add-on will require a restart.
+ *
+ * @param aAddon
+ * The add-on to test
+ * @return true if the operation requires a restart
+ */
+ enableRequiresRestart: function XPI_enableRequiresRestart(aAddon) {
+ // If the platform couldn't have activated extensions then we can make
+ // changes without any restart.
+ if (!this.extensionsActive)
+ return false;
+
+ // If the application is in safe mode then any change can be made without
+ // restarting
+ if (Services.appinfo.inSafeMode)
+ return false;
+
+ // Anything that is active is already enabled
+ if (aAddon.active)
+ return false;
+
+ if (aAddon.type == "theme") {
+ // If dynamic theme switching is enabled then switching themes does not
+ // require a restart
+ if (Preferences.get(PREF_EM_DSS_ENABLED))
+ return false;
+
+ // If the theme is already the theme in use then no restart is necessary.
+ // This covers the case where the default theme is in use but a
+ // lightweight theme is considered active.
+ return aAddon.internalName != this.currentSkin;
+ }
+
+ return !aAddon.bootstrap;
+ },
+
+ /**
+ * Tests whether disabling an add-on will require a restart.
+ *
+ * @param aAddon
+ * The add-on to test
+ * @return true if the operation requires a restart
+ */
+ disableRequiresRestart: function XPI_disableRequiresRestart(aAddon) {
+ // If the platform couldn't have activated up extensions then we can make
+ // changes without any restart.
+ if (!this.extensionsActive)
+ return false;
+
+ // If the application is in safe mode then any change can be made without
+ // restarting
+ if (Services.appinfo.inSafeMode)
+ return false;
+
+ // Anything that isn't active is already disabled
+ if (!aAddon.active)
+ return false;
+
+ if (aAddon.type == "theme") {
+ // If dynamic theme switching is enabled then switching themes does not
+ // require a restart
+ if (Preferences.get(PREF_EM_DSS_ENABLED))
+ return false;
+
+ // Non-default themes always require a restart to disable since it will
+ // be switching from one theme to another or to the default theme and a
+ // lightweight theme.
+ if (aAddon.internalName != this.defaultSkin)
+ return true;
+
+ // The default theme requires a restart to disable if we are in the
+ // process of switching to a different theme. Note that this makes the
+ // disabled flag of operationsRequiringRestart incorrect for the default
+ // theme (it will be false most of the time). Bug 520124 would be required
+ // to fix it. For the UI this isn't a problem since we never try to
+ // disable or uninstall the default theme.
+ return this.selectedSkin != this.currentSkin;
+ }
+
+ return !aAddon.bootstrap;
+ },
+
+ /**
+ * Tests whether installing an add-on will require a restart.
+ *
+ * @param aAddon
+ * The add-on to test
+ * @return true if the operation requires a restart
+ */
+ installRequiresRestart: function XPI_installRequiresRestart(aAddon) {
+ // If the platform couldn't have activated up extensions then we can make
+ // changes without any restart.
+ if (!this.extensionsActive)
+ return false;
+
+ // If the application is in safe mode then any change can be made without
+ // restarting
+ if (Services.appinfo.inSafeMode)
+ return false;
+
+ // Add-ons that are already installed don't require a restart to install.
+ // This wouldn't normally be called for an already installed add-on (except
+ // for forming the operationsRequiringRestart flags) so is really here as
+ // a safety measure.
+ if (aAddon.inDatabase)
+ return false;
+
+ // If we have an AddonInstall for this add-on then we can see if there is
+ // an existing installed add-on with the same ID
+ if ("_install" in aAddon && aAddon._install) {
+ // If there is an existing installed add-on and uninstalling it would
+ // require a restart then installing the update will also require a
+ // restart
+ let existingAddon = aAddon._install.existingAddon;
+ if (existingAddon && this.uninstallRequiresRestart(existingAddon))
+ return true;
+ }
+
+ // If the add-on is not going to be active after installation then it
+ // doesn't require a restart to install.
+ if (aAddon.disabled)
+ return false;
+
+ // Themes will require a restart (even if dynamic switching is enabled due
+ // to some caching issues) and non-bootstrapped add-ons will require a
+ // restart
+ return aAddon.type == "theme" || !aAddon.bootstrap;
+ },
+
+ /**
+ * Tests whether uninstalling an add-on will require a restart.
+ *
+ * @param aAddon
+ * The add-on to test
+ * @return true if the operation requires a restart
+ */
+ uninstallRequiresRestart: function XPI_uninstallRequiresRestart(aAddon) {
+ // If the platform couldn't have activated up extensions then we can make
+ // changes without any restart.
+ if (!this.extensionsActive)
+ return false;
+
+ // If the application is in safe mode then any change can be made without
+ // restarting
+ if (Services.appinfo.inSafeMode)
+ return false;
+
+ // If the add-on can be disabled without a restart then it can also be
+ // uninstalled without a restart
+ return this.disableRequiresRestart(aAddon);
+ },
+
+ /**
+ * Loads a bootstrapped add-on's bootstrap.js into a sandbox and the reason
+ * values as constants in the scope. This will also add information about the
+ * add-on to the bootstrappedAddons dictionary and notify the crash reporter
+ * that new add-ons have been loaded.
+ *
+ * @param aId
+ * The add-on's ID
+ * @param aFile
+ * The nsIFile for the add-on
+ * @param aVersion
+ * The add-on's version
+ * @param aType
+ * The type for the add-on
+ * @param aMultiprocessCompatible
+ * Boolean indicating whether the add-on is compatible with electrolysis.
+ * @return a JavaScript scope
+ */
+ loadBootstrapScope: function XPI_loadBootstrapScope(aId, aFile, aVersion, aType,
+ aMultiprocessCompatible) {
+ // Mark the add-on as active for the crash reporter before loading
+ this.bootstrappedAddons[aId] = {
+ version: aVersion,
+ type: aType,
+ descriptor: aFile.persistentDescriptor,
+ multiprocessCompatible: aMultiprocessCompatible
+ };
+ this.persistBootstrappedAddons();
+ this.addAddonsToCrashReporter();
+
+ // Locales only contain chrome and can't have bootstrap scripts
+ if (aType == "locale") {
+ this.bootstrapScopes[aId] = null;
+ return;
+ }
+
+ logger.debug("Loading bootstrap scope from " + aFile.path);
+
+ let principal = Cc["@mozilla.org/systemprincipal;1"].
+ createInstance(Ci.nsIPrincipal);
+ if (!aMultiprocessCompatible && Preferences.get(PREF_INTERPOSITION_ENABLED, false)) {
+ let interposition = Cc["@mozilla.org/addons/multiprocess-shims;1"].
+ getService(Ci.nsIAddonInterposition);
+ Cu.setAddonInterposition(aId, interposition);
+ }
+
+ if (!aFile.exists()) {
+ this.bootstrapScopes[aId] =
+ new Cu.Sandbox(principal, { sandboxName: aFile.path,
+ wantGlobalProperties: ["indexedDB"],
+ addonId: aId,
+ metadata: { addonID: aId } });
+ logger.error("Attempted to load bootstrap scope from missing directory " + aFile.path);
+ return;
+ }
+
+ let uri = getURIForResourceInFile(aFile, "bootstrap.js").spec;
+ if (aType == "dictionary")
+ uri = "resource://gre/modules/addons/SpellCheckDictionaryBootstrap.js"
+
+ this.bootstrapScopes[aId] =
+ new Cu.Sandbox(principal, { sandboxName: uri,
+ wantGlobalProperties: ["indexedDB"],
+ addonId: aId,
+ metadata: { addonID: aId, URI: uri } });
+
+ let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
+ createInstance(Ci.mozIJSSubScriptLoader);
+
+ try {
+ // Copy the reason values from the global object into the bootstrap scope.
+ for (let name in BOOTSTRAP_REASONS)
+ this.bootstrapScopes[aId][name] = BOOTSTRAP_REASONS[name];
+
+ // Add other stuff that extensions want.
+ const features = [ "Worker", "ChromeWorker" ];
+
+ for (let feature of features)
+ this.bootstrapScopes[aId][feature] = gGlobalScope[feature];
+
+ // Define a console for the add-on
+ this.bootstrapScopes[aId]["console"] = new ConsoleAPI({ consoleID: "addon/" + aId });
+
+ // As we don't want our caller to control the JS version used for the
+ // bootstrap file, we run loadSubScript within the context of the
+ // sandbox with the latest JS version set explicitly.
+ this.bootstrapScopes[aId].__SCRIPT_URI_SPEC__ = uri;
+ Components.utils.evalInSandbox(
+ "Components.classes['@mozilla.org/moz/jssubscript-loader;1'] \
+ .createInstance(Components.interfaces.mozIJSSubScriptLoader) \
+ .loadSubScript(__SCRIPT_URI_SPEC__);", this.bootstrapScopes[aId], "ECMAv5");
+ }
+ catch (e) {
+ logger.warn("Error loading bootstrap.js for " + aId, e);
+ }
+
+ // Only access BrowserToolboxProcess if ToolboxProcess.jsm has been
+ // initialized as otherwise, when it will be initialized, all addons'
+ // globals will be added anyways
+ if (this._toolboxProcessLoaded) {
+ BrowserToolboxProcess.setAddonOptions(aId, { global: this.bootstrapScopes[aId] });
+ }
+ },
+
+ /**
+ * Unloads a bootstrap scope by dropping all references to it and then
+ * updating the list of active add-ons with the crash reporter.
+ *
+ * @param aId
+ * The add-on's ID
+ */
+ unloadBootstrapScope: function XPI_unloadBootstrapScope(aId) {
+ // In case the add-on was not multiprocess-compatible, deregister
+ // any interpositions for it.
+ Cu.setAddonInterposition(aId, null);
+
+ delete this.bootstrapScopes[aId];
+ delete this.bootstrappedAddons[aId];
+ this.persistBootstrappedAddons();
+ this.addAddonsToCrashReporter();
+
+ // Only access BrowserToolboxProcess if ToolboxProcess.jsm has been
+ // initialized as otherwise, there won't be any addon globals added to it
+ if (this._toolboxProcessLoaded) {
+ BrowserToolboxProcess.setAddonOptions(aId, { global: null });
+ }
+ },
+
+ /**
+ * Calls a bootstrap method for an add-on.
+ *
+ * @param aAddon
+ * An object representing the add-on, with `id`, `type` and `version`
+ * @param aFile
+ * The nsIFile for the add-on
+ * @param aMethod
+ * The name of the bootstrap method to call
+ * @param aReason
+ * The reason flag to pass to the bootstrap's startup method
+ * @param aExtraParams
+ * An object of additional key/value pairs to pass to the method in
+ * the params argument
+ */
+ callBootstrapMethod: function XPI_callBootstrapMethod(aAddon, aFile, aMethod, aReason, aExtraParams) {
+ // Never call any bootstrap methods in safe mode
+ if (Services.appinfo.inSafeMode)
+ return;
+
+ if (!aAddon.id || !aAddon.version || !aAddon.type) {
+ logger.error(new Error("aAddon must include an id, version, and type"));
+ return;
+ }
+
+ let timeStart = new Date();
+ if (aMethod == "startup") {
+ logger.debug("Registering manifest for " + aFile.path);
+ Components.manager.addBootstrappedManifestLocation(aFile);
+ }
+
+ try {
+ // Load the scope if it hasn't already been loaded
+ if (!(aAddon.id in this.bootstrapScopes))
+ this.loadBootstrapScope(aAddon.id, aFile, aAddon.version, aAddon.type,
+ aAddon.multiprocessCompatible || false);
+
+ // Nothing to call for locales
+ if (aAddon.type == "locale")
+ return;
+
+ if (!(aMethod in this.bootstrapScopes[aAddon.id])) {
+ logger.warn("Add-on " + aAddon.id + " is missing bootstrap method " + aMethod);
+ return;
+ }
+
+ let params = {
+ id: aAddon.id,
+ version: aAddon.version,
+ installPath: aFile.clone(),
+ resourceURI: getURIForResourceInFile(aFile, "")
+ };
+
+ if (aExtraParams) {
+ for (let key in aExtraParams) {
+ params[key] = aExtraParams[key];
+ }
+ }
+
+ logger.debug("Calling bootstrap method " + aMethod + " on " + aAddon.id + " version " +
+ aAddon.version);
+ try {
+ this.bootstrapScopes[aAddon.id][aMethod](params, aReason);
+ }
+ catch (e) {
+ logger.warn("Exception running bootstrap method " + aMethod + " on " + aAddon.id, e);
+ }
+ }
+ finally {
+ if (aMethod == "shutdown" && aReason != BOOTSTRAP_REASONS.APP_SHUTDOWN) {
+ logger.debug("Removing manifest for " + aFile.path);
+ Components.manager.removeBootstrappedManifestLocation(aFile);
+
+ let manifest = getURIForResourceInFile(aFile, "chrome.manifest");
+ for (let line of ChromeManifestParser.parseSync(manifest)) {
+ if (line.type == "resource") {
+ ResProtocolHandler.setSubstitution(line.args[0], null);
+ }
+ }
+ }
+ }
+ },
+
+ /**
+ * Updates the disabled state for an add-on. Its appDisabled property will be
+ * calculated and if the add-on is changed the database will be saved and
+ * appropriate notifications will be sent out to the registered AddonListeners.
+ *
+ * @param aAddon
+ * The DBAddonInternal to update
+ * @param aUserDisabled
+ * Value for the userDisabled property. If undefined the value will
+ * not change
+ * @param aSoftDisabled
+ * Value for the softDisabled property. If undefined the value will
+ * not change. If true this will force userDisabled to be true
+ * @throws if addon is not a DBAddonInternal
+ */
+ updateAddonDisabledState: function XPI_updateAddonDisabledState(aAddon,
+ aUserDisabled,
+ aSoftDisabled) {
+ if (!(aAddon.inDatabase))
+ throw new Error("Can only update addon states for installed addons.");
+ if (aUserDisabled !== undefined && aSoftDisabled !== undefined) {
+ throw new Error("Cannot change userDisabled and softDisabled at the " +
+ "same time");
+ }
+
+ if (aUserDisabled === undefined) {
+ aUserDisabled = aAddon.userDisabled;
+ }
+ else if (!aUserDisabled) {
+ // If enabling the add-on then remove softDisabled
+ aSoftDisabled = false;
+ }
+
+ // If not changing softDisabled or the add-on is already userDisabled then
+ // use the existing value for softDisabled
+ if (aSoftDisabled === undefined || aUserDisabled)
+ aSoftDisabled = aAddon.softDisabled;
+
+ let appDisabled = !isUsableAddon(aAddon);
+ // No change means nothing to do here
+ if (aAddon.userDisabled == aUserDisabled &&
+ aAddon.appDisabled == appDisabled &&
+ aAddon.softDisabled == aSoftDisabled)
+ return;
+
+ let wasDisabled = aAddon.disabled;
+ let isDisabled = aUserDisabled || aSoftDisabled || appDisabled;
+
+ // If appDisabled changes but addon.disabled doesn't,
+ // no onDisabling/onEnabling is sent - so send a onPropertyChanged.
+ let appDisabledChanged = aAddon.appDisabled != appDisabled;
+
+ // Update the properties in the database.
+ // We never persist this for experiments because the disabled flags
+ // are controlled by the Experiments Manager.
+ if (aAddon.type != "experiment") {
+ XPIDatabase.setAddonProperties(aAddon, {
+ userDisabled: aUserDisabled,
+ appDisabled: appDisabled,
+ softDisabled: aSoftDisabled
+ });
+ }
+
+ if (appDisabledChanged) {
+ AddonManagerPrivate.callAddonListeners("onPropertyChanged",
+ aAddon,
+ ["appDisabled"]);
+ }
+
+ // If the add-on is not visible or the add-on is not changing state then
+ // there is no need to do anything else
+ if (!aAddon.visible || (wasDisabled == isDisabled))
+ return;
+
+ // Flag that active states in the database need to be updated on shutdown
+ Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
+
+ let wrapper = createWrapper(aAddon);
+ // Have we just gone back to the current state?
+ if (isDisabled != aAddon.active) {
+ AddonManagerPrivate.callAddonListeners("onOperationCancelled", wrapper);
+ }
+ else {
+ if (isDisabled) {
+ var needsRestart = this.disableRequiresRestart(aAddon);
+ AddonManagerPrivate.callAddonListeners("onDisabling", wrapper,
+ needsRestart);
+ }
+ else {
+ needsRestart = this.enableRequiresRestart(aAddon);
+ AddonManagerPrivate.callAddonListeners("onEnabling", wrapper,
+ needsRestart);
+ }
+
+ if (!needsRestart) {
+ XPIDatabase.updateAddonActive(aAddon, !isDisabled);
+ if (isDisabled) {
+ if (aAddon.bootstrap) {
+ let file = aAddon._installLocation.getLocationForID(aAddon.id);
+ this.callBootstrapMethod(aAddon, file, "shutdown",
+ BOOTSTRAP_REASONS.ADDON_DISABLE);
+ this.unloadBootstrapScope(aAddon.id);
+ }
+ AddonManagerPrivate.callAddonListeners("onDisabled", wrapper);
+ }
+ else {
+ if (aAddon.bootstrap) {
+ let file = aAddon._installLocation.getLocationForID(aAddon.id);
+ this.callBootstrapMethod(aAddon, file, "startup",
+ BOOTSTRAP_REASONS.ADDON_ENABLE);
+ }
+ AddonManagerPrivate.callAddonListeners("onEnabled", wrapper);
+ }
+ }
+ }
+
+ // Sync with XPIStates.
+ let xpiState = XPIStates.getAddon(aAddon.location, aAddon.id);
+ if (xpiState) {
+ xpiState.syncWithDB(aAddon);
+ XPIStates.save();
+ } else {
+ // There should always be an xpiState
+ logger.warn("No XPIState for ${id} in ${location}", aAddon);
+ }
+
+ // Notify any other providers that a new theme has been enabled
+ if (aAddon.type == "theme" && !isDisabled)
+ AddonManagerPrivate.notifyAddonChanged(aAddon.id, aAddon.type, needsRestart);
+ },
+
+ /**
+ * Uninstalls an add-on, immediately if possible or marks it as pending
+ * uninstall if not.
+ *
+ * @param aAddon
+ * The DBAddonInternal to uninstall
+ * @param aForcePending
+ * Force this addon into the pending uninstall state, even if
+ * it isn't marked as requiring a restart (used e.g. while the
+ * add-on manager is open and offering an "undo" button)
+ * @throws if the addon cannot be uninstalled because it is in an install
+ * location that does not allow it
+ */
+ uninstallAddon: function XPI_uninstallAddon(aAddon, aForcePending) {
+ if (!(aAddon.inDatabase))
+ throw new Error("Cannot uninstall addon " + aAddon.id + " because it is not installed");
+
+ if (aAddon._installLocation.locked)
+ throw new Error("Cannot uninstall addon " + aAddon.id
+ + " from locked install location " + aAddon._installLocation.name);
+
+ // Inactive add-ons don't require a restart to uninstall
+ let requiresRestart = this.uninstallRequiresRestart(aAddon);
+
+ // if makePending is true, we don't actually apply the uninstall,
+ // we just mark the addon as having a pending uninstall
+ let makePending = aForcePending || requiresRestart;
+
+ if (makePending && aAddon.pendingUninstall)
+ throw new Error("Add-on is already marked to be uninstalled");
+
+ if ("_hasResourceCache" in aAddon)
+ aAddon._hasResourceCache = new Map();
+
+ if (aAddon._updateCheck) {
+ logger.debug("Cancel in-progress update check for " + aAddon.id);
+ aAddon._updateCheck.cancel();
+ }
+
+ let wasPending = aAddon.pendingUninstall;
+
+ if (makePending) {
+ // We create an empty directory in the staging directory to indicate
+ // that an uninstall is necessary on next startup.
+ let stage = aAddon._installLocation.getStagingDir();
+ stage.append(aAddon.id);
+ if (!stage.exists())
+ stage.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+ XPIDatabase.setAddonProperties(aAddon, {
+ pendingUninstall: true
+ });
+ Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
+ let xpiState = XPIStates.getAddon(aAddon.location, aAddon.id);
+ if (xpiState) {
+ xpiState.enabled = false;
+ XPIStates.save();
+ } else {
+ logger.warn("Can't find XPI state while uninstalling ${id} from ${location}", aAddon);
+ }
+ }
+
+ // If the add-on is not visible then there is no need to notify listeners.
+ if (!aAddon.visible)
+ return;
+
+ let wrapper = createWrapper(aAddon);
+
+ // If the add-on wasn't already pending uninstall then notify listeners.
+ if (!wasPending) {
+ // Passing makePending as the requiresRestart parameter is a little
+ // strange as in some cases this operation can complete without a restart
+ // so really this is now saying that the uninstall isn't going to happen
+ // immediately but will happen later.
+ AddonManagerPrivate.callAddonListeners("onUninstalling", wrapper,
+ makePending);
+ }
+
+ // Reveal the highest priority add-on with the same ID
+ function revealAddon(aAddon) {
+ XPIDatabase.makeAddonVisible(aAddon);
+
+ let wrappedAddon = createWrapper(aAddon);
+ AddonManagerPrivate.callAddonListeners("onInstalling", wrappedAddon, false);
+
+ if (!aAddon.disabled && !XPIProvider.enableRequiresRestart(aAddon)) {
+ XPIDatabase.updateAddonActive(aAddon, true);
+ }
+
+ if (aAddon.bootstrap) {
+ let file = aAddon._installLocation.getLocationForID(aAddon.id);
+ XPIProvider.callBootstrapMethod(aAddon, file,
+ "install", BOOTSTRAP_REASONS.ADDON_INSTALL);
+
+ if (aAddon.active) {
+ XPIProvider.callBootstrapMethod(aAddon, file,
+ "startup", BOOTSTRAP_REASONS.ADDON_INSTALL);
+ }
+ else {
+ XPIProvider.unloadBootstrapScope(aAddon.id);
+ }
+ }
+
+ // We always send onInstalled even if a restart is required to enable
+ // the revealed add-on
+ AddonManagerPrivate.callAddonListeners("onInstalled", wrappedAddon);
+ }
+
+ function findAddonAndReveal(aId) {
+ let [locationName, ] = XPIStates.findAddon(aId);
+ if (locationName) {
+ XPIDatabase.getAddonInLocation(aId, locationName, revealAddon);
+ }
+ }
+
+ if (!makePending) {
+ if (aAddon.bootstrap) {
+ let file = aAddon._installLocation.getLocationForID(aAddon.id);
+ if (aAddon.active) {
+ this.callBootstrapMethod(aAddon, file, "shutdown",
+ BOOTSTRAP_REASONS.ADDON_UNINSTALL);
+ }
+
+ this.callBootstrapMethod(aAddon, file, "uninstall",
+ BOOTSTRAP_REASONS.ADDON_UNINSTALL);
+ this.unloadBootstrapScope(aAddon.id);
+ flushStartupCache();
+ }
+ aAddon._installLocation.uninstallAddon(aAddon.id);
+ XPIDatabase.removeAddonMetadata(aAddon);
+ XPIStates.removeAddon(aAddon.location, aAddon.id);
+ AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper);
+
+ findAddonAndReveal(aAddon.id);
+ }
+ else if (aAddon.bootstrap && aAddon.active && !this.disableRequiresRestart(aAddon)) {
+ this.callBootstrapMethod(aAddon, aAddon._sourceBundle, "shutdown",
+ BOOTSTRAP_REASONS.ADDON_UNINSTALL);
+ this.unloadBootstrapScope(aAddon.id);
+ XPIDatabase.updateAddonActive(aAddon, false);
+ }
+
+ // Notify any other providers that a new theme has been enabled
+ if (aAddon.type == "theme" && aAddon.active)
+ AddonManagerPrivate.notifyAddonChanged(null, aAddon.type, requiresRestart);
+ },
+
+ /**
+ * Cancels the pending uninstall of an add-on.
+ *
+ * @param aAddon
+ * The DBAddonInternal to cancel uninstall for
+ */
+ cancelUninstallAddon: function XPI_cancelUninstallAddon(aAddon) {
+ if (!(aAddon.inDatabase))
+ throw new Error("Can only cancel uninstall for installed addons.");
+
+ if (!aAddon.pendingUninstall)
+ throw new Error("Add-on is not marked to be uninstalled");
+
+ aAddon._installLocation.cleanStagingDir([aAddon.id]);
+
+ XPIDatabase.setAddonProperties(aAddon, {
+ pendingUninstall: false
+ });
+
+ if (!aAddon.visible)
+ return;
+
+ Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
+
+ // TODO hide hidden add-ons (bug 557710)
+ let wrapper = createWrapper(aAddon);
+ AddonManagerPrivate.callAddonListeners("onOperationCancelled", wrapper);
+
+ if (aAddon.bootstrap && !aAddon.disabled && !this.enableRequiresRestart(aAddon)) {
+ this.callBootstrapMethod(aAddon, aAddon._sourceBundle, "startup",
+ BOOTSTRAP_REASONS.ADDON_INSTALL);
+ XPIDatabase.updateAddonActive(aAddon, true);
+ }
+
+ // Notify any other providers that this theme is now enabled again.
+ if (aAddon.type == "theme" && aAddon.active)
+ AddonManagerPrivate.notifyAddonChanged(aAddon.id, aAddon.type, false);
+ }
+};
+
+function getHashStringForCrypto(aCrypto) {
+ // return the two-digit hexadecimal code for a byte
+ let toHexString = charCode => ("0" + charCode.toString(16)).slice(-2);
+
+ // convert the binary hash data to a hex string.
+ let binary = aCrypto.finish(false);
+ let hash = Array.from(binary, c => toHexString(c.charCodeAt(0)))
+ return hash.join("").toLowerCase();
+}
+
+/**
+ * Instantiates an AddonInstall.
+ *
+ * @param aInstallLocation
+ * The install location the add-on will be installed into
+ * @param aUrl
+ * The nsIURL to get the add-on from. If this is an nsIFileURL then
+ * the add-on will not need to be downloaded
+ * @param aHash
+ * An optional hash for the add-on
+ * @param aReleaseNotesURI
+ * An optional nsIURI of release notes for the add-on
+ * @param aExistingAddon
+ * The add-on this install will update if known
+ * @param aBrowser
+ * The browser performing the install
+ * @throws if the url is the url of a local file and the hash does not match
+ * or the add-on does not contain an valid install manifest
+ */
+function AddonInstall(aInstallLocation, aUrl, aHash, aReleaseNotesURI,
+ aExistingAddon, aBrowser) {
+ this.wrapper = new AddonInstallWrapper(this);
+ this.installLocation = aInstallLocation;
+ this.sourceURI = aUrl;
+ this.releaseNotesURI = aReleaseNotesURI;
+ if (aHash) {
+ let hashSplit = aHash.toLowerCase().split(":");
+ this.originalHash = {
+ algorithm: hashSplit[0],
+ data: hashSplit[1]
+ };
+ }
+ this.hash = this.originalHash;
+ this.browser = aBrowser;
+ this.listeners = [];
+ this.icons = {};
+ this.existingAddon = aExistingAddon;
+ this.error = 0;
+ this.window = aBrowser ? aBrowser.contentWindow : null;
+
+ // Giving each instance of AddonInstall a reference to the logger.
+ this.logger = logger;
+}
+
+AddonInstall.prototype = {
+ installLocation: null,
+ wrapper: null,
+ stream: null,
+ crypto: null,
+ originalHash: null,
+ hash: null,
+ browser: null,
+ badCertHandler: null,
+ listeners: null,
+ restartDownload: false,
+
+ name: null,
+ type: null,
+ version: null,
+ icons: null,
+ releaseNotesURI: null,
+ sourceURI: null,
+ file: null,
+ ownsTempFile: false,
+ certificate: null,
+ certName: null,
+
+ linkedInstalls: null,
+ existingAddon: null,
+ addon: null,
+
+ state: null,
+ error: null,
+ progress: null,
+ maxProgress: null,
+
+ /**
+ * Initialises this install to be a staged install waiting to be applied
+ *
+ * @param aManifest
+ * The cached manifest for the staged install
+ */
+ initStagedInstall: function AI_initStagedInstall(aManifest) {
+ this.name = aManifest.name;
+ this.type = aManifest.type;
+ this.version = aManifest.version;
+ this.icons = aManifest.icons;
+ this.releaseNotesURI = aManifest.releaseNotesURI ?
+ NetUtil.newURI(aManifest.releaseNotesURI) :
+ null
+ this.sourceURI = aManifest.sourceURI ?
+ NetUtil.newURI(aManifest.sourceURI) :
+ null;
+ this.file = null;
+ this.addon = aManifest;
+
+ this.state = AddonManager.STATE_INSTALLED;
+
+ XPIProvider.installs.push(this);
+ },
+
+ /**
+ * Initialises this install to be an install from a local file.
+ *
+ * @param aCallback
+ * The callback to pass the initialised AddonInstall to
+ */
+ initLocalInstall: function AI_initLocalInstall(aCallback) {
+ aCallback = makeSafe(aCallback);
+ this.file = this.sourceURI.QueryInterface(Ci.nsIFileURL).file;
+
+ if (!this.file.exists()) {
+ logger.warn("XPI file " + this.file.path + " does not exist");
+ this.state = AddonManager.STATE_DOWNLOAD_FAILED;
+ this.error = AddonManager.ERROR_NETWORK_FAILURE;
+ aCallback(this);
+ return;
+ }
+
+ this.state = AddonManager.STATE_DOWNLOADED;
+ this.progress = this.file.fileSize;
+ this.maxProgress = this.file.fileSize;
+
+ if (this.hash) {
+ let crypto = Cc["@mozilla.org/security/hash;1"].
+ createInstance(Ci.nsICryptoHash);
+ try {
+ crypto.initWithString(this.hash.algorithm);
+ }
+ catch (e) {
+ logger.warn("Unknown hash algorithm '" + this.hash.algorithm + "' for addon " + this.sourceURI.spec, e);
+ this.state = AddonManager.STATE_DOWNLOAD_FAILED;
+ this.error = AddonManager.ERROR_INCORRECT_HASH;
+ aCallback(this);
+ return;
+ }
+
+ let fis = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+ fis.init(this.file, -1, -1, false);
+ crypto.updateFromStream(fis, this.file.fileSize);
+ let calculatedHash = getHashStringForCrypto(crypto);
+ if (calculatedHash != this.hash.data) {
+ logger.warn("File hash (" + calculatedHash + ") did not match provided hash (" +
+ this.hash.data + ")");
+ this.state = AddonManager.STATE_DOWNLOAD_FAILED;
+ this.error = AddonManager.ERROR_INCORRECT_HASH;
+ aCallback(this);
+ return;
+ }
+ }
+
+ try {
+ let self = this;
+ this.loadManifest(function initLocalInstall_loadManifest() {
+ XPIDatabase.getVisibleAddonForID(self.addon.id, function initLocalInstall_getVisibleAddon(aAddon) {
+ self.existingAddon = aAddon;
+ if (aAddon)
+ applyBlocklistChanges(aAddon, self.addon);
+ self.addon.updateDate = Date.now();
+ self.addon.installDate = aAddon ? aAddon.installDate : self.addon.updateDate;
+
+ if (!self.addon.isCompatible) {
+ // TODO Should we send some event here?
+ self.state = AddonManager.STATE_CHECKING;
+ new UpdateChecker(self.addon, {
+ onUpdateFinished: function updateChecker_onUpdateFinished(aAddon) {
+ self.state = AddonManager.STATE_DOWNLOADED;
+ XPIProvider.installs.push(self);
+ AddonManagerPrivate.callInstallListeners("onNewInstall",
+ self.listeners,
+ self.wrapper);
+
+ aCallback(self);
+ }
+ }, AddonManager.UPDATE_WHEN_ADDON_INSTALLED);
+ }
+ else {
+ XPIProvider.installs.push(self);
+ AddonManagerPrivate.callInstallListeners("onNewInstall",
+ self.listeners,
+ self.wrapper);
+
+ aCallback(self);
+ }
+ });
+ });
+ }
+ catch (e) {
+ this.state = AddonManager.STATE_DOWNLOAD_FAILED;
+ if (e.webext) {
+ logger.warn("WebExtension XPI", e);
+ this.error = AddonManager.ERROR_WEBEXT_FILE;
+ } else {
+ logger.warn("Invalid XPI", e);
+ this.error = AddonManager.ERROR_CORRUPT_FILE;
+ }
+ aCallback(this);
+ return;
+ }
+ },
+
+ /**
+ * Initialises this install to be a download from a remote url.
+ *
+ * @param aCallback
+ * The callback to pass the initialised AddonInstall to
+ * @param aName
+ * An optional name for the add-on
+ * @param aType
+ * An optional type for the add-on
+ * @param aIcons
+ * Optional icons for the add-on
+ * @param aVersion
+ * An optional version for the add-on
+ */
+ initAvailableDownload: function AI_initAvailableDownload(aName, aType, aIcons, aVersion, aCallback) {
+ this.state = AddonManager.STATE_AVAILABLE;
+ this.name = aName;
+ this.type = aType;
+ this.version = aVersion;
+ this.icons = aIcons;
+ this.progress = 0;
+ this.maxProgress = -1;
+
+ XPIProvider.installs.push(this);
+ AddonManagerPrivate.callInstallListeners("onNewInstall", this.listeners,
+ this.wrapper);
+
+ makeSafe(aCallback)(this);
+ },
+
+ /**
+ * Starts installation of this add-on from whatever state it is currently at
+ * if possible.
+ *
+ * @throws if installation cannot proceed from the current state
+ */
+ install: function AI_install() {
+ switch (this.state) {
+ case AddonManager.STATE_AVAILABLE:
+ this.startDownload();
+ break;
+ case AddonManager.STATE_DOWNLOADED:
+ this.startInstall();
+ break;
+ case AddonManager.STATE_DOWNLOAD_FAILED:
+ case AddonManager.STATE_INSTALL_FAILED:
+ case AddonManager.STATE_CANCELLED:
+ this.removeTemporaryFile();
+ this.state = AddonManager.STATE_AVAILABLE;
+ this.error = 0;
+ this.progress = 0;
+ this.maxProgress = -1;
+ this.hash = this.originalHash;
+ XPIProvider.installs.push(this);
+ this.startDownload();
+ break;
+ case AddonManager.STATE_DOWNLOADING:
+ case AddonManager.STATE_CHECKING:
+ case AddonManager.STATE_INSTALLING:
+ // Installation is already running
+ return;
+ default:
+ throw new Error("Cannot start installing from this state");
+ }
+ },
+
+ /**
+ * Cancels installation of this add-on.
+ *
+ * @throws if installation cannot be cancelled from the current state
+ */
+ cancel: function AI_cancel() {
+ switch (this.state) {
+ case AddonManager.STATE_DOWNLOADING:
+ if (this.channel) {
+ logger.debug("Cancelling download of " + this.sourceURI.spec);
+ this.channel.cancel(Cr.NS_BINDING_ABORTED);
+ }
+ break;
+ case AddonManager.STATE_AVAILABLE:
+ case AddonManager.STATE_DOWNLOADED:
+ logger.debug("Cancelling download of " + this.sourceURI.spec);
+ this.state = AddonManager.STATE_CANCELLED;
+ XPIProvider.removeActiveInstall(this);
+ AddonManagerPrivate.callInstallListeners("onDownloadCancelled",
+ this.listeners, this.wrapper);
+ this.removeTemporaryFile();
+ break;
+ case AddonManager.STATE_INSTALLED:
+ logger.debug("Cancelling install of " + this.addon.id);
+ let xpi = this.installLocation.getStagingDir();
+ xpi.append(this.addon.id + ".xpi");
+ flushJarCache(xpi);
+ this.installLocation.cleanStagingDir([this.addon.id, this.addon.id + ".xpi",
+ this.addon.id + ".json"]);
+ this.state = AddonManager.STATE_CANCELLED;
+ XPIProvider.removeActiveInstall(this);
+
+ if (this.existingAddon) {
+ delete this.existingAddon.pendingUpgrade;
+ this.existingAddon.pendingUpgrade = null;
+ }
+
+ AddonManagerPrivate.callAddonListeners("onOperationCancelled", createWrapper(this.addon));
+
+ AddonManagerPrivate.callInstallListeners("onInstallCancelled",
+ this.listeners, this.wrapper);
+ break;
+ default:
+ throw new Error("Cannot cancel install of " + this.sourceURI.spec +
+ " from this state (" + this.state + ")");
+ }
+ },
+
+ /**
+ * Adds an InstallListener for this instance if the listener is not already
+ * registered.
+ *
+ * @param aListener
+ * The InstallListener to add
+ */
+ addListener: function AI_addListener(aListener) {
+ if (!this.listeners.some(function addListener_matchListener(i) { return i == aListener; }))
+ this.listeners.push(aListener);
+ },
+
+ /**
+ * Removes an InstallListener for this instance if it is registered.
+ *
+ * @param aListener
+ * The InstallListener to remove
+ */
+ removeListener: function AI_removeListener(aListener) {
+ this.listeners = this.listeners.filter(function removeListener_filterListener(i) {
+ return i != aListener;
+ });
+ },
+
+ /**
+ * Removes the temporary file owned by this AddonInstall if there is one.
+ */
+ removeTemporaryFile: function AI_removeTemporaryFile() {
+ // Only proceed if this AddonInstall owns its XPI file
+ if (!this.ownsTempFile) {
+ this.logger.debug("removeTemporaryFile: " + this.sourceURI.spec + " does not own temp file");
+ return;
+ }
+
+ try {
+ this.logger.debug("removeTemporaryFile: " + this.sourceURI.spec + " removing temp file " +
+ this.file.path);
+ this.file.remove(true);
+ this.ownsTempFile = false;
+ }
+ catch (e) {
+ this.logger.warn("Failed to remove temporary file " + this.file.path + " for addon " +
+ this.sourceURI.spec,
+ e);
+ }
+ },
+
+ /**
+ * Updates the sourceURI and releaseNotesURI values on the Addon being
+ * installed by this AddonInstall instance.
+ */
+ updateAddonURIs: function AI_updateAddonURIs() {
+ this.addon.sourceURI = this.sourceURI.spec;
+ if (this.releaseNotesURI)
+ this.addon.releaseNotesURI = this.releaseNotesURI.spec;
+ },
+
+ /**
+ * Loads add-on manifests from a multi-package XPI file. Each of the
+ * XPI and JAR files contained in the XPI will be extracted. Any that
+ * do not contain valid add-ons will be ignored. The first valid add-on will
+ * be installed by this AddonInstall instance, the rest will have new
+ * AddonInstall instances created for them.
+ *
+ * @param aZipReader
+ * An open nsIZipReader for the multi-package XPI's files. This will
+ * be closed before this method returns.
+ * @param aCallback
+ * A function to call when all of the add-on manifests have been
+ * loaded. Because this loadMultipackageManifests is an internal API
+ * we don't exception-wrap this callback
+ */
+ _loadMultipackageManifests: function AI_loadMultipackageManifests(aZipReader,
+ aCallback) {
+ let files = [];
+ let entries = aZipReader.findEntries("(*.[Xx][Pp][Ii]|*.[Jj][Aa][Rr])");
+ while (entries.hasMore()) {
+ let entryName = entries.getNext();
+ var target = getTemporaryFile();
+ try {
+ aZipReader.extract(entryName, target);
+ files.push(target);
+ }
+ catch (e) {
+ logger.warn("Failed to extract " + entryName + " from multi-package " +
+ "XPI", e);
+ target.remove(false);
+ }
+ }
+
+ aZipReader.close();
+
+ if (files.length == 0) {
+ throw new Error("Multi-package XPI does not contain any packages " +
+ "to install");
+ }
+
+ let addon = null;
+
+ // Find the first file that has a valid install manifest and use it for
+ // the add-on that this AddonInstall instance will install.
+ while (files.length > 0) {
+ this.removeTemporaryFile();
+ this.file = files.shift();
+ this.ownsTempFile = true;
+ try {
+ addon = loadManifestFromZipFile(this.file);
+ break;
+ }
+ catch (e) {
+ logger.warn(this.file.leafName + " cannot be installed from multi-package " +
+ "XPI", e);
+ }
+ }
+
+ if (!addon) {
+ // No valid add-on was found
+ aCallback();
+ return;
+ }
+
+ this.addon = addon;
+
+ this.updateAddonURIs();
+
+ this.addon._install = this;
+ this.name = this.addon.selectedLocale.name || this.addon.defaultLocale.name;
+ this.type = this.addon.type;
+ this.version = this.addon.version;
+
+ // Setting the iconURL to something inside the XPI locks the XPI and
+ // makes it impossible to delete on Windows.
+ //let newIcon = createWrapper(this.addon).iconURL;
+ //if (newIcon)
+ // this.iconURL = newIcon;
+
+ // Create new AddonInstall instances for every remaining file
+ if (files.length > 0) {
+ this.linkedInstalls = [];
+ let count = 0;
+ let self = this;
+ files.forEach(function(file) {
+ AddonInstall.createInstall(function loadMultipackageManifests_createInstall(aInstall) {
+ // Ignore bad add-ons (createInstall will have logged the error)
+ if (aInstall.state == AddonManager.STATE_DOWNLOAD_FAILED) {
+ // Manually remove the temporary file
+ file.remove(true);
+ }
+ else {
+ // Make the new install own its temporary file
+ aInstall.ownsTempFile = true;
+
+ self.linkedInstalls.push(aInstall)
+
+ aInstall.sourceURI = self.sourceURI;
+ aInstall.releaseNotesURI = self.releaseNotesURI;
+ aInstall.updateAddonURIs();
+ }
+
+ count++;
+ if (count == files.length)
+ aCallback();
+ }, file);
+ }, this);
+ }
+ else {
+ aCallback();
+ }
+ },
+
+ /**
+ * Called after the add-on is a local file and the signature and install
+ * manifest can be read.
+ *
+ * @param aCallback
+ * A function to call when the manifest has been loaded
+ * @throws if the add-on does not contain a valid install manifest or the
+ * XPI is incorrectly signed
+ */
+ loadManifest: function AI_loadManifest(aCallback) {
+ aCallback = makeSafe(aCallback);
+ let self = this;
+ function addRepositoryData(aAddon) {
+ // Try to load from the existing cache first
+ AddonRepository.getCachedAddonByID(aAddon.id, function loadManifest_getCachedAddonByID(aRepoAddon) {
+ if (aRepoAddon) {
+ aAddon._repositoryAddon = aRepoAddon;
+ self.name = self.name || aAddon._repositoryAddon.name;
+ aAddon.compatibilityOverrides = aRepoAddon.compatibilityOverrides;
+ aAddon.appDisabled = !isUsableAddon(aAddon);
+ aCallback();
+ return;
+ }
+
+ // It wasn't there so try to re-download it
+ AddonRepository.cacheAddons([aAddon.id], function loadManifest_cacheAddons() {
+ AddonRepository.getCachedAddonByID(aAddon.id, function loadManifest_getCachedAddonByID(aRepoAddon) {
+ aAddon._repositoryAddon = aRepoAddon;
+ self.name = self.name || aAddon._repositoryAddon.name;
+ aAddon.compatibilityOverrides = aRepoAddon ?
+ aRepoAddon.compatibilityOverrides :
+ null;
+ aAddon.appDisabled = !isUsableAddon(aAddon);
+ aCallback();
+ });
+ });
+ });
+ }
+
+ let zipreader = Cc["@mozilla.org/libjar/zip-reader;1"].
+ createInstance(Ci.nsIZipReader);
+ try {
+ zipreader.open(this.file);
+ }
+ catch (e) {
+ zipreader.close();
+ throw e;
+ }
+
+ let x509 = zipreader.getSigningCert(null);
+ if (x509) {
+ logger.debug("Verifying XPI signature");
+ if (verifyZipSigning(zipreader, x509)) {
+ this.certificate = x509;
+ if (this.certificate.commonName.length > 0) {
+ this.certName = this.certificate.commonName;
+ } else {
+ this.certName = this.certificate.organization;
+ }
+ } else {
+ zipreader.close();
+ throw new Error("XPI is incorrectly signed");
+ }
+ }
+
+ try {
+ this.addon = loadManifestFromZipReader(zipreader);
+ }
+ catch (e) {
+ zipreader.close();
+ throw e;
+ }
+
+ if (this.addon.type == "multipackage") {
+ this._loadMultipackageManifests(zipreader, function loadManifest_loadMultipackageManifests() {
+ addRepositoryData(self.addon);
+ });
+ return;
+ }
+
+ zipreader.close();
+
+ this.updateAddonURIs();
+
+ this.addon._install = this;
+ this.name = this.addon.selectedLocale.name || this.addon.defaultLocale.name;
+ this.type = this.addon.type;
+ this.version = this.addon.version;
+
+ // Setting the iconURL to something inside the XPI locks the XPI and
+ // makes it impossible to delete on Windows.
+ //let newIcon = createWrapper(this.addon).iconURL;
+ //if (newIcon)
+ // this.iconURL = newIcon;
+
+ addRepositoryData(this.addon);
+ },
+
+ observe: function AI_observe(aSubject, aTopic, aData) {
+ // Network is going offline
+ this.cancel();
+ },
+
+ /**
+ * Starts downloading the add-on's XPI file.
+ */
+ startDownload: function AI_startDownload() {
+ this.state = AddonManager.STATE_DOWNLOADING;
+ if (!AddonManagerPrivate.callInstallListeners("onDownloadStarted",
+ this.listeners, this.wrapper)) {
+ logger.debug("onDownloadStarted listeners cancelled installation of addon " + this.sourceURI.spec);
+ this.state = AddonManager.STATE_CANCELLED;
+ XPIProvider.removeActiveInstall(this);
+ AddonManagerPrivate.callInstallListeners("onDownloadCancelled",
+ this.listeners, this.wrapper)
+ return;
+ }
+
+ // If a listener changed our state then do not proceed with the download
+ if (this.state != AddonManager.STATE_DOWNLOADING)
+ return;
+
+ if (this.channel) {
+ // A previous download attempt hasn't finished cleaning up yet, signal
+ // that it should restart when complete
+ logger.debug("Waiting for previous download to complete");
+ this.restartDownload = true;
+ return;
+ }
+
+ this.openChannel();
+ },
+
+ openChannel: function AI_openChannel() {
+ this.restartDownload = false;
+
+ try {
+ this.file = getTemporaryFile();
+ this.ownsTempFile = true;
+ this.stream = Cc["@mozilla.org/network/file-output-stream;1"].
+ createInstance(Ci.nsIFileOutputStream);
+ this.stream.init(this.file, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE |
+ FileUtils.MODE_TRUNCATE, FileUtils.PERMS_FILE, 0);
+ }
+ catch (e) {
+ logger.warn("Failed to start download for addon " + this.sourceURI.spec, e);
+ this.state = AddonManager.STATE_DOWNLOAD_FAILED;
+ this.error = AddonManager.ERROR_FILE_ACCESS;
+ XPIProvider.removeActiveInstall(this);
+ AddonManagerPrivate.callInstallListeners("onDownloadFailed",
+ this.listeners, this.wrapper);
+ return;
+ }
+
+ let listener = Cc["@mozilla.org/network/stream-listener-tee;1"].
+ createInstance(Ci.nsIStreamListenerTee);
+ listener.init(this, this.stream);
+ try {
+ Components.utils.import("resource://gre/modules/CertUtils.jsm");
+ let requireBuiltIn = Preferences.get(PREF_INSTALL_REQUIREBUILTINCERTS, true);
+ this.badCertHandler = new BadCertHandler(!requireBuiltIn);
+
+ this.channel = NetUtil.newChannel2(this.sourceURI,
+ null,
+ null,
+ null, // aLoadingNode
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null, // aTriggeringPrincipal
+ Ci.nsILoadInfo.SEC_NORMAL,
+ Ci.nsIContentPolicy.TYPE_OTHER);
+ this.channel.notificationCallbacks = this;
+ if (this.channel instanceof Ci.nsIHttpChannel) {
+ this.channel.setRequestHeader("Moz-XPI-Update", "1", true);
+ if (this.channel instanceof Ci.nsIHttpChannelInternal)
+ this.channel.forceAllowThirdPartyCookie = true;
+ }
+ this.channel.asyncOpen(listener, null);
+
+ Services.obs.addObserver(this, "network:offline-about-to-go-offline", false);
+ }
+ catch (e) {
+ logger.warn("Failed to start download for addon " + this.sourceURI.spec, e);
+ this.state = AddonManager.STATE_DOWNLOAD_FAILED;
+ this.error = AddonManager.ERROR_NETWORK_FAILURE;
+ XPIProvider.removeActiveInstall(this);
+ AddonManagerPrivate.callInstallListeners("onDownloadFailed",
+ this.listeners, this.wrapper);
+ }
+ },
+
+ /**
+ * Update the crypto hasher with the new data and call the progress listeners.
+ *
+ * @see nsIStreamListener
+ */
+ onDataAvailable: function AI_onDataAvailable(aRequest, aContext, aInputstream,
+ aOffset, aCount) {
+ this.crypto.updateFromStream(aInputstream, aCount);
+ this.progress += aCount;
+ if (!AddonManagerPrivate.callInstallListeners("onDownloadProgress",
+ this.listeners, this.wrapper)) {
+ // TODO cancel the download and make it available again (bug 553024)
+ }
+ },
+
+ /**
+ * Check the redirect response for a hash of the target XPI and verify that
+ * we don't end up on an insecure channel.
+ *
+ * @see nsIChannelEventSink
+ */
+ asyncOnChannelRedirect: function AI_asyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, aCallback) {
+ if (!this.hash && aOldChannel.originalURI.schemeIs("https") &&
+ aOldChannel instanceof Ci.nsIHttpChannel) {
+ try {
+ let hashStr = aOldChannel.getResponseHeader("X-Target-Digest");
+ let hashSplit = hashStr.toLowerCase().split(":");
+ this.hash = {
+ algorithm: hashSplit[0],
+ data: hashSplit[1]
+ };
+ }
+ catch (e) {
+ }
+ }
+
+ // Verify that we don't end up on an insecure channel if we haven't got a
+ // hash to verify with (see bug 537761 for discussion)
+ if (!this.hash)
+ this.badCertHandler.asyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, aCallback);
+ else
+ aCallback.onRedirectVerifyCallback(Cr.NS_OK);
+
+ this.channel = aNewChannel;
+ },
+
+ /**
+ * This is the first chance to get at real headers on the channel.
+ *
+ * @see nsIStreamListener
+ */
+ onStartRequest: function AI_onStartRequest(aRequest, aContext) {
+ this.crypto = Cc["@mozilla.org/security/hash;1"].
+ createInstance(Ci.nsICryptoHash);
+ if (this.hash) {
+ try {
+ this.crypto.initWithString(this.hash.algorithm);
+ }
+ catch (e) {
+ logger.warn("Unknown hash algorithm '" + this.hash.algorithm + "' for addon " + this.sourceURI.spec, e);
+ this.state = AddonManager.STATE_DOWNLOAD_FAILED;
+ this.error = AddonManager.ERROR_INCORRECT_HASH;
+ XPIProvider.removeActiveInstall(this);
+ AddonManagerPrivate.callInstallListeners("onDownloadFailed",
+ this.listeners, this.wrapper);
+ aRequest.cancel(Cr.NS_BINDING_ABORTED);
+ return;
+ }
+ }
+ else {
+ // We always need something to consume data from the inputstream passed
+ // to onDataAvailable so just create a dummy cryptohasher to do that.
+ this.crypto.initWithString("sha1");
+ }
+
+ this.progress = 0;
+ if (aRequest instanceof Ci.nsIChannel) {
+ try {
+ this.maxProgress = aRequest.contentLength;
+ }
+ catch (e) {
+ }
+ logger.debug("Download started for " + this.sourceURI.spec + " to file " +
+ this.file.path);
+ }
+ },
+
+ /**
+ * The download is complete.
+ *
+ * @see nsIStreamListener
+ */
+ onStopRequest: function AI_onStopRequest(aRequest, aContext, aStatus) {
+ this.stream.close();
+ this.channel = null;
+ this.badCerthandler = null;
+ Services.obs.removeObserver(this, "network:offline-about-to-go-offline");
+
+ // If the download was cancelled then update the state and send events
+ if (aStatus == Cr.NS_BINDING_ABORTED) {
+ if (this.state == AddonManager.STATE_DOWNLOADING) {
+ logger.debug("Cancelled download of " + this.sourceURI.spec);
+ this.state = AddonManager.STATE_CANCELLED;
+ XPIProvider.removeActiveInstall(this);
+ AddonManagerPrivate.callInstallListeners("onDownloadCancelled",
+ this.listeners, this.wrapper);
+ // If a listener restarted the download then there is no need to
+ // remove the temporary file
+ if (this.state != AddonManager.STATE_CANCELLED)
+ return;
+ }
+
+ this.removeTemporaryFile();
+ if (this.restartDownload)
+ this.openChannel();
+ return;
+ }
+
+ logger.debug("Download of " + this.sourceURI.spec + " completed.");
+
+ if (Components.isSuccessCode(aStatus)) {
+ if (!(aRequest instanceof Ci.nsIHttpChannel) || aRequest.requestSucceeded) {
+ if (!this.hash && (aRequest instanceof Ci.nsIChannel)) {
+ try {
+ checkCert(aRequest,
+ !Preferences.get(PREF_INSTALL_REQUIREBUILTINCERTS, true));
+ }
+ catch (e) {
+ this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, e);
+ return;
+ }
+ }
+
+ // convert the binary hash data to a hex string.
+ let calculatedHash = getHashStringForCrypto(this.crypto);
+ this.crypto = null;
+ if (this.hash && calculatedHash != this.hash.data) {
+ this.downloadFailed(AddonManager.ERROR_INCORRECT_HASH,
+ "Downloaded file hash (" + calculatedHash +
+ ") did not match provided hash (" + this.hash.data + ")");
+ return;
+ }
+ try {
+ let self = this;
+ this.loadManifest(function onStopRequest_loadManifest() {
+ if (self.addon.isCompatible) {
+ self.downloadCompleted();
+ }
+ else {
+ // TODO Should we send some event here (bug 557716)?
+ self.state = AddonManager.STATE_CHECKING;
+ new UpdateChecker(self.addon, {
+ onUpdateFinished: function onStopRequest_onUpdateFinished(aAddon) {
+ self.downloadCompleted();
+ }
+ }, AddonManager.UPDATE_WHEN_ADDON_INSTALLED);
+ }
+ });
+ }
+ catch (e) {
+ if (e.webext) {
+ this.downloadFailed(AddonManager.ERROR_WEBEXT_FILE, e);
+ } else {
+ this.downloadFailed(AddonManager.ERROR_CORRUPT_FILE, e);
+ }
+ }
+ }
+ else {
+ if (aRequest instanceof Ci.nsIHttpChannel)
+ this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE,
+ aRequest.responseStatus + " " +
+ aRequest.responseStatusText);
+ else
+ this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, aStatus);
+ }
+ }
+ else {
+ this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, aStatus);
+ }
+ },
+
+ /**
+ * Notify listeners that the download failed.
+ *
+ * @param aReason
+ * Something to log about the failure
+ * @param error
+ * The error code to pass to the listeners
+ */
+ downloadFailed: function AI_downloadFailed(aReason, aError) {
+ logger.warn("Download of " + this.sourceURI.spec + " failed", aError);
+ this.state = AddonManager.STATE_DOWNLOAD_FAILED;
+ this.error = aReason;
+ XPIProvider.removeActiveInstall(this);
+ AddonManagerPrivate.callInstallListeners("onDownloadFailed", this.listeners,
+ this.wrapper);
+
+ // If the listener hasn't restarted the download then remove any temporary
+ // file
+ if (this.state == AddonManager.STATE_DOWNLOAD_FAILED) {
+ logger.debug("downloadFailed: removing temp file for " + this.sourceURI.spec);
+ this.removeTemporaryFile();
+ }
+ else
+ logger.debug("downloadFailed: listener changed AddonInstall state for " +
+ this.sourceURI.spec + " to " + this.state);
+ },
+
+ /**
+ * Notify listeners that the download completed.
+ */
+ downloadCompleted: function AI_downloadCompleted() {
+ let self = this;
+ XPIDatabase.getVisibleAddonForID(this.addon.id, function downloadCompleted_getVisibleAddonForID(aAddon) {
+ if (aAddon)
+ self.existingAddon = aAddon;
+
+ self.state = AddonManager.STATE_DOWNLOADED;
+ self.addon.updateDate = Date.now();
+
+ if (self.existingAddon) {
+ self.addon.existingAddonID = self.existingAddon.id;
+ self.addon.installDate = self.existingAddon.installDate;
+ applyBlocklistChanges(self.existingAddon, self.addon);
+ }
+ else {
+ self.addon.installDate = self.addon.updateDate;
+ }
+
+ if (AddonManagerPrivate.callInstallListeners("onDownloadEnded",
+ self.listeners,
+ self.wrapper)) {
+ // If a listener changed our state then do not proceed with the install
+ if (self.state != AddonManager.STATE_DOWNLOADED)
+ return;
+
+ self.install();
+
+ if (self.linkedInstalls) {
+ self.linkedInstalls.forEach(function(aInstall) {
+ aInstall.install();
+ });
+ }
+ }
+ });
+ },
+
+ // TODO This relies on the assumption that we are always installing into the
+ // highest priority install location so the resulting add-on will be visible
+ // overriding any existing copy in another install location (bug 557710).
+ /**
+ * Installs the add-on into the install location.
+ */
+ startInstall: function AI_startInstall() {
+ this.state = AddonManager.STATE_INSTALLING;
+ if (!AddonManagerPrivate.callInstallListeners("onInstallStarted",
+ this.listeners, this.wrapper)) {
+ this.state = AddonManager.STATE_DOWNLOADED;
+ XPIProvider.removeActiveInstall(this);
+ AddonManagerPrivate.callInstallListeners("onInstallCancelled",
+ this.listeners, this.wrapper)
+ return;
+ }
+
+ // Find and cancel any pending installs for the same add-on in the same
+ // install location
+ for (let aInstall of XPIProvider.installs) {
+ if (aInstall.state == AddonManager.STATE_INSTALLED &&
+ aInstall.installLocation == this.installLocation &&
+ aInstall.addon.id == this.addon.id) {
+ logger.debug("Cancelling previous pending install of " + aInstall.addon.id);
+ aInstall.cancel();
+ }
+ }
+
+ let isUpgrade = this.existingAddon &&
+ this.existingAddon._installLocation == this.installLocation;
+ let requiresRestart = XPIProvider.installRequiresRestart(this.addon);
+
+ logger.debug("Starting install of " + this.addon.id + " from " + this.sourceURI.spec);
+ AddonManagerPrivate.callAddonListeners("onInstalling",
+ createWrapper(this.addon),
+ requiresRestart);
+
+ let stagingDir = this.installLocation.getStagingDir();
+ let stagedAddon = stagingDir.clone();
+
+ Task.spawn((function() {
+ let installedUnpacked = 0;
+ yield this.installLocation.requestStagingDir();
+
+ // Remove any staged items for this add-on
+ stagedAddon.append(this.addon.id);
+ yield removeAsync(stagedAddon);
+ stagedAddon.leafName = this.addon.id + ".xpi";
+ yield removeAsync(stagedAddon);
+
+ // First stage the file regardless of whether restarting is necessary
+ if (this.addon.unpack || Preferences.get(PREF_XPI_UNPACK, false)) {
+ logger.debug("Addon " + this.addon.id + " will be installed as " +
+ "an unpacked directory");
+ stagedAddon.leafName = this.addon.id;
+ yield OS.File.makeDir(stagedAddon.path);
+ yield ZipUtils.extractFilesAsync(this.file, stagedAddon);
+ installedUnpacked = 1;
+ }
+ else {
+ logger.debug("Addon " + this.addon.id + " will be installed as " +
+ "a packed xpi");
+ stagedAddon.leafName = this.addon.id + ".xpi";
+ yield OS.File.copy(this.file.path, stagedAddon.path);
+ }
+
+ if (requiresRestart) {
+ // Point the add-on to its extracted files as the xpi may get deleted
+ this.addon._sourceBundle = stagedAddon;
+
+ // Cache the AddonInternal as it may have updated compatibility info
+ let stagedJSON = stagedAddon.clone();
+ stagedJSON.leafName = this.addon.id + ".json";
+ if (stagedJSON.exists())
+ stagedJSON.remove(true);
+ let stream = Cc["@mozilla.org/network/file-output-stream;1"].
+ createInstance(Ci.nsIFileOutputStream);
+ let converter = Cc["@mozilla.org/intl/converter-output-stream;1"].
+ createInstance(Ci.nsIConverterOutputStream);
+
+ try {
+ stream.init(stagedJSON, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE |
+ FileUtils.MODE_TRUNCATE, FileUtils.PERMS_FILE,
+ 0);
+ converter.init(stream, "UTF-8", 0, 0x0000);
+ converter.writeString(JSON.stringify(this.addon));
+ }
+ finally {
+ converter.close();
+ stream.close();
+ }
+
+ logger.debug("Staged install of " + this.addon.id + " from " + this.sourceURI.spec + " ready; waiting for restart.");
+ this.state = AddonManager.STATE_INSTALLED;
+ if (isUpgrade) {
+ delete this.existingAddon.pendingUpgrade;
+ this.existingAddon.pendingUpgrade = this.addon;
+ }
+ AddonManagerPrivate.callInstallListeners("onInstallEnded",
+ this.listeners, this.wrapper,
+ createWrapper(this.addon));
+ }
+ else {
+ // The install is completed so it should be removed from the active list
+ XPIProvider.removeActiveInstall(this);
+
+ // TODO We can probably reduce the number of DB operations going on here
+ // We probably also want to support rolling back failed upgrades etc.
+ // See bug 553015.
+
+ // Deactivate and remove the old add-on as necessary
+ let reason = BOOTSTRAP_REASONS.ADDON_INSTALL;
+ if (this.existingAddon) {
+ if (Services.vc.compare(this.existingAddon.version, this.addon.version) < 0)
+ reason = BOOTSTRAP_REASONS.ADDON_UPGRADE;
+ else
+ reason = BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
+
+ if (this.existingAddon.bootstrap) {
+ let file = this.existingAddon._installLocation
+ .getLocationForID(this.existingAddon.id);
+ if (this.existingAddon.active) {
+ XPIProvider.callBootstrapMethod(this.existingAddon, file,
+ "shutdown", reason,
+ { newVersion: this.addon.version });
+ }
+
+ XPIProvider.callBootstrapMethod(this.existingAddon, file,
+ "uninstall", reason,
+ { newVersion: this.addon.version });
+ XPIProvider.unloadBootstrapScope(this.existingAddon.id);
+ flushStartupCache();
+ }
+
+ if (!isUpgrade && this.existingAddon.active) {
+ XPIDatabase.updateAddonActive(this.existingAddon, false);
+ }
+ }
+
+ // Install the new add-on into its final location
+ let existingAddonID = this.existingAddon ? this.existingAddon.id : null;
+ let file = this.installLocation.installAddon(this.addon.id, stagedAddon,
+ existingAddonID);
+
+ // Update the metadata in the database
+ this.addon._sourceBundle = file;
+ this.addon._installLocation = this.installLocation;
+ this.addon.visible = true;
+
+ if (isUpgrade) {
+ this.addon = XPIDatabase.updateAddonMetadata(this.existingAddon, this.addon,
+ file.persistentDescriptor);
+ let state = XPIStates.getAddon(this.installLocation.name, this.addon.id);
+ if (state) {
+ state.syncWithDB(this.addon, true);
+ } else {
+ logger.warn("Unexpected missing XPI state for add-on ${id}", this.addon);
+ }
+ }
+ else {
+ this.addon.active = (this.addon.visible && !this.addon.disabled);
+ this.addon = XPIDatabase.addAddonMetadata(this.addon, file.persistentDescriptor);
+ XPIStates.addAddon(this.addon);
+ this.addon.installDate = this.addon.updateDate;
+ XPIDatabase.saveChanges();
+ }
+ XPIStates.save();
+
+ let extraParams = {};
+ if (this.existingAddon) {
+ extraParams.oldVersion = this.existingAddon.version;
+ }
+
+ if (this.addon.bootstrap) {
+ XPIProvider.callBootstrapMethod(this.addon, file, "install",
+ reason, extraParams);
+ }
+
+ AddonManagerPrivate.callAddonListeners("onInstalled",
+ createWrapper(this.addon));
+
+ logger.debug("Install of " + this.sourceURI.spec + " completed.");
+ this.state = AddonManager.STATE_INSTALLED;
+ AddonManagerPrivate.callInstallListeners("onInstallEnded",
+ this.listeners, this.wrapper,
+ createWrapper(this.addon));
+
+ if (this.addon.bootstrap) {
+ if (this.addon.active) {
+ XPIProvider.callBootstrapMethod(this.addon, file, "startup",
+ reason, extraParams);
+ }
+ else {
+ // XXX this makes it dangerous to do some things in onInstallEnded
+ // listeners because important cleanup hasn't been done yet
+ XPIProvider.unloadBootstrapScope(this.addon.id);
+ }
+ }
+ }
+ }).bind(this)).then(null, (e) => {
+ logger.warn("Failed to install " + this.file.path + " from " + this.sourceURI.spec, e);
+ if (stagedAddon.exists())
+ recursiveRemove(stagedAddon);
+ this.state = AddonManager.STATE_INSTALL_FAILED;
+ this.error = AddonManager.ERROR_FILE_ACCESS;
+ XPIProvider.removeActiveInstall(this);
+ AddonManagerPrivate.callAddonListeners("onOperationCancelled",
+ createWrapper(this.addon));
+ AddonManagerPrivate.callInstallListeners("onInstallFailed",
+ this.listeners,
+ this.wrapper);
+ }).then(() => {
+ this.removeTemporaryFile();
+ return this.installLocation.releaseStagingDir();
+ });
+ },
+
+ getInterface: function AI_getInterface(iid) {
+ if (iid.equals(Ci.nsIAuthPrompt2)) {
+ let win = this.window;
+ if (!win && this.browser)
+ win = this.browser.ownerDocument.defaultView;
+
+ let factory = Cc["@mozilla.org/prompter;1"].
+ getService(Ci.nsIPromptFactory);
+ let prompt = factory.getPrompt(win, Ci.nsIAuthPrompt2);
+
+ if (this.browser && this.browser.isRemoteBrowser && prompt instanceof Ci.nsILoginManagerPrompter)
+ prompt.setE10sData(this.browser, null);
+
+ return prompt;
+ }
+ else if (iid.equals(Ci.nsIChannelEventSink)) {
+ return this;
+ }
+
+ return this.badCertHandler.getInterface(iid);
+ }
+}
+
+/**
+ * Creates a new AddonInstall for an already staged install. Used when
+ * installing the staged install failed for some reason.
+ *
+ * @param aDir
+ * The directory holding the staged install
+ * @param aManifest
+ * The cached manifest for the install
+ */
+AddonInstall.createStagedInstall = function AI_createStagedInstall(aInstallLocation, aDir, aManifest) {
+ let url = Services.io.newFileURI(aDir);
+
+ let install = new AddonInstall(aInstallLocation, aDir);
+ install.initStagedInstall(aManifest);
+};
+
+/**
+ * Creates a new AddonInstall to install an add-on from a local file. Installs
+ * always go into the profile install location.
+ *
+ * @param aCallback
+ * The callback to pass the new AddonInstall to
+ * @param aFile
+ * The file to install
+ */
+AddonInstall.createInstall = function AI_createInstall(aCallback, aFile) {
+ let location = XPIProvider.installLocationsByName[KEY_APP_PROFILE];
+ let url = Services.io.newFileURI(aFile);
+
+ try {
+ let install = new AddonInstall(location, url);
+ install.initLocalInstall(aCallback);
+ }
+ catch(e) {
+ logger.error("Error creating install", e);
+ makeSafe(aCallback)(null);
+ }
+};
+
+/**
+ * Creates a new AddonInstall to download and install a URL.
+ *
+ * @param aCallback
+ * The callback to pass the new AddonInstall to
+ * @param aUri
+ * The URI to download
+ * @param aHash
+ * A hash for the add-on
+ * @param aName
+ * A name for the add-on
+ * @param aIcons
+ * An icon URLs for the add-on
+ * @param aVersion
+ * A version for the add-on
+ * @param aBrowser
+ * The browser performing the install
+ */
+AddonInstall.createDownload = function AI_createDownload(aCallback, aUri, aHash, aName, aIcons,
+ aVersion, aBrowser) {
+ let location = XPIProvider.installLocationsByName[KEY_APP_PROFILE];
+ let url = NetUtil.newURI(aUri);
+
+ let install = new AddonInstall(location, url, aHash, null, null, aBrowser);
+ if (url instanceof Ci.nsIFileURL)
+ install.initLocalInstall(aCallback);
+ else
+ install.initAvailableDownload(aName, null, aIcons, aVersion, aCallback);
+};
+
+/**
+ * Creates a new AddonInstall for an update.
+ *
+ * @param aCallback
+ * The callback to pass the new AddonInstall to
+ * @param aAddon
+ * The add-on being updated
+ * @param aUpdate
+ * The metadata about the new version from the update manifest
+ */
+AddonInstall.createUpdate = function AI_createUpdate(aCallback, aAddon, aUpdate) {
+ let url = NetUtil.newURI(aUpdate.updateURL);
+ let releaseNotesURI = null;
+ try {
+ if (aUpdate.updateInfoURL)
+ releaseNotesURI = NetUtil.newURI(escapeAddonURI(aAddon, aUpdate.updateInfoURL));
+ }
+ catch (e) {
+ // If the releaseNotesURI cannot be parsed then just ignore it.
+ }
+
+ let install = new AddonInstall(aAddon._installLocation, url,
+ aUpdate.updateHash, releaseNotesURI, aAddon);
+ if (url instanceof Ci.nsIFileURL) {
+ install.initLocalInstall(aCallback);
+ }
+ else {
+ install.initAvailableDownload(aAddon.selectedLocale.name ?
+ aAddon.selectedLocale.name : aAddon.defaultLocale.name,
+ aAddon.type, aAddon.icons, aUpdate.version, aCallback);
+ }
+};
+
+/**
+ * Creates a wrapper for an AddonInstall that only exposes the public API
+ *
+ * @param install
+ * The AddonInstall to create a wrapper for
+ */
+function AddonInstallWrapper(aInstall) {
+#ifdef MOZ_EM_DEBUG
+ this.__defineGetter__("__AddonInstallInternal__", function AIW_debugGetter() {
+ return aInstall;
+ });
+#endif
+
+ ["name", "type", "version", "icons", "releaseNotesURI", "file", "state", "error",
+ "progress", "maxProgress", "certificate", "certName"].forEach(function(aProp) {
+ this.__defineGetter__(aProp, function AIW_propertyGetter() aInstall[aProp]);
+ }, this);
+
+ this.__defineGetter__("iconURL", function AIW_iconURL() aInstall.icons[32]);
+
+ this.__defineGetter__("existingAddon", function AIW_existingAddonGetter() {
+ return createWrapper(aInstall.existingAddon);
+ });
+ this.__defineGetter__("addon", function AIW_addonGetter() createWrapper(aInstall.addon));
+ this.__defineGetter__("sourceURI", function AIW_sourceURIGetter() aInstall.sourceURI);
+
+ this.__defineGetter__("linkedInstalls", function AIW_linkedInstallsGetter() {
+ if (!aInstall.linkedInstalls)
+ return null;
+ // Tycho: return [i.wrapper for each (i in aInstall.linkedInstalls)];
+ let result = [];
+ for each (let i in aInstall.linkedInstalls) {
+ result.push(i.wrapper);
+ }
+
+ return result;
+ });
+
+ this.install = function AIW_install() {
+ aInstall.install();
+ }
+
+ this.cancel = function AIW_cancel() {
+ aInstall.cancel();
+ }
+
+ this.addListener = function AIW_addListener(listener) {
+ aInstall.addListener(listener);
+ }
+
+ this.removeListener = function AIW_removeListener(listener) {
+ aInstall.removeListener(listener);
+ }
+}
+
+AddonInstallWrapper.prototype = {};
+
+/**
+ * Creates a new update checker.
+ *
+ * @param aAddon
+ * The add-on to check for updates
+ * @param aListener
+ * An UpdateListener to notify of updates
+ * @param aReason
+ * The reason for the update check
+ * @param aAppVersion
+ * An optional application version to check for updates for
+ * @param aPlatformVersion
+ * An optional platform version to check for updates for
+ * @throws if the aListener or aReason arguments are not valid
+ */
+function UpdateChecker(aAddon, aListener, aReason, aAppVersion, aPlatformVersion) {
+ if (!aListener || !aReason)
+ throw Cr.NS_ERROR_INVALID_ARG;
+
+ Components.utils.import("resource://gre/modules/addons/AddonUpdateChecker.jsm");
+
+ this.addon = aAddon;
+ aAddon._updateCheck = this;
+ XPIProvider.doing(this);
+ this.listener = aListener;
+ this.appVersion = aAppVersion;
+ this.platformVersion = aPlatformVersion;
+ this.syncCompatibility = (aReason == AddonManager.UPDATE_WHEN_NEW_APP_INSTALLED);
+
+ let updateURL = aAddon.updateURL;
+ if (!updateURL) {
+ if (aReason == AddonManager.UPDATE_WHEN_PERIODIC_UPDATE &&
+ Services.prefs.getPrefType(PREF_EM_UPDATE_BACKGROUND_URL) == Services.prefs.PREF_STRING) {
+ updateURL = Services.prefs.getCharPref(PREF_EM_UPDATE_BACKGROUND_URL);
+ } else {
+ updateURL = Services.prefs.getCharPref(PREF_EM_UPDATE_URL);
+ }
+ }
+
+ const UPDATE_TYPE_COMPATIBILITY = 32;
+ const UPDATE_TYPE_NEWVERSION = 64;
+
+ aReason |= UPDATE_TYPE_COMPATIBILITY;
+ if ("onUpdateAvailable" in this.listener)
+ aReason |= UPDATE_TYPE_NEWVERSION;
+
+ let url = escapeAddonURI(aAddon, updateURL, aReason, aAppVersion);
+ this._parser = AddonUpdateChecker.checkForUpdates(aAddon.id, aAddon.updateKey,
+ url, this);
+}
+
+UpdateChecker.prototype = {
+ addon: null,
+ listener: null,
+ appVersion: null,
+ platformVersion: null,
+ syncCompatibility: null,
+
+ /**
+ * Calls a method on the listener passing any number of arguments and
+ * consuming any exceptions.
+ *
+ * @param aMethod
+ * The method to call on the listener
+ */
+ callListener: function UC_callListener(aMethod, ...aArgs) {
+ if (!(aMethod in this.listener))
+ return;
+
+ try {
+ this.listener[aMethod].apply(this.listener, aArgs);
+ }
+ catch (e) {
+ logger.warn("Exception calling UpdateListener method " + aMethod, e);
+ }
+ },
+
+ /**
+ * Called when AddonUpdateChecker completes the update check
+ *
+ * @param updates
+ * The list of update details for the add-on
+ */
+ onUpdateCheckComplete: function UC_onUpdateCheckComplete(aUpdates) {
+ XPIProvider.done(this.addon._updateCheck);
+ this.addon._updateCheck = null;
+ let AUC = AddonUpdateChecker;
+
+ let ignoreMaxVersion = false;
+ let ignoreStrictCompat = false;
+ if (!AddonManager.checkCompatibility) {
+ ignoreMaxVersion = true;
+ ignoreStrictCompat = true;
+ } else if (this.addon.type in COMPATIBLE_BY_DEFAULT_TYPES &&
+ !AddonManager.strictCompatibility &&
+ !this.addon.strictCompatibility &&
+ !this.addon.hasBinaryComponents) {
+ ignoreMaxVersion = true;
+ }
+
+ // Always apply any compatibility update for the current version
+ let compatUpdate = AUC.getCompatibilityUpdate(aUpdates, this.addon.version,
+ this.syncCompatibility,
+ null, null,
+ ignoreMaxVersion,
+ ignoreStrictCompat);
+ // Apply the compatibility update to the database
+ if (compatUpdate)
+ this.addon.applyCompatibilityUpdate(compatUpdate, this.syncCompatibility);
+
+ // If the request is for an application or platform version that is
+ // different to the current application or platform version then look for a
+ // compatibility update for those versions.
+ if ((this.appVersion &&
+ Services.vc.compare(this.appVersion, Services.appinfo.version) != 0) ||
+ (this.platformVersion &&
+ Services.vc.compare(this.platformVersion, Services.appinfo.platformVersion) != 0)) {
+ compatUpdate = AUC.getCompatibilityUpdate(aUpdates, this.addon.version,
+ false, this.appVersion,
+ this.platformVersion,
+ ignoreMaxVersion,
+ ignoreStrictCompat);
+ }
+
+ if (compatUpdate)
+ this.callListener("onCompatibilityUpdateAvailable", createWrapper(this.addon));
+ else
+ this.callListener("onNoCompatibilityUpdateAvailable", createWrapper(this.addon));
+
+ function sendUpdateAvailableMessages(aSelf, aInstall) {
+ if (aInstall) {
+ aSelf.callListener("onUpdateAvailable", createWrapper(aSelf.addon),
+ aInstall.wrapper);
+ }
+ else {
+ aSelf.callListener("onNoUpdateAvailable", createWrapper(aSelf.addon));
+ }
+ aSelf.callListener("onUpdateFinished", createWrapper(aSelf.addon),
+ AddonManager.UPDATE_STATUS_NO_ERROR);
+ }
+
+ let compatOverrides = AddonManager.strictCompatibility ?
+ null :
+ this.addon.compatibilityOverrides;
+
+ let update = AUC.getNewestCompatibleUpdate(aUpdates,
+ this.appVersion,
+ this.platformVersion,
+ ignoreMaxVersion,
+ ignoreStrictCompat,
+ compatOverrides);
+
+ if (update && Services.vc.compare(this.addon.version, update.version) < 0) {
+ for (let currentInstall of XPIProvider.installs) {
+ // Skip installs that don't match the available update
+ if (currentInstall.existingAddon != this.addon ||
+ currentInstall.version != update.version)
+ continue;
+
+ // If the existing install has not yet started downloading then send an
+ // available update notification. If it is already downloading then
+ // don't send any available update notification
+ if (currentInstall.state == AddonManager.STATE_AVAILABLE) {
+ logger.debug("Found an existing AddonInstall for " + this.addon.id);
+ sendUpdateAvailableMessages(this, currentInstall);
+ }
+ else
+ sendUpdateAvailableMessages(this, null);
+ return;
+ }
+
+ let self = this;
+ AddonInstall.createUpdate(function onUpdateCheckComplete_createUpdate(aInstall) {
+ sendUpdateAvailableMessages(self, aInstall);
+ }, this.addon, update);
+ }
+ else {
+ sendUpdateAvailableMessages(this, null);
+ }
+ },
+
+ /**
+ * Called when AddonUpdateChecker fails the update check
+ *
+ * @param aError
+ * An error status
+ */
+ onUpdateCheckError: function UC_onUpdateCheckError(aError) {
+ XPIProvider.done(this.addon._updateCheck);
+ this.addon._updateCheck = null;
+ this.callListener("onNoCompatibilityUpdateAvailable", createWrapper(this.addon));
+ this.callListener("onNoUpdateAvailable", createWrapper(this.addon));
+ this.callListener("onUpdateFinished", createWrapper(this.addon), aError);
+ },
+
+ /**
+ * Called to cancel an in-progress update check
+ */
+ cancel: function UC_cancel() {
+ let parser = this._parser;
+ if (parser) {
+ this._parser = null;
+ // This will call back to onUpdateCheckError with a CANCELLED error
+ parser.cancel();
+ }
+ }
+};
+
+/**
+ * The AddonInternal is an internal only representation of add-ons. It may
+ * have come from the database (see DBAddonInternal in XPIProviderUtils.jsm)
+ * or an install manifest.
+ */
+function AddonInternal() {
+}
+
+AddonInternal.prototype = {
+ _selectedLocale: null,
+ active: false,
+ visible: false,
+ userDisabled: false,
+ appDisabled: false,
+ softDisabled: false,
+ sourceURI: null,
+ releaseNotesURI: null,
+ foreignInstall: false,
+
+ get selectedLocale() {
+ if (this._selectedLocale)
+ return this._selectedLocale;
+ let locale = findClosestLocale(this.locales);
+ this._selectedLocale = locale ? locale : this.defaultLocale;
+ return this._selectedLocale;
+ },
+
+ get providesUpdatesSecurely() {
+ return !!(this.updateKey || !this.updateURL ||
+ this.updateURL.substring(0, 6) == "https:");
+ },
+
+ get isCompatible() {
+ return this.isCompatibleWith();
+ },
+
+ get disabled() {
+ return (this.userDisabled || this.appDisabled || this.softDisabled);
+ },
+
+ get isPlatformCompatible() {
+ if (this.targetPlatforms.length == 0)
+ return true;
+
+ let matchedOS = false;
+
+ // If any targetPlatform matches the OS and contains an ABI then we will
+ // only match a targetPlatform that contains both the current OS and ABI
+ let needsABI = false;
+
+ // Some platforms do not specify an ABI, test against null in that case.
+ let abi = null;
+ try {
+ abi = Services.appinfo.XPCOMABI;
+ }
+ catch (e) { }
+
+ // Something is causing errors in here
+ try {
+ for (let platform of this.targetPlatforms) {
+ if (platform.os == Services.appinfo.OS) {
+ if (platform.abi) {
+ needsABI = true;
+ if (platform.abi === abi)
+ return true;
+ }
+ else {
+ matchedOS = true;
+ }
+ }
+ }
+ } catch (e) {
+ let message = "Problem with addon " + this.id + " targetPlatforms "
+ + JSON.stringify(this.targetPlatforms);
+ logger.error(message, e);
+ AddonManagerPrivate.recordException("XPI", message, e);
+ // don't trust this add-on
+ return false;
+ }
+
+ return matchedOS && !needsABI;
+ },
+
+ isCompatibleWith: function AddonInternal_isCompatibleWith(aAppVersion, aPlatformVersion) {
+ // Experiments are installed through an external mechanism that
+ // limits target audience to compatible clients. We trust it knows what
+ // it's doing and skip compatibility checks.
+ //
+ // This decision does forfeit defense in depth. If the experiments system
+ // is ever wrong about targeting an add-on to a specific application
+ // or platform, the client will likely see errors.
+ if (this.type == "experiment") {
+ return true;
+ }
+
+ let app = this.matchingTargetApplication;
+ if (!app)
+ return false;
+
+ if (!aAppVersion)
+ aAppVersion = Services.appinfo.version;
+ if (!aPlatformVersion)
+ aPlatformVersion = Services.appinfo.platformVersion;
+
+ this.native = false;
+
+ let version;
+ if (app.id == Services.appinfo.ID) {
+ version = aAppVersion;
+ this.native = true;
+ }
+#ifdef MOZ_PHOENIX_EXTENSIONS
+ else if (app.id == FIREFOX_ID) {
+ version = FIREFOX_APPCOMPATVERSION;
+ if (this.type == "locale")
+ //Never allow language packs in Firefox compatibility mode
+ return false;
+ }
+#endif
+ else if (app.id == TOOLKIT_ID)
+ version = aPlatformVersion
+
+ // Only extensions and dictionaries can be compatible by default; themes
+ // and language packs always use strict compatibility checking.
+ if (this.type in COMPATIBLE_BY_DEFAULT_TYPES &&
+ !AddonManager.strictCompatibility && !this.strictCompatibility &&
+ !this.hasBinaryComponents) {
+
+ // The repository can specify compatibility overrides.
+ // Note: For now, only blacklisting is supported by overrides.
+ if (this._repositoryAddon &&
+ this._repositoryAddon.compatibilityOverrides) {
+ let overrides = this._repositoryAddon.compatibilityOverrides;
+ let override = AddonRepository.findMatchingCompatOverride(this.version,
+ overrides);
+ if (override && override.type == "incompatible")
+ return false;
+ }
+
+ // Extremely old extensions should not be compatible by default.
+ let minCompatVersion;
+#ifdef MOZ_PHOENIX_EXTENSIONS
+ if (app.id == Services.appinfo.ID || app.id == FIREFOX_ID)
+#else
+ if (app.id == Services.appinfo.ID)
+#endif
+ minCompatVersion = XPIProvider.minCompatibleAppVersion;
+ else if (app.id == TOOLKIT_ID)
+ minCompatVersion = XPIProvider.minCompatiblePlatformVersion;
+
+ if (minCompatVersion &&
+ Services.vc.compare(minCompatVersion, app.maxVersion) > 0)
+ return false;
+
+ return Services.vc.compare(version, app.minVersion) >= 0;
+ }
+
+ return (Services.vc.compare(version, app.minVersion) >= 0) &&
+ (Services.vc.compare(version, app.maxVersion) <= 0)
+ },
+
+ get matchingTargetApplication() {
+ let app = null;
+ for (let targetApp of this.targetApplications) {
+ if (targetApp.id == Services.appinfo.ID)
+ return targetApp;
+ if (targetApp.id == TOOLKIT_ID)
+ app = targetApp;
+ }
+#ifdef MOZ_PHOENIX_EXTENSIONS
+ //Special case: check for Firefox TargetApps. this has to be done AFTER
+ //the initial check to make sure appinfo.ID is preferred, even if
+ //Firefox is listed before it in the install manifest.
+ for (let targetApp of this.targetApplications) {
+ if (targetApp.id == FIREFOX_ID) //Firefox GUID
+ return targetApp;
+ }
+#endif
+ // Return toolkit ID if toolkit.
+ return app;
+ },
+
+ get blocklistState() {
+ let staticItem = findMatchingStaticBlocklistItem(this);
+ if (staticItem)
+ return staticItem.level;
+
+ return Blocklist.getAddonBlocklistState(createWrapper(this));
+ },
+
+ get blocklistURL() {
+ let staticItem = findMatchingStaticBlocklistItem(this);
+ if (staticItem) {
+ let url = Services.urlFormatter.formatURLPref("extensions.blocklist.itemURL");
+ return url.replace(/%blockID%/g, staticItem.blockID);
+ }
+
+ return Blocklist.getAddonBlocklistURL(createWrapper(this));
+ },
+
+ applyCompatibilityUpdate: function AddonInternal_applyCompatibilityUpdate(aUpdate, aSyncCompatibility) {
+ if (this.strictCompatibility) {
+ return;
+ }
+ this.targetApplications.forEach(function(aTargetApp) {
+ aUpdate.targetApplications.forEach(function(aUpdateTarget) {
+ if (aTargetApp.id == aUpdateTarget.id && (aSyncCompatibility ||
+ Services.vc.compare(aTargetApp.maxVersion, aUpdateTarget.maxVersion) < 0)) {
+ aTargetApp.minVersion = aUpdateTarget.minVersion;
+ aTargetApp.maxVersion = aUpdateTarget.maxVersion;
+ }
+ });
+ });
+ if (aUpdate.multiprocessCompatible !== undefined)
+ this.multiprocessCompatible = aUpdate.multiprocessCompatible;
+ this.appDisabled = !isUsableAddon(this);
+ },
+
+ /**
+ * getDataDirectory tries to execute the callback with two arguments:
+ * 1) the path of the data directory within the profile,
+ * 2) any exception generated from trying to build it.
+ */
+ getDataDirectory: function(callback) {
+ let parentPath = OS.Path.join(OS.Constants.Path.profileDir, "extension-data");
+ let dirPath = OS.Path.join(parentPath, this.id);
+
+ Task.spawn(function*() {
+ yield OS.File.makeDir(parentPath, {ignoreExisting: true});
+ yield OS.File.makeDir(dirPath, {ignoreExisting: true});
+ }).then(() => callback(dirPath, null),
+ e => callback(dirPath, e));
+ },
+
+ /**
+ * toJSON is called by JSON.stringify in order to create a filtered version
+ * of this object to be serialized to a JSON file. A new object is returned
+ * with copies of all non-private properties. Functions, getters and setters
+ * are not copied.
+ *
+ * @param aKey
+ * The key that this object is being serialized as in the JSON.
+ * Unused here since this is always the main object serialized
+ *
+ * @return an object containing copies of the properties of this object
+ * ignoring private properties, functions, getters and setters
+ */
+ toJSON: function AddonInternal_toJSON(aKey) {
+ let obj = {};
+ for (let prop in this) {
+ // Ignore private properties
+ if (prop.substring(0, 1) == "_")
+ continue;
+
+ // Ignore getters
+ if (this.__lookupGetter__(prop))
+ continue;
+
+ // Ignore setters
+ if (this.__lookupSetter__(prop))
+ continue;
+
+ // Ignore functions
+ if (typeof this[prop] == "function")
+ continue;
+
+ obj[prop] = this[prop];
+ }
+
+ return obj;
+ },
+
+ /**
+ * When an add-on install is pending its metadata will be cached in a file.
+ * This method reads particular properties of that metadata that may be newer
+ * than that in the install manifest, like compatibility information.
+ *
+ * @param aObj
+ * A JS object containing the cached metadata
+ */
+ importMetadata: function AddonInternal_importMetaData(aObj) {
+ PENDING_INSTALL_METADATA.forEach(function(aProp) {
+ if (!(aProp in aObj))
+ return;
+
+ this[aProp] = aObj[aProp];
+ }, this);
+
+ // Compatibility info may have changed so update appDisabled
+ this.appDisabled = !isUsableAddon(this);
+ },
+
+ permissions: function AddonInternal_permissions() {
+ let permissions = 0;
+
+ // Add-ons that aren't installed cannot be modified in any way
+ if (!(this.inDatabase))
+ return permissions;
+
+ // Experiments can only be uninstalled. An uninstall reflects the user
+ // intent of "disable this experiment." This is partially managed by the
+ // experiments manager.
+ if (this.type == "experiment") {
+ return AddonManager.PERM_CAN_UNINSTALL;
+ }
+
+ if (!this.appDisabled) {
+ if (this.userDisabled || this.softDisabled) {
+ permissions |= AddonManager.PERM_CAN_ENABLE;
+ }
+ else if (this.type != "theme") {
+ permissions |= AddonManager.PERM_CAN_DISABLE;
+ }
+ }
+
+ // Add-ons that are in locked install locations, or are pending uninstall
+ // cannot be upgraded or uninstalled
+ if (!this._installLocation.locked && !this.pendingUninstall) {
+ // Add-ons that are installed by a file link cannot be upgraded
+ if (!this._installLocation.isLinkedAddon(this.id)) {
+ permissions |= AddonManager.PERM_CAN_UPGRADE;
+ }
+
+ permissions |= AddonManager.PERM_CAN_UNINSTALL;
+ }
+
+ return permissions;
+ },
+};
+
+/**
+ * Creates an AddonWrapper for an AddonInternal.
+ *
+ * @param addon
+ * The AddonInternal to wrap
+ * @return an AddonWrapper or null if addon was null
+ */
+function createWrapper(aAddon) {
+ if (!aAddon)
+ return null;
+ if (!aAddon._wrapper) {
+ aAddon._hasResourceCache = new Map();
+ aAddon._wrapper = new AddonWrapper(aAddon);
+ }
+ return aAddon._wrapper;
+}
+
+/**
+ * The AddonWrapper wraps an Addon to provide the data visible to consumers of
+ * the public API.
+ */
+function AddonWrapper(aAddon) {
+#ifdef MOZ_EM_DEBUG
+ this.__defineGetter__("__AddonInternal__", function AW_debugGetter() {
+ return aAddon;
+ });
+#endif
+
+ function chooseValue(aObj, aProp) {
+ let repositoryAddon = aAddon._repositoryAddon;
+ let objValue = aObj[aProp];
+
+ if (repositoryAddon && (aProp in repositoryAddon) &&
+ (objValue === undefined || objValue === null)) {
+ return [repositoryAddon[aProp], true];
+ }
+
+ return [objValue, false];
+ }
+
+ ["id", "syncGUID", "version", "type", "isCompatible", "isPlatformCompatible",
+ "providesUpdatesSecurely", "blocklistState", "blocklistURL", "appDisabled",
+ "softDisabled", "skinnable", "size", "foreignInstall", "hasBinaryComponents",
+ "strictCompatibility", "compatibilityOverrides", "updateURL",
+ "getDataDirectory", "multiprocessCompatible", "jetsdk", "native"].forEach(function(aProp) {
+ this.__defineGetter__(aProp, function AddonWrapper_propertyGetter() aAddon[aProp]);
+ }, this);
+
+ ["fullDescription", "developerComments", "eula", "supportURL",
+ "contributionURL", "contributionAmount", "averageRating", "reviewCount",
+ "reviewURL", "totalDownloads", "weeklyDownloads", "dailyUsers",
+ "repositoryStatus"].forEach(function(aProp) {
+ this.__defineGetter__(aProp, function AddonWrapper_repoPropertyGetter() {
+ if (aAddon._repositoryAddon)
+ return aAddon._repositoryAddon[aProp];
+
+ return null;
+ });
+ }, this);
+
+ this.__defineGetter__("aboutURL", function AddonWrapper_aboutURLGetter() {
+ return this.isActive ? aAddon["aboutURL"] : null;
+ });
+
+ ["installDate", "updateDate"].forEach(function(aProp) {
+ this.__defineGetter__(aProp, function AddonWrapper_datePropertyGetter() new Date(aAddon[aProp]));
+ }, this);
+
+ ["sourceURI", "releaseNotesURI"].forEach(function(aProp) {
+ this.__defineGetter__(aProp, function AddonWrapper_URIPropertyGetter() {
+ let [target, fromRepo] = chooseValue(aAddon, aProp);
+ if (!target)
+ return null;
+ if (fromRepo)
+ return target;
+ return NetUtil.newURI(target);
+ });
+ }, this);
+
+ this.__defineGetter__("optionsURL", function AddonWrapper_optionsURLGetter() {
+ if (this.isActive && aAddon.optionsURL)
+ return aAddon.optionsURL;
+
+ if (this.isActive && this.hasResource("options.xul"))
+ return this.getResourceURI("options.xul").spec;
+
+ return null;
+ }, this);
+
+ this.__defineGetter__("optionsType", function AddonWrapper_optionsTypeGetter() {
+ if (!this.isActive)
+ return null;
+
+ let hasOptionsXUL = this.hasResource("options.xul");
+ let hasOptionsURL = !!this.optionsURL;
+
+ if (aAddon.optionsType) {
+ switch (parseInt(aAddon.optionsType, 10)) {
+ case AddonManager.OPTIONS_TYPE_DIALOG:
+ case AddonManager.OPTIONS_TYPE_TAB:
+ return hasOptionsURL ? aAddon.optionsType : null;
+ case AddonManager.OPTIONS_TYPE_INLINE:
+ case AddonManager.OPTIONS_TYPE_INLINE_INFO:
+ return (hasOptionsXUL || hasOptionsURL) ? aAddon.optionsType : null;
+ }
+ return null;
+ }
+
+ if (hasOptionsXUL)
+ return AddonManager.OPTIONS_TYPE_INLINE;
+
+ if (hasOptionsURL)
+ return AddonManager.OPTIONS_TYPE_DIALOG;
+
+ return null;
+ }, this);
+
+ this.__defineGetter__("iconURL", function AddonWrapper_iconURLGetter() {
+ return this.icons[32] || undefined;
+ }, this);
+
+ this.__defineGetter__("icon64URL", function AddonWrapper_icon64URLGetter() {
+ return this.icons[64] || undefined;
+ }, this);
+
+ this.__defineGetter__("icons", function AddonWrapper_iconsGetter() {
+ let icons = {};
+ if (aAddon._repositoryAddon) {
+ for (let size in aAddon._repositoryAddon.icons) {
+ icons[size] = aAddon._repositoryAddon.icons[size];
+ }
+ }
+ if (this.isActive && aAddon.iconURL) {
+ icons[32] = aAddon.iconURL;
+ } else if (this.hasResource("icon.png")) {
+ icons[32] = this.getResourceURI("icon.png").spec;
+ }
+ if (this.isActive && aAddon.icon64URL) {
+ icons[64] = aAddon.icon64URL;
+ } else if (this.hasResource("icon64.png")) {
+ icons[64] = this.getResourceURI("icon64.png").spec;
+ }
+ Object.freeze(icons);
+ return icons;
+ }, this);
+
+ PROP_LOCALE_SINGLE.forEach(function(aProp) {
+ this.__defineGetter__(aProp, function AddonWrapper_singleLocaleGetter() {
+ // Override XPI creator if repository creator is defined
+ if (aProp == "creator" &&
+ aAddon._repositoryAddon && aAddon._repositoryAddon.creator) {
+ return aAddon._repositoryAddon.creator;
+ }
+
+ let result = null;
+
+ if (aAddon.active) {
+ try {
+ let pref = PREF_EM_EXTENSION_FORMAT + aAddon.id + "." + aProp;
+ let value = Preferences.get(pref, null, Ci.nsIPrefLocalizedString);
+ if (value)
+ result = value;
+ }
+ catch (e) {
+ }
+ }
+
+ if (result == null) {
+ if (typeof aAddon.selectedLocale[aProp] == "string" && aAddon.selectedLocale[aProp].length)
+ [result, ] = chooseValue(aAddon.selectedLocale, aProp);
+ else
+ [result, ] = chooseValue(aAddon.defaultLocale, aProp);
+ }
+
+ if (aProp == "creator")
+ return result ? new AddonManagerPrivate.AddonAuthor(result) : null;
+
+ return result;
+ });
+ }, this);
+
+ PROP_LOCALE_MULTI.forEach(function(aProp) {
+ this.__defineGetter__(aProp, function AddonWrapper_multiLocaleGetter() {
+ let results = null;
+ let usedRepository = false;
+
+ if (aAddon.active) {
+ let pref = PREF_EM_EXTENSION_FORMAT + aAddon.id + "." +
+ aProp.substring(0, aProp.length - 1);
+ let list = Services.prefs.getChildList(pref, {});
+ if (list.length > 0) {
+ list.sort();
+ results = [];
+ list.forEach(function(aPref) {
+ let value = Preferences.get(aPref, null, Ci.nsIPrefLocalizedString);
+ if (value)
+ results.push(value);
+ });
+ }
+ }
+
+ if (results == null) {
+ if (aAddon.selectedLocale[aProp] instanceof Array && aAddon.selectedLocale[aProp].length)
+ [results, usedRepository] = chooseValue(aAddon.selectedLocale, aProp);
+ else
+ [results, usedRepository] = chooseValue(aAddon.defaultLocale, aProp);
+ }
+
+ if (results && !usedRepository) {
+ results = results.map(function mapResult(aResult) {
+ return new AddonManagerPrivate.AddonAuthor(aResult);
+ });
+ }
+
+ return results;
+ });
+ }, this);
+
+ this.__defineGetter__("screenshots", function AddonWrapper_screenshotsGetter() {
+ let repositoryAddon = aAddon._repositoryAddon;
+ if (repositoryAddon && ("screenshots" in repositoryAddon)) {
+ let repositoryScreenshots = repositoryAddon.screenshots;
+ if (repositoryScreenshots && repositoryScreenshots.length > 0)
+ return repositoryScreenshots;
+ }
+
+ if (aAddon.type == "theme" && this.hasResource("preview.png")) {
+ let url = this.getResourceURI("preview.png").spec;
+ return [new AddonManagerPrivate.AddonScreenshot(url)];
+ }
+
+ return null;
+ });
+
+ this.__defineGetter__("applyBackgroundUpdates", function AddonWrapper_applyBackgroundUpdatesGetter() {
+ return aAddon.applyBackgroundUpdates;
+ });
+ this.__defineSetter__("applyBackgroundUpdates", function AddonWrapper_applyBackgroundUpdatesSetter(val) {
+ if (this.type == "experiment") {
+ logger.warn("Setting applyBackgroundUpdates on an experiment is not supported.");
+ return;
+ }
+
+ if (val != AddonManager.AUTOUPDATE_DEFAULT &&
+ val != AddonManager.AUTOUPDATE_DISABLE &&
+ val != AddonManager.AUTOUPDATE_ENABLE) {
+ val = val ? AddonManager.AUTOUPDATE_DEFAULT :
+ AddonManager.AUTOUPDATE_DISABLE;
+ }
+
+ if (val == aAddon.applyBackgroundUpdates)
+ return val;
+
+ XPIDatabase.setAddonProperties(aAddon, {
+ applyBackgroundUpdates: val
+ });
+ AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, ["applyBackgroundUpdates"]);
+
+ return val;
+ });
+
+ this.__defineSetter__("syncGUID", function AddonWrapper_syncGUIDGetter(val) {
+ if (aAddon.syncGUID == val)
+ return val;
+
+ if (aAddon.inDatabase)
+ XPIDatabase.setAddonSyncGUID(aAddon, val);
+
+ aAddon.syncGUID = val;
+
+ return val;
+ });
+
+ this.__defineGetter__("install", function AddonWrapper_installGetter() {
+ if (!("_install" in aAddon) || !aAddon._install)
+ return null;
+ return aAddon._install.wrapper;
+ });
+
+ this.__defineGetter__("pendingUpgrade", function AddonWrapper_pendingUpgradeGetter() {
+ return createWrapper(aAddon.pendingUpgrade);
+ });
+
+ this.__defineGetter__("scope", function AddonWrapper_scopeGetter() {
+ if (aAddon._installLocation)
+ return aAddon._installLocation.scope;
+
+ return AddonManager.SCOPE_PROFILE;
+ });
+
+ this.__defineGetter__("pendingOperations", function AddonWrapper_pendingOperationsGetter() {
+ let pending = 0;
+ if (!(aAddon.inDatabase)) {
+ // Add-on is pending install if there is no associated install (shouldn't
+ // happen here) or if the install is in the process of or has successfully
+ // completed the install. If an add-on is pending install then we ignore
+ // any other pending operations.
+ if (!aAddon._install || aAddon._install.state == AddonManager.STATE_INSTALLING ||
+ aAddon._install.state == AddonManager.STATE_INSTALLED)
+ return AddonManager.PENDING_INSTALL;
+ }
+ else if (aAddon.pendingUninstall) {
+ // If an add-on is pending uninstall then we ignore any other pending
+ // operations
+ return AddonManager.PENDING_UNINSTALL;
+ }
+
+ // Extensions have an intentional inconsistency between what the DB says is
+ // enabled and what we say to the ouside world. so we need to cover up that
+ // lie here as well.
+ if (aAddon.type != "experiment") {
+ if (aAddon.active && aAddon.disabled)
+ pending |= AddonManager.PENDING_DISABLE;
+ else if (!aAddon.active && !aAddon.disabled)
+ pending |= AddonManager.PENDING_ENABLE;
+ }
+
+ if (aAddon.pendingUpgrade)
+ pending |= AddonManager.PENDING_UPGRADE;
+
+ return pending;
+ });
+
+ this.__defineGetter__("operationsRequiringRestart", function AddonWrapper_operationsRequiringRestartGetter() {
+ let ops = 0;
+ if (XPIProvider.installRequiresRestart(aAddon))
+ ops |= AddonManager.OP_NEEDS_RESTART_INSTALL;
+ if (XPIProvider.uninstallRequiresRestart(aAddon))
+ ops |= AddonManager.OP_NEEDS_RESTART_UNINSTALL;
+ if (XPIProvider.enableRequiresRestart(aAddon))
+ ops |= AddonManager.OP_NEEDS_RESTART_ENABLE;
+ if (XPIProvider.disableRequiresRestart(aAddon))
+ ops |= AddonManager.OP_NEEDS_RESTART_DISABLE;
+
+ return ops;
+ });
+
+ this.__defineGetter__("isDebuggable", function AddonWrapper_isDebuggable() {
+ return this.isActive && aAddon.bootstrap;
+ });
+
+ this.__defineGetter__("permissions", function AddonWrapper_permisionsGetter() {
+ return aAddon.permissions();
+ });
+
+ this.__defineGetter__("isActive", function AddonWrapper_isActiveGetter() {
+ if (Services.appinfo.inSafeMode)
+ return false;
+ return aAddon.active;
+ });
+
+ this.__defineGetter__("userDisabled", function AddonWrapper_userDisabledGetter() {
+ if (XPIProvider._enabledExperiments.has(aAddon.id)) {
+ return false;
+ }
+
+ return aAddon.softDisabled || aAddon.userDisabled;
+ });
+ this.__defineSetter__("userDisabled", function AddonWrapper_userDisabledSetter(val) {
+ if (val == this.userDisabled) {
+ return val;
+ }
+
+ if (aAddon.type == "experiment") {
+ if (val) {
+ XPIProvider._enabledExperiments.delete(aAddon.id);
+ } else {
+ XPIProvider._enabledExperiments.add(aAddon.id);
+ }
+ }
+
+ if (aAddon.inDatabase) {
+ if (aAddon.type == "theme" && val) {
+ if (aAddon.internalName == XPIProvider.defaultSkin)
+ throw new Error("Cannot disable the default theme");
+ XPIProvider.enableDefaultTheme();
+ }
+ else {
+ XPIProvider.updateAddonDisabledState(aAddon, val);
+ }
+ }
+ else {
+ aAddon.userDisabled = val;
+ // When enabling remove the softDisabled flag
+ if (!val)
+ aAddon.softDisabled = false;
+ }
+
+ return val;
+ });
+
+ this.__defineSetter__("softDisabled", function AddonWrapper_softDisabledSetter(val) {
+ if (val == aAddon.softDisabled)
+ return val;
+
+ if (aAddon.inDatabase) {
+ // When softDisabling a theme just enable the active theme
+ if (aAddon.type == "theme" && val && !aAddon.userDisabled) {
+ if (aAddon.internalName == XPIProvider.defaultSkin)
+ throw new Error("Cannot disable the default theme");
+ XPIProvider.enableDefaultTheme();
+ }
+ else {
+ XPIProvider.updateAddonDisabledState(aAddon, undefined, val);
+ }
+ }
+ else {
+ // Only set softDisabled if not already disabled
+ if (!aAddon.userDisabled)
+ aAddon.softDisabled = val;
+ }
+
+ return val;
+ });
+
+ this.isCompatibleWith = function AddonWrapper_isCompatiblewith(aAppVersion, aPlatformVersion) {
+ return aAddon.isCompatibleWith(aAppVersion, aPlatformVersion);
+ };
+
+ this.uninstall = function AddonWrapper_uninstall(alwaysAllowUndo) {
+ XPIProvider.uninstallAddon(aAddon, alwaysAllowUndo);
+ };
+
+ this.cancelUninstall = function AddonWrapper_cancelUninstall() {
+ XPIProvider.cancelUninstallAddon(aAddon);
+ };
+
+ this.findUpdates = function AddonWrapper_findUpdates(aListener, aReason, aAppVersion, aPlatformVersion) {
+ // Short-circuit updates for experiments because updates are handled
+ // through the Experiments Manager.
+ if (this.type == "experiment") {
+ AddonManagerPrivate.callNoUpdateListeners(this, aListener, aReason,
+ aAppVersion, aPlatformVersion);
+ return;
+ }
+
+ new UpdateChecker(aAddon, aListener, aReason, aAppVersion, aPlatformVersion);
+ };
+
+ // Returns true if there was an update in progress, false if there was no update to cancel
+ this.cancelUpdate = function AddonWrapper_cancelUpdate() {
+ if (aAddon._updateCheck) {
+ aAddon._updateCheck.cancel();
+ return true;
+ }
+ return false;
+ };
+
+ this.hasResource = function AddonWrapper_hasResource(aPath) {
+ if (aAddon._hasResourceCache.has(aPath))
+ return aAddon._hasResourceCache.get(aPath);
+
+ let bundle = aAddon._sourceBundle.clone();
+
+ // Bundle may not exist any more if the addon has just been uninstalled,
+ // but explicitly first checking .exists() results in unneeded file I/O.
+ try {
+ var isDir = bundle.isDirectory();
+ } catch (e) {
+ aAddon._hasResourceCache.set(aPath, false);
+ return false;
+ }
+
+ if (isDir) {
+ if (aPath) {
+ aPath.split("/").forEach(function(aPart) {
+ bundle.append(aPart);
+ });
+ }
+ let result = bundle.exists();
+ aAddon._hasResourceCache.set(aPath, result);
+ return result;
+ }
+
+ let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].
+ createInstance(Ci.nsIZipReader);
+ try {
+ zipReader.open(bundle);
+ let result = zipReader.hasEntry(aPath);
+ aAddon._hasResourceCache.set(aPath, result);
+ return result;
+ }
+ catch (e) {
+ aAddon._hasResourceCache.set(aPath, false);
+ return false;
+ }
+ finally {
+ zipReader.close();
+ }
+ },
+
+ /**
+ * Returns a URI to the selected resource or to the add-on bundle if aPath
+ * is null. URIs to the bundle will always be file: URIs. URIs to resources
+ * will be file: URIs if the add-on is unpacked or jar: URIs if the add-on is
+ * still an XPI file.
+ *
+ * @param aPath
+ * The path in the add-on to get the URI for or null to get a URI to
+ * the file or directory the add-on is installed as.
+ * @return an nsIURI
+ */
+ this.getResourceURI = function AddonWrapper_getResourceURI(aPath) {
+ if (!aPath)
+ return NetUtil.newURI(aAddon._sourceBundle);
+
+ return getURIForResourceInFile(aAddon._sourceBundle, aPath);
+ }
+}
+
+/**
+ * An object which identifies a directory install location for add-ons. The
+ * location consists of a directory which contains the add-ons installed in the
+ * location.
+ *
+ * Each add-on installed in the location is either a directory containing the
+ * add-on's files or a text file containing an absolute path to the directory
+ * containing the add-ons files. The directory or text file must have the same
+ * name as the add-on's ID.
+ *
+ * There may also a special directory named "staged" which can contain
+ * directories with the same name as an add-on ID. If the directory is empty
+ * then it means the add-on will be uninstalled from this location during the
+ * next startup. If the directory contains the add-on's files then they will be
+ * installed during the next startup.
+ *
+ * @param aName
+ * The string identifier for the install location
+ * @param aDirectory
+ * The nsIFile directory for the install location
+ * @param aScope
+ * The scope of add-ons installed in this location
+ * @param aLocked
+ * true if add-ons cannot be installed, uninstalled or upgraded in the
+ * install location
+ */
+function DirectoryInstallLocation(aName, aDirectory, aScope, aLocked) {
+ this._name = aName;
+ this.locked = aLocked;
+ this._directory = aDirectory;
+ this._scope = aScope
+ this._IDToFileMap = {};
+ this._FileToIDMap = {};
+ this._linkedAddons = [];
+ this._stagingDirLock = 0;
+
+ if (!aDirectory.exists())
+ return;
+ if (!aDirectory.isDirectory())
+ throw new Error("Location must be a directory.");
+
+ this._readAddons();
+}
+
+DirectoryInstallLocation.prototype = {
+ _name : "",
+ _directory : null,
+ _IDToFileMap : null, // mapping from add-on ID to nsIFile
+ _FileToIDMap : null, // mapping from add-on path to add-on ID
+
+ /**
+ * Reads a directory linked to in a file.
+ *
+ * @param file
+ * The file containing the directory path
+ * @return An nsIFile object representing the linked directory.
+ */
+ _readDirectoryFromFile: function DirInstallLocation__readDirectoryFromFile(aFile) {
+ let fis = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+ fis.init(aFile, -1, -1, false);
+ let line = { value: "" };
+ if (fis instanceof Ci.nsILineInputStream)
+ fis.readLine(line);
+ fis.close();
+ if (line.value) {
+ let linkedDirectory = Cc["@mozilla.org/file/local;1"].
+ createInstance(Ci.nsIFile);
+
+ try {
+ linkedDirectory.initWithPath(line.value);
+ }
+ catch (e) {
+ linkedDirectory.setRelativeDescriptor(aFile.parent, line.value);
+ }
+
+ if (!linkedDirectory.exists()) {
+ logger.warn("File pointer " + aFile.path + " points to " + linkedDirectory.path +
+ " which does not exist");
+ return null;
+ }
+
+ if (!linkedDirectory.isDirectory()) {
+ logger.warn("File pointer " + aFile.path + " points to " + linkedDirectory.path +
+ " which is not a directory");
+ return null;
+ }
+
+ return linkedDirectory;
+ }
+
+ logger.warn("File pointer " + aFile.path + " does not contain a path");
+ return null;
+ },
+
+ /**
+ * Finds all the add-ons installed in this location.
+ */
+ _readAddons: function DirInstallLocation__readAddons() {
+ // Use a snapshot of the directory contents to avoid possible issues with
+ // iterating over a directory while removing files from it (the YAFFS2
+ // embedded filesystem has this issue, see bug 772238).
+ let entries = getDirectoryEntries(this._directory);
+ for (let entry of entries) {
+ let id = entry.leafName;
+
+ if (id == DIR_STAGE || id == DIR_XPI_STAGE || id == DIR_TRASH)
+ continue;
+
+ let directLoad = false;
+ if (entry.isFile() &&
+ id.substring(id.length - 4).toLowerCase() == ".xpi") {
+ directLoad = true;
+ id = id.substring(0, id.length - 4);
+ }
+
+ if (!gIDTest.test(id)) {
+ logger.debug("Ignoring file entry whose name is not a valid add-on ID: " +
+ entry.path);
+ continue;
+ }
+
+ if (entry.isFile() && !directLoad) {
+ let newEntry = this._readDirectoryFromFile(entry);
+ if (!newEntry) {
+ logger.debug("Deleting stale pointer file " + entry.path);
+ try {
+ entry.remove(true);
+ }
+ catch (e) {
+ logger.warn("Failed to remove stale pointer file " + entry.path, e);
+ // Failing to remove the stale pointer file is ignorable
+ }
+ continue;
+ }
+
+ entry = newEntry;
+ this._linkedAddons.push(id);
+ }
+
+ this._IDToFileMap[id] = entry;
+ this._FileToIDMap[entry.path] = id;
+ XPIProvider._addURIMapping(id, entry);
+ }
+ },
+
+ /**
+ * Gets the name of this install location.
+ */
+ get name() {
+ return this._name;
+ },
+
+ /**
+ * Gets the scope of this install location.
+ */
+ get scope() {
+ return this._scope;
+ },
+
+ /**
+ * Gets an array of nsIFiles for add-ons installed in this location.
+ */
+ get addonLocations() {
+ let locations = [];
+ for (let id in this._IDToFileMap) {
+ locations.push(this._IDToFileMap[id].clone());
+ }
+ return locations;
+ },
+
+ /**
+ * Gets the staging directory to put add-ons that are pending install and
+ * uninstall into.
+ *
+ * @return an nsIFile
+ */
+ getStagingDir: function DirInstallLocation_getStagingDir() {
+ let dir = this._directory.clone();
+ dir.append(DIR_STAGE);
+ return dir;
+ },
+
+ requestStagingDir: function() {
+ this._stagingDirLock++;
+
+ if (this._stagingDirPromise)
+ return this._stagingDirPromise;
+
+ OS.File.makeDir(this._directory.path);
+ let stagepath = OS.Path.join(this._directory.path, DIR_STAGE);
+ return this._stagingDirPromise = OS.File.makeDir(stagepath).then(null, (e) => {
+ if (e instanceof OS.File.Error && e.becauseExists)
+ return;
+ logger.error("Failed to create staging directory", e);
+ throw e;
+ });
+ },
+
+ releaseStagingDir: function() {
+ this._stagingDirLock--;
+
+ if (this._stagingDirLock == 0) {
+ this._stagingDirPromise = null;
+ this.cleanStagingDir();
+ }
+
+ return Promise.resolve();
+ },
+
+ /**
+ * Removes the specified files or directories in the staging directory and
+ * then if the staging directory is empty attempts to remove it.
+ *
+ * @param aLeafNames
+ * An array of file or directory to remove from the directory, the
+ * array may be empty
+ */
+ cleanStagingDir: function(aLeafNames = []) {
+ let dir = this.getStagingDir();
+
+ for (let name of aLeafNames) {
+ let file = dir.clone();
+ file.append(name);
+ recursiveRemove(file);
+ }
+
+ if (this._stagingDirLock > 0)
+ return;
+
+ let dirEntries = dir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
+ try {
+ if (dirEntries.nextFile)
+ return;
+ }
+ finally {
+ dirEntries.close();
+ }
+
+ try {
+ setFilePermissions(dir, FileUtils.PERMS_DIRECTORY);
+ dir.remove(false);
+ }
+ catch (e) {
+ logger.warn("Failed to remove staging dir", e);
+ // Failing to remove the staging directory is ignorable
+ }
+ },
+
+ /**
+ * Gets the directory used by old versions for staging XPI and JAR files ready
+ * to be installed.
+ *
+ * @return an nsIFile
+ */
+ getXPIStagingDir: function DirInstallLocation_getXPIStagingDir() {
+ let dir = this._directory.clone();
+ dir.append(DIR_XPI_STAGE);
+ return dir;
+ },
+
+ /**
+ * Returns a directory that is normally on the same filesystem as the rest of
+ * the install location and can be used for temporarily storing files during
+ * safe move operations. Calling this method will delete the existing trash
+ * directory and its contents.
+ *
+ * @return an nsIFile
+ */
+ getTrashDir: function DirInstallLocation_getTrashDir() {
+ let trashDir = this._directory.clone();
+ trashDir.append(DIR_TRASH);
+ let trashDirExists = trashDir.exists();
+ try {
+ if (trashDirExists)
+ recursiveRemove(trashDir);
+ trashDirExists = false;
+ } catch (e) {
+ logger.warn("Failed to remove trash directory", e);
+ }
+ if (!trashDirExists)
+ trashDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ return trashDir;
+ },
+
+ /**
+ * Installs an add-on into the install location.
+ *
+ * @param aId
+ * The ID of the add-on to install
+ * @param aSource
+ * The source nsIFile to install from
+ * @param aExistingAddonID
+ * The ID of an existing add-on to uninstall at the same time
+ * @param aCopy
+ * If false the source files will be moved to the new location,
+ * otherwise they will only be copied
+ * @return an nsIFile indicating where the add-on was installed to
+ */
+ installAddon: function DirInstallLocation_installAddon(aId, aSource,
+ aExistingAddonID,
+ aCopy) {
+ let trashDir = this.getTrashDir();
+
+ let transaction = new SafeInstallOperation();
+
+ let self = this;
+ function moveOldAddon(aId) {
+ let file = self._directory.clone();
+ file.append(aId);
+
+ if (file.exists())
+ transaction.moveUnder(file, trashDir);
+
+ file = self._directory.clone();
+ file.append(aId + ".xpi");
+ if (file.exists()) {
+ flushJarCache(file);
+ transaction.moveUnder(file, trashDir);
+ }
+ }
+
+ // If any of these operations fails the finally block will clean up the
+ // temporary directory
+ try {
+ moveOldAddon(aId);
+ if (aExistingAddonID && aExistingAddonID != aId) {
+ moveOldAddon(aExistingAddonID);
+
+ {
+ // Move the data directories.
+ /* XXX ajvincent We can't use OS.File: installAddon isn't compatible
+ * with Promises, nor is SafeInstallOperation. Bug 945540 has been filed
+ * for porting to OS.File.
+ */
+ let oldDataDir = FileUtils.getDir(
+ KEY_PROFILEDIR, ["extension-data", aExistingAddonID], false, true
+ );
+
+ if (oldDataDir.exists()) {
+ let newDataDir = FileUtils.getDir(
+ KEY_PROFILEDIR, ["extension-data", aId], false, true
+ );
+ if (newDataDir.exists()) {
+ let trashData = trashDir.clone();
+ trashData.append("data-directory");
+ transaction.moveUnder(newDataDir, trashData);
+ }
+
+ transaction.moveTo(oldDataDir, newDataDir);
+ }
+ }
+ }
+
+ if (aCopy) {
+ transaction.copy(aSource, this._directory);
+ }
+ else {
+ if (aSource.isFile())
+ flushJarCache(aSource);
+
+ transaction.moveUnder(aSource, this._directory);
+ }
+ }
+ finally {
+ // It isn't ideal if this cleanup fails but it isn't worth rolling back
+ // the install because of it.
+ try {
+ recursiveRemove(trashDir);
+ }
+ catch (e) {
+ logger.warn("Failed to remove trash directory when installing " + aId, e);
+ }
+ }
+
+ let newFile = this._directory.clone();
+ newFile.append(aSource.leafName);
+ try {
+ newFile.lastModifiedTime = Date.now();
+ } catch (e) {
+ logger.warn("failed to set lastModifiedTime on " + newFile.path, e);
+ }
+ this._FileToIDMap[newFile.path] = aId;
+ this._IDToFileMap[aId] = newFile;
+ XPIProvider._addURIMapping(aId, newFile);
+
+ if (aExistingAddonID && aExistingAddonID != aId &&
+ aExistingAddonID in this._IDToFileMap) {
+ delete this._FileToIDMap[this._IDToFileMap[aExistingAddonID]];
+ delete this._IDToFileMap[aExistingAddonID];
+ }
+
+ return newFile;
+ },
+
+ /**
+ * Uninstalls an add-on from this location.
+ *
+ * @param aId
+ * The ID of the add-on to uninstall
+ * @throws if the ID does not match any of the add-ons installed
+ */
+ uninstallAddon: function DirInstallLocation_uninstallAddon(aId) {
+ let file = this._IDToFileMap[aId];
+ if (!file) {
+ logger.warn("Attempted to remove " + aId + " from " +
+ this._name + " but it was already gone");
+ return;
+ }
+
+ file = this._directory.clone();
+ file.append(aId);
+ if (!file.exists())
+ file.leafName += ".xpi";
+
+ if (!file.exists()) {
+ logger.warn("Attempted to remove " + aId + " from " +
+ this._name + " but it was already gone");
+
+ delete this._FileToIDMap[file.path];
+ delete this._IDToFileMap[aId];
+ return;
+ }
+
+ let trashDir = this.getTrashDir();
+
+ if (file.leafName != aId) {
+ logger.debug("uninstallAddon: flushing jar cache " + file.path + " for addon " + aId);
+ flushJarCache(file);
+ }
+
+ let transaction = new SafeInstallOperation();
+
+ try {
+ transaction.moveUnder(file, trashDir);
+ }
+ finally {
+ // It isn't ideal if this cleanup fails, but it is probably better than
+ // rolling back the uninstall at this point
+ try {
+ recursiveRemove(trashDir);
+ }
+ catch (e) {
+ logger.warn("Failed to remove trash directory when uninstalling " + aId, e);
+ }
+ }
+
+ delete this._FileToIDMap[file.path];
+ delete this._IDToFileMap[aId];
+ },
+
+ /**
+ * Gets the ID of the add-on installed in the given nsIFile.
+ *
+ * @param aFile
+ * The nsIFile to look in
+ * @return the ID
+ * @throws if the file does not represent an installed add-on
+ */
+ getIDForLocation: function DirInstallLocation_getIDForLocation(aFile) {
+ if (aFile.path in this._FileToIDMap)
+ return this._FileToIDMap[aFile.path];
+ throw new Error("Unknown add-on location " + aFile.path);
+ },
+
+ /**
+ * Gets the directory that the add-on with the given ID is installed in.
+ *
+ * @param aId
+ * The ID of the add-on
+ * @return The nsIFile
+ * @throws if the ID does not match any of the add-ons installed
+ */
+ getLocationForID: function DirInstallLocation_getLocationForID(aId) {
+ if (aId in this._IDToFileMap)
+ return this._IDToFileMap[aId].clone();
+ throw new Error("Unknown add-on ID " + aId);
+ },
+
+ /**
+ * Returns true if the given addon was installed in this location by a text
+ * file pointing to its real path.
+ *
+ * @param aId
+ * The ID of the addon
+ */
+ isLinkedAddon: function DirInstallLocation__isLinkedAddon(aId) {
+ return this._linkedAddons.indexOf(aId) != -1;
+ }
+};
+
+#ifdef XP_WIN
+/**
+ * An object that identifies a registry install location for add-ons. The location
+ * consists of a registry key which contains string values mapping ID to the
+ * path where an add-on is installed
+ *
+ * @param aName
+ * The string identifier of this Install Location.
+ * @param aRootKey
+ * The root key (one of the ROOT_KEY_ values from nsIWindowsRegKey).
+ * @param scope
+ * The scope of add-ons installed in this location
+ */
+function WinRegInstallLocation(aName, aRootKey, aScope) {
+ this.locked = true;
+ this._name = aName;
+ this._rootKey = aRootKey;
+ this._scope = aScope;
+ this._IDToFileMap = {};
+ this._FileToIDMap = {};
+
+ let path = this._appKeyPath + "\\Extensions";
+ let key = Cc["@mozilla.org/windows-registry-key;1"].
+ createInstance(Ci.nsIWindowsRegKey);
+
+ // Reading the registry may throw an exception, and that's ok. In error
+ // cases, we just leave ourselves in the empty state.
+ try {
+ key.open(this._rootKey, path, Ci.nsIWindowsRegKey.ACCESS_READ);
+ }
+ catch (e) {
+ return;
+ }
+
+ this._readAddons(key);
+ key.close();
+}
+
+WinRegInstallLocation.prototype = {
+ _name : "",
+ _rootKey : null,
+ _scope : null,
+ _IDToFileMap : null, // mapping from ID to nsIFile
+ _FileToIDMap : null, // mapping from path to ID
+
+ /**
+ * Retrieves the path of this Application's data key in the registry.
+ */
+ get _appKeyPath() {
+ let appVendor = Services.appinfo.vendor;
+ let appName = Services.appinfo.name;
+
+#ifdef MOZ_THUNDERBIRD
+ // XXX Thunderbird doesn't specify a vendor string
+ if (appVendor == "")
+ appVendor = "Mozilla";
+#endif
+
+ // XULRunner-based apps may intentionally not specify a vendor
+ if (appVendor != "")
+ appVendor += "\\";
+
+ return "SOFTWARE\\" + appVendor + appName;
+ },
+
+ /**
+ * Read the registry and build a mapping between ID and path for each
+ * installed add-on.
+ *
+ * @param key
+ * The key that contains the ID to path mapping
+ */
+ _readAddons: function RegInstallLocation__readAddons(aKey) {
+ let count = aKey.valueCount;
+ for (let i = 0; i < count; ++i) {
+ let id = aKey.getValueName(i);
+
+ let file = Cc["@mozilla.org/file/local;1"].
+ createInstance(Ci.nsIFile);
+ file.initWithPath(aKey.readStringValue(id));
+
+ if (!file.exists()) {
+ logger.warn("Ignoring missing add-on in " + file.path);
+ continue;
+ }
+
+ this._IDToFileMap[id] = file;
+ this._FileToIDMap[file.path] = id;
+ XPIProvider._addURIMapping(id, file);
+ }
+ },
+
+ /**
+ * Gets the name of this install location.
+ */
+ get name() {
+ return this._name;
+ },
+
+ /**
+ * Gets the scope of this install location.
+ */
+ get scope() {
+ return this._scope;
+ },
+
+ /**
+ * Gets an array of nsIFiles for add-ons installed in this location.
+ */
+ get addonLocations() {
+ let locations = [];
+ for (let id in this._IDToFileMap) {
+ locations.push(this._IDToFileMap[id].clone());
+ }
+ return locations;
+ },
+
+ /**
+ * Gets the ID of the add-on installed in the given nsIFile.
+ *
+ * @param aFile
+ * The nsIFile to look in
+ * @return the ID
+ * @throws if the file does not represent an installed add-on
+ */
+ getIDForLocation: function RegInstallLocation_getIDForLocation(aFile) {
+ if (aFile.path in this._FileToIDMap)
+ return this._FileToIDMap[aFile.path];
+ throw new Error("Unknown add-on location");
+ },
+
+ /**
+ * Gets the nsIFile that the add-on with the given ID is installed in.
+ *
+ * @param aId
+ * The ID of the add-on
+ * @return the nsIFile
+ */
+ getLocationForID: function RegInstallLocation_getLocationForID(aId) {
+ if (aId in this._IDToFileMap)
+ return this._IDToFileMap[aId].clone();
+ throw new Error("Unknown add-on ID");
+ },
+
+ /**
+ * @see DirectoryInstallLocation
+ */
+ isLinkedAddon: function RegInstallLocation_isLinkedAddon(aId) {
+ return true;
+ }
+};
+#endif
+
+let addonTypes = [
+ new AddonManagerPrivate.AddonType("extension", URI_EXTENSION_STRINGS,
+ STRING_TYPE_NAME,
+ AddonManager.VIEW_TYPE_LIST, 4000,
+ AddonManager.TYPE_SUPPORTS_UNDO_RESTARTLESS_UNINSTALL),
+ new AddonManagerPrivate.AddonType("theme", URI_EXTENSION_STRINGS,
+ STRING_TYPE_NAME,
+ AddonManager.VIEW_TYPE_LIST, 5000),
+ new AddonManagerPrivate.AddonType("dictionary", URI_EXTENSION_STRINGS,
+ STRING_TYPE_NAME,
+ AddonManager.VIEW_TYPE_LIST, 7000,
+ AddonManager.TYPE_UI_HIDE_EMPTY | AddonManager.TYPE_SUPPORTS_UNDO_RESTARTLESS_UNINSTALL),
+ new AddonManagerPrivate.AddonType("locale", URI_EXTENSION_STRINGS,
+ STRING_TYPE_NAME,
+ AddonManager.VIEW_TYPE_LIST, 8000,
+ AddonManager.TYPE_UI_HIDE_EMPTY | AddonManager.TYPE_SUPPORTS_UNDO_RESTARTLESS_UNINSTALL),
+];
+
+// We only register experiments support if the application supports them.
+// Ideally, we would install an observer to watch the pref. Installing
+// an observer for this pref is not necessary here and may be buggy with
+// regards to registering this XPIProvider twice.
+if (Preferences.get("experiments.supported", false)) {
+ addonTypes.push(
+ new AddonManagerPrivate.AddonType("experiment",
+ URI_EXTENSION_STRINGS,
+ STRING_TYPE_NAME,
+ AddonManager.VIEW_TYPE_LIST, 11000,
+ AddonManager.TYPE_UI_HIDE_EMPTY | AddonManager.TYPE_SUPPORTS_UNDO_RESTARTLESS_UNINSTALL));
+}
+
+AddonManagerPrivate.registerProvider(XPIProvider, addonTypes);
diff --git a/toolkit/mozapps/extensions/internal/XPIProviderUtils.js b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
new file mode 100644
index 000000000..d4798b726
--- /dev/null
+++ b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
@@ -0,0 +1,1481 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/AddonManager.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository",
+ "resource://gre/modules/addons/AddonRepository.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DeferredSave",
+ "resource://gre/modules/DeferredSave.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+
+Cu.import("resource://gre/modules/Log.jsm");
+const LOGGER_ID = "addons.xpi-utils";
+
+// Create a new logger for use by the Addons XPI Provider Utils
+// (Requires AddonManager.jsm)
+let logger = Log.repository.getLogger(LOGGER_ID);
+
+const KEY_PROFILEDIR = "ProfD";
+const FILE_DATABASE = "extensions.sqlite";
+const FILE_JSON_DB = "extensions.json";
+const FILE_OLD_DATABASE = "extensions.rdf";
+const FILE_XPI_ADDONS_LIST = "extensions.ini";
+
+// The value for this is in Makefile.in
+#expand const DB_SCHEMA = __MOZ_EXTENSIONS_DB_SCHEMA__;
+
+// The last version of DB_SCHEMA implemented in SQLITE
+const LAST_SQLITE_DB_SCHEMA = 14;
+const PREF_DB_SCHEMA = "extensions.databaseSchema";
+const PREF_PENDING_OPERATIONS = "extensions.pendingOperations";
+const PREF_EM_ENABLED_ADDONS = "extensions.enabledAddons";
+const PREF_EM_DSS_ENABLED = "extensions.dss.enabled";
+
+// Properties that only exist in the database
+const DB_METADATA = ["syncGUID",
+ "installDate",
+ "updateDate",
+ "size",
+ "sourceURI",
+ "releaseNotesURI",
+ "applyBackgroundUpdates"];
+const DB_BOOL_METADATA = ["visible", "active", "userDisabled", "appDisabled",
+ "pendingUninstall", "bootstrap", "skinnable",
+ "softDisabled", "isForeignInstall",
+ "hasBinaryComponents", "strictCompatibility"];
+
+// Properties to save in JSON file
+const PROP_JSON_FIELDS = ["id", "syncGUID", "location", "version", "type",
+ "internalName", "updateURL", "updateKey", "optionsURL",
+ "optionsType", "aboutURL", "iconURL", "icon64URL",
+ "defaultLocale", "visible", "active", "userDisabled",
+ "appDisabled", "pendingUninstall", "descriptor", "installDate",
+ "updateDate", "applyBackgroundUpdates", "bootstrap",
+ "skinnable", "size", "sourceURI", "releaseNotesURI",
+ "softDisabled", "foreignInstall", "hasBinaryComponents",
+ "strictCompatibility", "locales", "targetApplications",
+ "targetPlatforms", "multiprocessCompatible", "jetsdk", "native"];
+
+// Time to wait before async save of XPI JSON database, in milliseconds
+const ASYNC_SAVE_DELAY_MS = 20;
+
+const PREFIX_ITEM_URI = "urn:mozilla:item:";
+const RDFURI_ITEM_ROOT = "urn:mozilla:item:root"
+const PREFIX_NS_EM = "http://www.mozilla.org/2004/em-rdf#";
+
+XPCOMUtils.defineLazyServiceGetter(this, "gRDF", "@mozilla.org/rdf/rdf-service;1",
+ Ci.nsIRDFService);
+
+function EM_R(aProperty) {
+ return gRDF.GetResource(PREFIX_NS_EM + aProperty);
+}
+
+/**
+ * Converts an RDF literal, resource or integer into a string.
+ *
+ * @param aLiteral
+ * The RDF object to convert
+ * @return a string if the object could be converted or null
+ */
+function getRDFValue(aLiteral) {
+ if (aLiteral instanceof Ci.nsIRDFLiteral)
+ return aLiteral.Value;
+ if (aLiteral instanceof Ci.nsIRDFResource)
+ return aLiteral.Value;
+ if (aLiteral instanceof Ci.nsIRDFInt)
+ return aLiteral.Value;
+ return null;
+}
+
+/**
+ * Gets an RDF property as a string
+ *
+ * @param aDs
+ * The RDF datasource to read the property from
+ * @param aResource
+ * The RDF resource to read the property from
+ * @param aProperty
+ * The property to read
+ * @return a string if the property existed or null
+ */
+function getRDFProperty(aDs, aResource, aProperty) {
+ return getRDFValue(aDs.GetTarget(aResource, EM_R(aProperty), true));
+}
+
+/**
+ * Asynchronously fill in the _repositoryAddon field for one addon
+ */
+function getRepositoryAddon(aAddon, aCallback) {
+ if (!aAddon) {
+ aCallback(aAddon);
+ return;
+ }
+ function completeAddon(aRepositoryAddon) {
+ aAddon._repositoryAddon = aRepositoryAddon;
+ aAddon.compatibilityOverrides = aRepositoryAddon ?
+ aRepositoryAddon.compatibilityOverrides :
+ null;
+ aCallback(aAddon);
+ }
+ AddonRepository.getCachedAddonByID(aAddon.id, completeAddon);
+}
+
+/**
+ * Wrap an API-supplied function in an exception handler to make it safe to call
+ */
+function makeSafe(aCallback) {
+ return function(...aArgs) {
+ try {
+ aCallback(...aArgs);
+ }
+ catch(ex) {
+ logger.warn("XPI Database callback failed", ex);
+ }
+ }
+}
+
+/**
+ * A helper method to asynchronously call a function on an array
+ * of objects, calling a callback when function(x) has been gathered
+ * for every element of the array.
+ * WARNING: not currently error-safe; if the async function does not call
+ * our internal callback for any of the array elements, asyncMap will not
+ * call the callback parameter.
+ *
+ * @param aObjects
+ * The array of objects to process asynchronously
+ * @param aMethod
+ * Function with signature function(object, function aCallback(f_of_object))
+ * @param aCallback
+ * Function with signature f([aMethod(object)]), called when all values
+ * are available
+ */
+function asyncMap(aObjects, aMethod, aCallback) {
+ var resultsPending = aObjects.length;
+ var results = []
+ if (resultsPending == 0) {
+ aCallback(results);
+ return;
+ }
+
+ function asyncMap_gotValue(aIndex, aValue) {
+ results[aIndex] = aValue;
+ if (--resultsPending == 0) {
+ aCallback(results);
+ }
+ }
+
+ aObjects.map(function asyncMap_each(aObject, aIndex, aArray) {
+ try {
+ aMethod(aObject, function asyncMap_callback(aResult) {
+ asyncMap_gotValue(aIndex, aResult);
+ });
+ }
+ catch (e) {
+ logger.warn("Async map function failed", e);
+ asyncMap_gotValue(aIndex, undefined);
+ }
+ });
+}
+
+/**
+ * A generator to synchronously return result rows from an mozIStorageStatement.
+ *
+ * @param aStatement
+ * The statement to execute
+ */
+function resultRows(aStatement) {
+ try {
+ while (stepStatement(aStatement))
+ yield aStatement.row;
+ }
+ finally {
+ aStatement.reset();
+ }
+}
+
+/**
+ * A helper function to log an SQL error.
+ *
+ * @param aError
+ * The storage error code associated with the error
+ * @param aErrorString
+ * An error message
+ */
+function logSQLError(aError, aErrorString) {
+ logger.error("SQL error " + aError + ": " + aErrorString);
+}
+
+/**
+ * A helper function to log any errors that occur during async statements.
+ *
+ * @param aError
+ * A mozIStorageError to log
+ */
+function asyncErrorLogger(aError) {
+ logSQLError(aError.result, aError.message);
+}
+
+/**
+ * A helper function to step a statement synchronously and log any error that
+ * occurs.
+ *
+ * @param aStatement
+ * A mozIStorageStatement to execute
+ */
+function stepStatement(aStatement) {
+ try {
+ return aStatement.executeStep();
+ }
+ catch (e) {
+ logSQLError(XPIDatabase.connection.lastError,
+ XPIDatabase.connection.lastErrorString);
+ throw e;
+ }
+}
+
+/**
+ * Copies properties from one object to another. If no target object is passed
+ * a new object will be created and returned.
+ *
+ * @param aObject
+ * An object to copy from
+ * @param aProperties
+ * An array of properties to be copied
+ * @param aTarget
+ * An optional target object to copy the properties to
+ * @return the object that the properties were copied onto
+ */
+function copyProperties(aObject, aProperties, aTarget) {
+ if (!aTarget)
+ aTarget = {};
+ aProperties.forEach(function(aProp) {
+ aTarget[aProp] = aObject[aProp];
+ });
+ return aTarget;
+}
+
+/**
+ * Copies properties from a mozIStorageRow to an object. If no target object is
+ * passed a new object will be created and returned.
+ *
+ * @param aRow
+ * A mozIStorageRow to copy from
+ * @param aProperties
+ * An array of properties to be copied
+ * @param aTarget
+ * An optional target object to copy the properties to
+ * @return the object that the properties were copied onto
+ */
+function copyRowProperties(aRow, aProperties, aTarget) {
+ if (!aTarget)
+ aTarget = {};
+ aProperties.forEach(function(aProp) {
+ aTarget[aProp] = aRow.getResultByName(aProp);
+ });
+ return aTarget;
+}
+
+/**
+ * The DBAddonInternal is a special AddonInternal that has been retrieved from
+ * the database. The constructor will initialize the DBAddonInternal with a set
+ * of fields, which could come from either the JSON store or as an
+ * XPIProvider.AddonInternal created from an addon's manifest
+ * @constructor
+ * @param aLoaded
+ * Addon data fields loaded from JSON or the addon manifest.
+ */
+function DBAddonInternal(aLoaded) {
+ copyProperties(aLoaded, PROP_JSON_FIELDS, this);
+
+ if (aLoaded._installLocation) {
+ this._installLocation = aLoaded._installLocation;
+ this.location = aLoaded._installLocation._name;
+ }
+ else if (aLoaded.location) {
+ this._installLocation = XPIProvider.installLocationsByName[this.location];
+ }
+
+ this._key = this.location + ":" + this.id;
+
+ try {
+ this._sourceBundle = this._installLocation.getLocationForID(this.id);
+ }
+ catch (e) {
+ // An exception will be thrown if the add-on appears in the database but
+ // not on disk. In general this should only happen during startup as
+ // this change is being detected.
+ }
+
+ XPCOMUtils.defineLazyGetter(this, "pendingUpgrade",
+ function DBA_pendingUpgradeGetter() {
+ for (let install of XPIProvider.installs) {
+ if (install.state == AddonManager.STATE_INSTALLED &&
+ !(install.addon.inDatabase) &&
+ install.addon.id == this.id &&
+ install.installLocation == this._installLocation) {
+ delete this.pendingUpgrade;
+ return this.pendingUpgrade = install.addon;
+ }
+ };
+ return null;
+ });
+}
+
+function DBAddonInternalPrototype()
+{
+ this.applyCompatibilityUpdate =
+ function(aUpdate, aSyncCompatibility) {
+ this.targetApplications.forEach(function(aTargetApp) {
+ aUpdate.targetApplications.forEach(function(aUpdateTarget) {
+ if (aTargetApp.id == aUpdateTarget.id && (aSyncCompatibility ||
+ Services.vc.compare(aTargetApp.maxVersion, aUpdateTarget.maxVersion) < 0)) {
+ aTargetApp.minVersion = aUpdateTarget.minVersion;
+ aTargetApp.maxVersion = aUpdateTarget.maxVersion;
+ XPIDatabase.saveChanges();
+ }
+ });
+ });
+ if (aUpdate.multiprocessCompatible !== undefined &&
+ aUpdate.multiprocessCompatible != this.multiprocessCompatible) {
+ this.multiprocessCompatible = aUpdate.multiprocessCompatible;
+ XPIDatabase.saveChanges();
+ }
+ XPIProvider.updateAddonDisabledState(this);
+ };
+
+ this.toJSON =
+ function() {
+ return copyProperties(this, PROP_JSON_FIELDS);
+ };
+
+ Object.defineProperty(this, "inDatabase",
+ { get: function() { return true; },
+ enumerable: true,
+ configurable: true });
+}
+DBAddonInternalPrototype.prototype = AddonInternal.prototype;
+
+DBAddonInternal.prototype = new DBAddonInternalPrototype();
+
+/**
+ * Internal interface: find an addon from an already loaded addonDB
+ */
+function _findAddon(addonDB, aFilter) {
+ for (let addon of addonDB.values()) {
+ if (aFilter(addon)) {
+ return addon;
+ }
+ }
+ return null;
+}
+
+/**
+ * Internal interface to get a filtered list of addons from a loaded addonDB
+ */
+function _filterDB(addonDB, aFilter) {
+ return [for (addon of addonDB.values()) if (aFilter(addon)) addon];
+}
+
+this.XPIDatabase = {
+ // true if the database connection has been opened
+ initialized: false,
+ // The database file
+ jsonFile: FileUtils.getFile(KEY_PROFILEDIR, [FILE_JSON_DB], true),
+ // Migration data loaded from an old version of the database.
+ migrateData: null,
+ // Active add-on directories loaded from extensions.ini and prefs at startup.
+ activeBundles: null,
+
+ // Saved error object if we fail to read an existing database
+ _loadError: null,
+
+ // Error reported by our most recent attempt to read or write the database, if any
+ get lastError() {
+ if (this._loadError)
+ return this._loadError;
+ if (this._deferredSave)
+ return this._deferredSave.lastError;
+ return null;
+ },
+
+ /**
+ * Mark the current stored data dirty, and schedule a flush to disk
+ */
+ saveChanges: function() {
+ if (!this.initialized) {
+ throw new Error("Attempt to use XPI database when it is not initialized");
+ }
+
+ if (XPIProvider._closing) {
+ // use an Error here so we get a stack trace.
+ let err = new Error("XPI database modified after shutdown began");
+ logger.warn(err);
+ AddonManagerPrivate.recordSimpleMeasure("XPIDB_late_stack", Log.stackTrace(err));
+ }
+
+ if (!this._deferredSave) {
+ this._deferredSave = new DeferredSave(this.jsonFile.path,
+ () => JSON.stringify(this),
+ ASYNC_SAVE_DELAY_MS);
+ }
+
+ let promise = this._deferredSave.saveChanges();
+ if (!this._schemaVersionSet) {
+ this._schemaVersionSet = true;
+ promise.then(
+ count => {
+ // Update the XPIDB schema version preference the first time we successfully
+ // save the database.
+ logger.debug("XPI Database saved, setting schema version preference to " + DB_SCHEMA);
+ Services.prefs.setIntPref(PREF_DB_SCHEMA, DB_SCHEMA);
+ // Reading the DB worked once, so we don't need the load error
+ this._loadError = null;
+ },
+ error => {
+ // Need to try setting the schema version again later
+ this._schemaVersionSet = false;
+ logger.warn("Failed to save XPI database", error);
+ // this._deferredSave.lastError has the most recent error so we don't
+ // need this any more
+ this._loadError = null;
+ });
+ }
+ },
+
+ flush: function() {
+ // handle the "in memory only" and "saveChanges never called" cases
+ if (!this._deferredSave) {
+ return Promise.resolve(0);
+ }
+
+ return this._deferredSave.flush();
+ },
+
+ /**
+ * Converts the current internal state of the XPI addon database to
+ * a JSON.stringify()-ready structure
+ */
+ toJSON: function() {
+ if (!this.addonDB) {
+ // We never loaded the database?
+ throw new Error("Attempt to save database without loading it first");
+ }
+
+ let toSave = {
+ schemaVersion: DB_SCHEMA,
+ addons: [...this.addonDB.values()]
+ };
+ return toSave;
+ },
+
+ /**
+ * Pull upgrade information from an existing SQLITE database
+ *
+ * @return false if there is no SQLITE database
+ * true and sets this.migrateData to null if the SQLITE DB exists
+ * but does not contain useful information
+ * true and sets this.migrateData to
+ * {location: {id1:{addon1}, id2:{addon2}}, location2:{...}, ...}
+ * if there is useful information
+ */
+ getMigrateDataFromSQLITE: function XPIDB_getMigrateDataFromSQLITE() {
+ let connection = null;
+ let dbfile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_DATABASE], true);
+ // Attempt to open the database
+ try {
+ connection = Services.storage.openUnsharedDatabase(dbfile);
+ }
+ catch (e) {
+ logger.warn("Failed to open sqlite database " + dbfile.path + " for upgrade", e);
+ return null;
+ }
+ logger.debug("Migrating data from sqlite");
+ let migrateData = this.getMigrateDataFromDatabase(connection);
+ connection.close();
+ return migrateData;
+ },
+
+ /**
+ * Synchronously opens and reads the database file, upgrading from old
+ * databases or making a new DB if needed.
+ *
+ * The possibilities, in order of priority, are:
+ * 1) Perfectly good, up to date database
+ * 2) Out of date JSON database needs to be upgraded => upgrade
+ * 3) JSON database exists but is mangled somehow => build new JSON
+ * 4) no JSON DB, but a useable SQLITE db we can upgrade from => upgrade
+ * 5) useless SQLITE DB => build new JSON
+ * 6) useable RDF DB => upgrade
+ * 7) useless RDF DB => build new JSON
+ * 8) Nothing at all => build new JSON
+ * @param aRebuildOnError
+ * A boolean indicating whether add-on information should be loaded
+ * from the install locations if the database needs to be rebuilt.
+ * (if false, caller is XPIProvider.checkForChanges() which will rebuild)
+ */
+ syncLoadDB: function XPIDB_syncLoadDB(aRebuildOnError) {
+ this.migrateData = null;
+ let fstream = null;
+ let data = "";
+ try {
+ let readTimer = AddonManagerPrivate.simpleTimer("XPIDB_syncRead_MS");
+ logger.debug("Opening XPI database " + this.jsonFile.path);
+ fstream = Components.classes["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Components.interfaces.nsIFileInputStream);
+ fstream.init(this.jsonFile, -1, 0, 0);
+ let cstream = null;
+ try {
+ cstream = Components.classes["@mozilla.org/intl/converter-input-stream;1"].
+ createInstance(Components.interfaces.nsIConverterInputStream);
+ cstream.init(fstream, "UTF-8", 0, 0);
+
+ let str = {};
+ let read = 0;
+ do {
+ read = cstream.readString(0xffffffff, str); // read as much as we can and put it in str.value
+ data += str.value;
+ } while (read != 0);
+
+ readTimer.done();
+ this.parseDB(data, aRebuildOnError);
+ }
+ catch(e) {
+ logger.error("Failed to load XPI JSON data from profile", e);
+ let rebuildTimer = AddonManagerPrivate.simpleTimer("XPIDB_rebuildReadFailed_MS");
+ this.rebuildDatabase(aRebuildOnError);
+ rebuildTimer.done();
+ }
+ finally {
+ if (cstream)
+ cstream.close();
+ }
+ }
+ catch (e) {
+ if (e.result === Cr.NS_ERROR_FILE_NOT_FOUND) {
+ this.upgradeDB(aRebuildOnError);
+ }
+ else {
+ this.rebuildUnreadableDB(e, aRebuildOnError);
+ }
+ }
+ finally {
+ if (fstream)
+ fstream.close();
+ }
+ // If an async load was also in progress, resolve that promise with our DB;
+ // otherwise create a resolved promise
+ if (this._dbPromise) {
+ AddonManagerPrivate.recordSimpleMeasure("XPIDB_overlapped_load", 1);
+ this._dbPromise.resolve(this.addonDB);
+ }
+ else
+ this._dbPromise = Promise.resolve(this.addonDB);
+ },
+
+ /**
+ * Parse loaded data, reconstructing the database if the loaded data is not valid
+ * @param aRebuildOnError
+ * If true, synchronously reconstruct the database from installed add-ons
+ */
+ parseDB: function(aData, aRebuildOnError) {
+ let parseTimer = AddonManagerPrivate.simpleTimer("XPIDB_parseDB_MS");
+ try {
+ // dump("Loaded JSON:\n" + aData + "\n");
+ let inputAddons = JSON.parse(aData);
+ // Now do some sanity checks on our JSON db
+ if (!("schemaVersion" in inputAddons) || !("addons" in inputAddons)) {
+ parseTimer.done();
+ // Content of JSON file is bad, need to rebuild from scratch
+ logger.error("bad JSON file contents");
+ AddonManagerPrivate.recordSimpleMeasure("XPIDB_startupError", "badJSON");
+ let rebuildTimer = AddonManagerPrivate.simpleTimer("XPIDB_rebuildBadJSON_MS");
+ this.rebuildDatabase(aRebuildOnError);
+ rebuildTimer.done();
+ return;
+ }
+ if (inputAddons.schemaVersion != DB_SCHEMA) {
+ // Handle mismatched JSON schema version. For now, we assume
+ // compatibility for JSON data, though we throw away any fields we
+ // don't know about (bug 902956)
+ AddonManagerPrivate.recordSimpleMeasure("XPIDB_startupError",
+ "schemaMismatch-" + inputAddons.schemaVersion);
+ logger.debug("JSON schema mismatch: expected " + DB_SCHEMA +
+ ", actual " + inputAddons.schemaVersion);
+ // When we rev the schema of the JSON database, we need to make sure we
+ // force the DB to save so that the DB_SCHEMA value in the JSON file and
+ // the preference are updated.
+ }
+ // If we got here, we probably have good data
+ // Make AddonInternal instances from the loaded data and save them
+ let addonDB = new Map();
+ for (let loadedAddon of inputAddons.addons) {
+ let newAddon = new DBAddonInternal(loadedAddon);
+ addonDB.set(newAddon._key, newAddon);
+ };
+ parseTimer.done();
+ this.addonDB = addonDB;
+ logger.debug("Successfully read XPI database");
+ this.initialized = true;
+ }
+ catch(e) {
+ // If we catch and log a SyntaxError from the JSON
+ // parser, the xpcshell test harness fails the test for us: bug 870828
+ parseTimer.done();
+ if (e.name == "SyntaxError") {
+ logger.error("Syntax error parsing saved XPI JSON data");
+ AddonManagerPrivate.recordSimpleMeasure("XPIDB_startupError", "syntax");
+ }
+ else {
+ logger.error("Failed to load XPI JSON data from profile", e);
+ AddonManagerPrivate.recordSimpleMeasure("XPIDB_startupError", "other");
+ }
+ let rebuildTimer = AddonManagerPrivate.simpleTimer("XPIDB_rebuildReadFailed_MS");
+ this.rebuildDatabase(aRebuildOnError);
+ rebuildTimer.done();
+ }
+ },
+
+ /**
+ * Upgrade database from earlier (sqlite or RDF) version if available
+ */
+ upgradeDB: function(aRebuildOnError) {
+ let upgradeTimer = AddonManagerPrivate.simpleTimer("XPIDB_upgradeDB_MS");
+ try {
+ let schemaVersion = Services.prefs.getIntPref(PREF_DB_SCHEMA);
+ if (schemaVersion <= LAST_SQLITE_DB_SCHEMA) {
+ // we should have an older SQLITE database
+ logger.debug("Attempting to upgrade from SQLITE database");
+ this.migrateData = this.getMigrateDataFromSQLITE();
+ }
+ else {
+ // we've upgraded before but the JSON file is gone, fall through
+ // and rebuild from scratch
+ AddonManagerPrivate.recordSimpleMeasure("XPIDB_startupError", "dbMissing");
+ }
+ }
+ catch(e) {
+ // No schema version pref means either a really old upgrade (RDF) or
+ // a new profile
+ this.migrateData = this.getMigrateDataFromRDF();
+ }
+
+ this.rebuildDatabase(aRebuildOnError);
+ upgradeTimer.done();
+ },
+
+ /**
+ * Reconstruct when the DB file exists but is unreadable
+ * (for example because read permission is denied)
+ */
+ rebuildUnreadableDB: function(aError, aRebuildOnError) {
+ let rebuildTimer = AddonManagerPrivate.simpleTimer("XPIDB_rebuildUnreadableDB_MS");
+ logger.warn("Extensions database " + this.jsonFile.path +
+ " exists but is not readable; rebuilding", aError);
+ // Remember the error message until we try and write at least once, so
+ // we know at shutdown time that there was a problem
+ this._loadError = aError;
+ AddonManagerPrivate.recordSimpleMeasure("XPIDB_startupError", "unreadable");
+ this.rebuildDatabase(aRebuildOnError);
+ rebuildTimer.done();
+ },
+
+ /**
+ * Open and read the XPI database asynchronously, upgrading if
+ * necessary. If any DB load operation fails, we need to
+ * synchronously rebuild the DB from the installed extensions.
+ *
+ * @return Promise<Map> resolves to the Map of loaded JSON data stored
+ * in this.addonDB; never rejects.
+ */
+ asyncLoadDB: function XPIDB_asyncLoadDB() {
+ // Already started (and possibly finished) loading
+ if (this._dbPromise) {
+ return this._dbPromise;
+ }
+
+ logger.debug("Starting async load of XPI database " + this.jsonFile.path);
+ AddonManagerPrivate.recordSimpleMeasure("XPIDB_async_load", XPIProvider.runPhase);
+ let readOptions = {
+ outExecutionDuration: 0
+ };
+ return this._dbPromise = OS.File.read(this.jsonFile.path, null, readOptions).then(
+ byteArray => {
+ logger.debug("Async JSON file read took " + readOptions.outExecutionDuration + " MS");
+ AddonManagerPrivate.recordSimpleMeasure("XPIDB_asyncRead_MS",
+ readOptions.outExecutionDuration);
+ if (this._addonDB) {
+ logger.debug("Synchronous load completed while waiting for async load");
+ return this.addonDB;
+ }
+ logger.debug("Finished async read of XPI database, parsing...");
+ let decodeTimer = AddonManagerPrivate.simpleTimer("XPIDB_decode_MS");
+ let decoder = new TextDecoder();
+ let data = decoder.decode(byteArray);
+ decodeTimer.done();
+ this.parseDB(data, true);
+ return this.addonDB;
+ })
+ .then(null,
+ error => {
+ if (this._addonDB) {
+ logger.debug("Synchronous load completed while waiting for async load");
+ return this.addonDB;
+ }
+ if (error.becauseNoSuchFile) {
+ this.upgradeDB(true);
+ }
+ else {
+ // it's there but unreadable
+ this.rebuildUnreadableDB(error, true);
+ }
+ return this.addonDB;
+ });
+ },
+
+ /**
+ * Rebuild the database from addon install directories. If this.migrateData
+ * is available, uses migrated information for settings on the addons found
+ * during rebuild
+ * @param aRebuildOnError
+ * A boolean indicating whether add-on information should be loaded
+ * from the install locations if the database needs to be rebuilt.
+ * (if false, caller is XPIProvider.checkForChanges() which will rebuild)
+ */
+ rebuildDatabase: function XIPDB_rebuildDatabase(aRebuildOnError) {
+ this.addonDB = new Map();
+ this.initialized = true;
+
+ if (XPIStates.size == 0) {
+ // No extensions installed, so we're done
+ logger.debug("Rebuilding XPI database with no extensions");
+ return;
+ }
+
+ // If there is no migration data then load the list of add-on directories
+ // that were active during the last run
+ if (!this.migrateData)
+ this.activeBundles = this.getActiveBundles();
+
+ if (aRebuildOnError) {
+ logger.warn("Rebuilding add-ons database from installed extensions.");
+ try {
+ XPIProvider.processFileChanges({}, false);
+ }
+ catch (e) {
+ logger.error("Failed to rebuild XPI database from installed extensions", e);
+ }
+ // Make sure to update the active add-ons and add-ons list on shutdown
+ Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
+ }
+ },
+
+ /**
+ * Gets the list of file descriptors of active extension directories or XPI
+ * files from the add-ons list. This must be loaded from disk since the
+ * directory service gives no easy way to get both directly. This list doesn't
+ * include themes as preferences already say which theme is currently active
+ *
+ * @return an array of persistent descriptors for the directories
+ */
+ getActiveBundles: function XPIDB_getActiveBundles() {
+ let bundles = [];
+
+ // non-bootstrapped extensions
+ let addonsList = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST],
+ true);
+
+ if (!addonsList.exists())
+ // XXX Irving believes this is broken in the case where there is no
+ // extensions.ini but there are bootstrap extensions (e.g. Android)
+ return null;
+
+ try {
+ let iniFactory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"]
+ .getService(Ci.nsIINIParserFactory);
+ let parser = iniFactory.createINIParser(addonsList);
+ let keys = parser.getKeys("ExtensionDirs");
+
+ while (keys.hasMore())
+ bundles.push(parser.getString("ExtensionDirs", keys.getNext()));
+ }
+ catch (e) {
+ logger.warn("Failed to parse extensions.ini", e);
+ return null;
+ }
+
+ // Also include the list of active bootstrapped extensions
+ for (let id in XPIProvider.bootstrappedAddons)
+ bundles.push(XPIProvider.bootstrappedAddons[id].descriptor);
+
+ return bundles;
+ },
+
+ /**
+ * Retrieves migration data from the old extensions.rdf database.
+ *
+ * @return an object holding information about what add-ons were previously
+ * userDisabled and any updated compatibility information
+ */
+ getMigrateDataFromRDF: function XPIDB_getMigrateDataFromRDF(aDbWasMissing) {
+
+ // Migrate data from extensions.rdf
+ let rdffile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_OLD_DATABASE], true);
+ if (!rdffile.exists())
+ return null;
+
+ logger.debug("Migrating data from " + FILE_OLD_DATABASE);
+ let migrateData = {};
+
+ try {
+ let ds = gRDF.GetDataSourceBlocking(Services.io.newFileURI(rdffile).spec);
+ let root = Cc["@mozilla.org/rdf/container;1"].
+ createInstance(Ci.nsIRDFContainer);
+ root.Init(ds, gRDF.GetResource(RDFURI_ITEM_ROOT));
+ let elements = root.GetElements();
+
+ while (elements.hasMoreElements()) {
+ let source = elements.getNext().QueryInterface(Ci.nsIRDFResource);
+
+ let location = getRDFProperty(ds, source, "installLocation");
+ if (location) {
+ if (!(location in migrateData))
+ migrateData[location] = {};
+ let id = source.ValueUTF8.substring(PREFIX_ITEM_URI.length);
+ migrateData[location][id] = {
+ version: getRDFProperty(ds, source, "version"),
+ userDisabled: false,
+ targetApplications: []
+ }
+
+ let disabled = getRDFProperty(ds, source, "userDisabled");
+ if (disabled == "true" || disabled == "needs-disable")
+ migrateData[location][id].userDisabled = true;
+
+ let targetApps = ds.GetTargets(source, EM_R("targetApplication"),
+ true);
+ while (targetApps.hasMoreElements()) {
+ let targetApp = targetApps.getNext()
+ .QueryInterface(Ci.nsIRDFResource);
+ let appInfo = {
+ id: getRDFProperty(ds, targetApp, "id")
+ };
+
+ let minVersion = getRDFProperty(ds, targetApp, "updatedMinVersion");
+ if (minVersion) {
+ appInfo.minVersion = minVersion;
+ appInfo.maxVersion = getRDFProperty(ds, targetApp, "updatedMaxVersion");
+ }
+ else {
+ appInfo.minVersion = getRDFProperty(ds, targetApp, "minVersion");
+ appInfo.maxVersion = getRDFProperty(ds, targetApp, "maxVersion");
+ }
+ migrateData[location][id].targetApplications.push(appInfo);
+ }
+ }
+ }
+ }
+ catch (e) {
+ logger.warn("Error reading " + FILE_OLD_DATABASE, e);
+ migrateData = null;
+ }
+
+ return migrateData;
+ },
+
+ /**
+ * Retrieves migration data from a database that has an older or newer schema.
+ *
+ * @return an object holding information about what add-ons were previously
+ * userDisabled and any updated compatibility information
+ */
+ getMigrateDataFromDatabase: function XPIDB_getMigrateDataFromDatabase(aConnection) {
+ let migrateData = {};
+
+ // Attempt to migrate data from a different (even future!) version of the
+ // database
+ try {
+ var stmt = aConnection.createStatement("PRAGMA table_info(addon)");
+
+ const REQUIRED = ["internal_id", "id", "location", "userDisabled",
+ "installDate", "version"];
+
+ let reqCount = 0;
+ let props = [];
+ for (let row in resultRows(stmt)) {
+ if (REQUIRED.indexOf(row.name) != -1) {
+ reqCount++;
+ props.push(row.name);
+ }
+ else if (DB_METADATA.indexOf(row.name) != -1) {
+ props.push(row.name);
+ }
+ else if (DB_BOOL_METADATA.indexOf(row.name) != -1) {
+ props.push(row.name);
+ }
+ }
+
+ if (reqCount < REQUIRED.length) {
+ logger.error("Unable to read anything useful from the database");
+ return null;
+ }
+ stmt.finalize();
+
+ stmt = aConnection.createStatement("SELECT " + props.join(",") + " FROM addon");
+ for (let row in resultRows(stmt)) {
+ if (!(row.location in migrateData))
+ migrateData[row.location] = {};
+ let addonData = {
+ targetApplications: []
+ }
+ migrateData[row.location][row.id] = addonData;
+
+ props.forEach(function(aProp) {
+ if (aProp == "isForeignInstall")
+ addonData.foreignInstall = (row[aProp] == 1);
+ if (DB_BOOL_METADATA.indexOf(aProp) != -1)
+ addonData[aProp] = row[aProp] == 1;
+ else
+ addonData[aProp] = row[aProp];
+ })
+ }
+
+ var taStmt = aConnection.createStatement("SELECT id, minVersion, " +
+ "maxVersion FROM " +
+ "targetApplication WHERE " +
+ "addon_internal_id=:internal_id");
+
+ for (let location in migrateData) {
+ for (let id in migrateData[location]) {
+ taStmt.params.internal_id = migrateData[location][id].internal_id;
+ delete migrateData[location][id].internal_id;
+ for (let row in resultRows(taStmt)) {
+ migrateData[location][id].targetApplications.push({
+ id: row.id,
+ minVersion: row.minVersion,
+ maxVersion: row.maxVersion
+ });
+ }
+ }
+ }
+ }
+ catch (e) {
+ // An error here means the schema is too different to read
+ logger.error("Error migrating data", e);
+ return null;
+ }
+ finally {
+ if (taStmt)
+ taStmt.finalize();
+ if (stmt)
+ stmt.finalize();
+ }
+
+ return migrateData;
+ },
+
+ /**
+ * Shuts down the database connection and releases all cached objects.
+ * Return: Promise{integer} resolves / rejects with the result of the DB
+ * flush after the database is flushed and
+ * all cleanup is done
+ */
+ shutdown: function XPIDB_shutdown() {
+ logger.debug("shutdown");
+ if (this.initialized) {
+ // If our last database I/O had an error, try one last time to save.
+ if (this.lastError)
+ this.saveChanges();
+
+ this.initialized = false;
+
+ if (this._deferredSave) {
+ AddonManagerPrivate.recordSimpleMeasure(
+ "XPIDB_saves_total", this._deferredSave.totalSaves);
+ AddonManagerPrivate.recordSimpleMeasure(
+ "XPIDB_saves_overlapped", this._deferredSave.overlappedSaves);
+ AddonManagerPrivate.recordSimpleMeasure(
+ "XPIDB_saves_late", this._deferredSave.dirty ? 1 : 0);
+ }
+
+ // Return a promise that any pending writes of the DB are complete and we
+ // are finished cleaning up
+ let flushPromise = this.flush();
+ flushPromise.then(null, error => {
+ logger.error("Flush of XPI database failed", error);
+ AddonManagerPrivate.recordSimpleMeasure("XPIDB_shutdownFlush_failed", 1);
+ // If our last attempt to read or write the DB failed, force a new
+ // extensions.ini to be written to disk on the next startup
+ Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
+ })
+ .then(count => {
+ // Clear out the cached addons data loaded from JSON
+ delete this.addonDB;
+ delete this._dbPromise;
+ // same for the deferred save
+ delete this._deferredSave;
+ // re-enable the schema version setter
+ delete this._schemaVersionSet;
+ });
+ return flushPromise;
+ }
+ return Promise.resolve(0);
+ },
+
+ /**
+ * Asynchronously list all addons that match the filter function
+ * @param aFilter
+ * Function that takes an addon instance and returns
+ * true if that addon should be included in the selected array
+ * @param aCallback
+ * Called back with an array of addons matching aFilter
+ * or an empty array if none match
+ */
+ getAddonList: function(aFilter, aCallback) {
+ this.asyncLoadDB().then(
+ addonDB => {
+ let addonList = _filterDB(addonDB, aFilter);
+ asyncMap(addonList, getRepositoryAddon, makeSafe(aCallback));
+ })
+ .then(null,
+ error => {
+ logger.error("getAddonList failed", error);
+ makeSafe(aCallback)([]);
+ });
+ },
+
+ /**
+ * (Possibly asynchronously) get the first addon that matches the filter function
+ * @param aFilter
+ * Function that takes an addon instance and returns
+ * true if that addon should be selected
+ * @param aCallback
+ * Called back with the addon, or null if no matching addon is found
+ */
+ getAddon: function(aFilter, aCallback) {
+ return this.asyncLoadDB().then(
+ addonDB => {
+ getRepositoryAddon(_findAddon(addonDB, aFilter), makeSafe(aCallback));
+ })
+ .then(null,
+ error => {
+ logger.error("getAddon failed", e);
+ makeSafe(aCallback)(null);
+ });
+ },
+
+ /**
+ * Asynchronously gets an add-on with a particular ID in a particular
+ * install location.
+ *
+ * @param aId
+ * The ID of the add-on to retrieve
+ * @param aLocation
+ * The name of the install location
+ * @param aCallback
+ * A callback to pass the DBAddonInternal to
+ */
+ getAddonInLocation: function XPIDB_getAddonInLocation(aId, aLocation, aCallback) {
+ this.asyncLoadDB().then(
+ addonDB => getRepositoryAddon(addonDB.get(aLocation + ":" + aId),
+ makeSafe(aCallback)));
+ },
+
+ /**
+ * Asynchronously gets the add-on with the specified ID that is visible.
+ *
+ * @param aId
+ * The ID of the add-on to retrieve
+ * @param aCallback
+ * A callback to pass the DBAddonInternal to
+ */
+ getVisibleAddonForID: function XPIDB_getVisibleAddonForID(aId, aCallback) {
+ this.getAddon(aAddon => ((aAddon.id == aId) && aAddon.visible),
+ aCallback);
+ },
+
+ /**
+ * Asynchronously gets the visible add-ons, optionally restricting by type.
+ *
+ * @param aTypes
+ * An array of types to include or null to include all types
+ * @param aCallback
+ * A callback to pass the array of DBAddonInternals to
+ */
+ getVisibleAddons: function XPIDB_getVisibleAddons(aTypes, aCallback) {
+ this.getAddonList(aAddon => (aAddon.visible &&
+ (!aTypes || (aTypes.length == 0) ||
+ (aTypes.indexOf(aAddon.type) > -1))),
+ aCallback);
+ },
+
+ /**
+ * Synchronously gets all add-ons of a particular type.
+ *
+ * @param aType
+ * The type of add-on to retrieve
+ * @return an array of DBAddonInternals
+ */
+ getAddonsByType: function XPIDB_getAddonsByType(aType) {
+ if (!this.addonDB) {
+ // jank-tastic! Must synchronously load DB if the theme switches from
+ // an XPI theme to a lightweight theme before the DB has loaded,
+ // because we're called from sync XPIProvider.addonChanged
+ logger.warn("Synchronous load of XPI database due to getAddonsByType(" + aType + ")");
+ AddonManagerPrivate.recordSimpleMeasure("XPIDB_lateOpen_byType", XPIProvider.runPhase);
+ this.syncLoadDB(true);
+ }
+ return _filterDB(this.addonDB, aAddon => (aAddon.type == aType));
+ },
+
+ /**
+ * Synchronously gets an add-on with a particular internalName.
+ *
+ * @param aInternalName
+ * The internalName of the add-on to retrieve
+ * @return a DBAddonInternal
+ */
+ getVisibleAddonForInternalName: function XPIDB_getVisibleAddonForInternalName(aInternalName) {
+ if (!this.addonDB) {
+ // This may be called when the DB hasn't otherwise been loaded
+ logger.warn("Synchronous load of XPI database due to getVisibleAddonForInternalName");
+ AddonManagerPrivate.recordSimpleMeasure("XPIDB_lateOpen_forInternalName",
+ XPIProvider.runPhase);
+ this.syncLoadDB(true);
+ }
+
+ return _findAddon(this.addonDB,
+ aAddon => aAddon.visible &&
+ (aAddon.internalName == aInternalName));
+ },
+
+ /**
+ * Asynchronously gets all add-ons with pending operations.
+ *
+ * @param aTypes
+ * The types of add-ons to retrieve or null to get all types
+ * @param aCallback
+ * A callback to pass the array of DBAddonInternal to
+ */
+ getVisibleAddonsWithPendingOperations:
+ function XPIDB_getVisibleAddonsWithPendingOperations(aTypes, aCallback) {
+
+ this.getAddonList(
+ aAddon => (aAddon.visible &&
+ (aAddon.pendingUninstall ||
+ // Logic here is tricky. If we're active but disabled,
+ // we're pending disable; !active && !disabled, we're pending enable
+ (aAddon.active == aAddon.disabled)) &&
+ (!aTypes || (aTypes.length == 0) || (aTypes.indexOf(aAddon.type) > -1))),
+ aCallback);
+ },
+
+ /**
+ * Asynchronously get an add-on by its Sync GUID.
+ *
+ * @param aGUID
+ * Sync GUID of add-on to fetch
+ * @param aCallback
+ * A callback to pass the DBAddonInternal record to. Receives null
+ * if no add-on with that GUID is found.
+ *
+ */
+ getAddonBySyncGUID: function XPIDB_getAddonBySyncGUID(aGUID, aCallback) {
+ this.getAddon(aAddon => aAddon.syncGUID == aGUID,
+ aCallback);
+ },
+
+ /**
+ * Synchronously gets all add-ons in the database.
+ * This is only called from the preference observer for the default
+ * compatibility version preference, so we can return an empty list if
+ * we haven't loaded the database yet.
+ *
+ * @return an array of DBAddonInternals
+ */
+ getAddons: function XPIDB_getAddons() {
+ if (!this.addonDB) {
+ return [];
+ }
+ return _filterDB(this.addonDB, aAddon => true);
+ },
+
+ /**
+ * Synchronously adds an AddonInternal's metadata to the database.
+ *
+ * @param aAddon
+ * AddonInternal to add
+ * @param aDescriptor
+ * The file descriptor of the add-on
+ * @return The DBAddonInternal that was added to the database
+ */
+ addAddonMetadata: function XPIDB_addAddonMetadata(aAddon, aDescriptor) {
+ if (!this.addonDB) {
+ AddonManagerPrivate.recordSimpleMeasure("XPIDB_lateOpen_addMetadata",
+ XPIProvider.runPhase);
+ this.syncLoadDB(false);
+ }
+
+ let newAddon = new DBAddonInternal(aAddon);
+ newAddon.descriptor = aDescriptor;
+ this.addonDB.set(newAddon._key, newAddon);
+ if (newAddon.visible) {
+ this.makeAddonVisible(newAddon);
+ }
+
+ this.saveChanges();
+ return newAddon;
+ },
+
+ /**
+ * Synchronously updates an add-on's metadata in the database. Currently just
+ * removes and recreates.
+ *
+ * @param aOldAddon
+ * The DBAddonInternal to be replaced
+ * @param aNewAddon
+ * The new AddonInternal to add
+ * @param aDescriptor
+ * The file descriptor of the add-on
+ * @return The DBAddonInternal that was added to the database
+ */
+ updateAddonMetadata: function XPIDB_updateAddonMetadata(aOldAddon, aNewAddon,
+ aDescriptor) {
+ this.removeAddonMetadata(aOldAddon);
+ aNewAddon.syncGUID = aOldAddon.syncGUID;
+ aNewAddon.installDate = aOldAddon.installDate;
+ aNewAddon.applyBackgroundUpdates = aOldAddon.applyBackgroundUpdates;
+ aNewAddon.foreignInstall = aOldAddon.foreignInstall;
+ aNewAddon.active = (aNewAddon.visible && !aNewAddon.disabled && !aNewAddon.pendingUninstall);
+
+ // addAddonMetadata does a saveChanges()
+ return this.addAddonMetadata(aNewAddon, aDescriptor);
+ },
+
+ /**
+ * Synchronously removes an add-on from the database.
+ *
+ * @param aAddon
+ * The DBAddonInternal being removed
+ */
+ removeAddonMetadata: function XPIDB_removeAddonMetadata(aAddon) {
+ this.addonDB.delete(aAddon._key);
+ this.saveChanges();
+ },
+
+ /**
+ * Synchronously marks a DBAddonInternal as visible marking all other
+ * instances with the same ID as not visible.
+ *
+ * @param aAddon
+ * The DBAddonInternal to make visible
+ */
+ makeAddonVisible: function XPIDB_makeAddonVisible(aAddon) {
+ logger.debug("Make addon " + aAddon._key + " visible");
+ for (let [, otherAddon] of this.addonDB) {
+ if ((otherAddon.id == aAddon.id) && (otherAddon._key != aAddon._key)) {
+ logger.debug("Hide addon " + otherAddon._key);
+ otherAddon.visible = false;
+ }
+ }
+ aAddon.visible = true;
+ this.saveChanges();
+ },
+
+ /**
+ * Synchronously sets properties for an add-on.
+ *
+ * @param aAddon
+ * The DBAddonInternal being updated
+ * @param aProperties
+ * A dictionary of properties to set
+ */
+ setAddonProperties: function XPIDB_setAddonProperties(aAddon, aProperties) {
+ for (let key in aProperties) {
+ aAddon[key] = aProperties[key];
+ }
+ this.saveChanges();
+ },
+
+ /**
+ * Synchronously sets the Sync GUID for an add-on.
+ * Only called when the database is already loaded.
+ *
+ * @param aAddon
+ * The DBAddonInternal being updated
+ * @param aGUID
+ * GUID string to set the value to
+ * @throws if another addon already has the specified GUID
+ */
+ setAddonSyncGUID: function XPIDB_setAddonSyncGUID(aAddon, aGUID) {
+ // Need to make sure no other addon has this GUID
+ function excludeSyncGUID(otherAddon) {
+ return (otherAddon._key != aAddon._key) && (otherAddon.syncGUID == aGUID);
+ }
+ let otherAddon = _findAddon(this.addonDB, excludeSyncGUID);
+ if (otherAddon) {
+ throw new Error("Addon sync GUID conflict for addon " + aAddon._key +
+ ": " + otherAddon._key + " already has GUID " + aGUID);
+ }
+ aAddon.syncGUID = aGUID;
+ this.saveChanges();
+ },
+
+ /**
+ * Synchronously updates an add-on's active flag in the database.
+ *
+ * @param aAddon
+ * The DBAddonInternal to update
+ */
+ updateAddonActive: function XPIDB_updateAddonActive(aAddon, aActive) {
+ logger.debug("Updating active state for add-on " + aAddon.id + " to " + aActive);
+
+ aAddon.active = aActive;
+ this.saveChanges();
+ },
+
+ /**
+ * Synchronously calculates and updates all the active flags in the database.
+ */
+ updateActiveAddons: function XPIDB_updateActiveAddons() {
+ if (!this.addonDB) {
+ logger.warn("updateActiveAddons called when DB isn't loaded");
+ // force the DB to load
+ AddonManagerPrivate.recordSimpleMeasure("XPIDB_lateOpen_updateActive",
+ XPIProvider.runPhase);
+ this.syncLoadDB(true);
+ }
+ logger.debug("Updating add-on states");
+ for (let [, addon] of this.addonDB) {
+ let newActive = (addon.visible && !addon.disabled && !addon.pendingUninstall);
+ if (newActive != addon.active) {
+ addon.active = newActive;
+ this.saveChanges();
+ }
+ }
+ },
+
+ /**
+ * Writes out the XPI add-ons list for the platform to read.
+ * @return true if the file was successfully updated, false otherwise
+ */
+ writeAddonsList: function XPIDB_writeAddonsList() {
+ if (!this.addonDB) {
+ // force the DB to load
+ AddonManagerPrivate.recordSimpleMeasure("XPIDB_lateOpen_writeList",
+ XPIProvider.runPhase);
+ this.syncLoadDB(true);
+ }
+ Services.appinfo.invalidateCachesOnRestart();
+
+ let addonsList = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST],
+ true);
+ let enabledAddons = [];
+ let text = "[ExtensionDirs]\r\n";
+ let count = 0;
+ let fullCount = 0;
+
+ let activeAddons = _filterDB(
+ this.addonDB,
+ aAddon => aAddon.active && !aAddon.bootstrap && (aAddon.type != "theme"));
+
+ for (let row of activeAddons) {
+ text += "Extension" + (count++) + "=" + row.descriptor + "\r\n";
+ enabledAddons.push(encodeURIComponent(row.id) + ":" +
+ encodeURIComponent(row.version));
+ }
+ fullCount += count;
+
+ // The selected skin may come from an inactive theme (the default theme
+ // when a lightweight theme is applied for example)
+ text += "\r\n[ThemeDirs]\r\n";
+
+ let dssEnabled = false;
+ try {
+ dssEnabled = Services.prefs.getBoolPref(PREF_EM_DSS_ENABLED);
+ } catch (e) {}
+
+ let themes = [];
+ if (dssEnabled) {
+ themes = _filterDB(this.addonDB, aAddon => aAddon.type == "theme");
+ }
+ else {
+ let activeTheme = _findAddon(
+ this.addonDB,
+ aAddon => (aAddon.type == "theme") &&
+ (aAddon.internalName == XPIProvider.selectedSkin));
+ if (activeTheme) {
+ themes.push(activeTheme);
+ }
+ }
+
+ if (themes.length > 0) {
+ count = 0;
+ for (let row of themes) {
+ text += "Extension" + (count++) + "=" + row.descriptor + "\r\n";
+ enabledAddons.push(encodeURIComponent(row.id) + ":" +
+ encodeURIComponent(row.version));
+ }
+ fullCount += count;
+ }
+
+ text += "\r\n[MultiprocessIncompatibleExtensions]\r\n";
+
+ count = 0;
+ for (let row of activeAddons) {
+ if (!row.multiprocessCompatible) {
+ text += "Extension" + (count++) + "=" + row.id + "\r\n";
+ }
+ }
+
+ if (fullCount > 0) {
+ logger.debug("Writing add-ons list");
+
+ try {
+ let addonsListTmp = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST + ".tmp"],
+ true);
+ var fos = FileUtils.openFileOutputStream(addonsListTmp);
+ fos.write(text, text.length);
+ fos.close();
+ addonsListTmp.moveTo(addonsListTmp.parent, FILE_XPI_ADDONS_LIST);
+
+ Services.prefs.setCharPref(PREF_EM_ENABLED_ADDONS, enabledAddons.join(","));
+ }
+ catch (e) {
+ logger.error("Failed to write add-ons list to profile directory", e);
+ return false;
+ }
+ }
+ else {
+ if (addonsList.exists()) {
+ logger.debug("Deleting add-ons list");
+ try {
+ addonsList.remove(false);
+ }
+ catch (e) {
+ logger.error("Failed to remove " + addonsList.path, e);
+ return false;
+ }
+ }
+
+ Services.prefs.clearUserPref(PREF_EM_ENABLED_ADDONS);
+ }
+ return true;
+ }
+};
diff --git a/toolkit/mozapps/extensions/internal/moz.build b/toolkit/mozapps/extensions/internal/moz.build
new file mode 100644
index 000000000..efcd2af21
--- /dev/null
+++ b/toolkit/mozapps/extensions/internal/moz.build
@@ -0,0 +1,40 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXTRA_JS_MODULES.addons += [
+ '../../webextensions/internal/ProductAddonChecker.jsm',
+ 'AddonLogging.jsm',
+ 'AddonRepository_SQLiteMigrator.jsm',
+ 'Content.js',
+ 'GMPProvider.jsm',
+ 'LightweightThemeImageOptimizer.jsm',
+ 'SpellCheckDictionaryBootstrap.js',
+]
+
+# Don't ship unused providers on Android
+if CONFIG['MOZ_WIDGET_TOOLKIT'] != 'android':
+ EXTRA_JS_MODULES.addons += [
+ 'PluginProvider.jsm',
+ ]
+
+EXTRA_PP_JS_MODULES.addons += [
+ 'AddonRepository.jsm',
+ 'AddonUpdateChecker.jsm',
+ 'XPIProvider.jsm',
+ 'XPIProviderUtils.js',
+]
+
+# This is used in multiple places, so is defined here to avoid it getting
+# out of sync.
+DEFINES['MOZ_EXTENSIONS_DB_SCHEMA'] = 16
+
+# Additional debugging info is exposed in debug builds
+if CONFIG['MOZ_EM_DEBUG']:
+ DEFINES['MOZ_EM_DEBUG'] = 1
+
+# Apperently this needs to be defined because it isn't picked up automagically any more
+if CONFIG['MOZ_PHOENIX_EXTENSIONS']:
+ DEFINES['MOZ_PHOENIX_EXTENSIONS'] = 1 \ No newline at end of file