--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -1581,20 +1581,23 @@ var AddonManagerInternal = {
* @param aName
* An optional placeholder name while the add-on is being downloaded
* @param aIcons
* Optional placeholder icons while the add-on is being downloaded
* @param aVersion
* An optional placeholder version while the add-on is being downloaded
* @param aBrowser
* An optional <browser> element for download permissions prompts.
+ * @param aTelemetryInfo
+ * An optional object which provides details about the installation source
+ * included in the addon manager telemetry events.
* @throws if the aUrl, aCallback or aMimetype arguments are not specified
*/
getInstallForURL(aUrl, aMimetype, aHash, aName,
- aIcons, aVersion, aBrowser) {
+ aIcons, aVersion, aBrowser, aTelemetryInfo) {
if (!gStarted)
throw Components.Exception("AddonManager is not initialized",
Cr.NS_ERROR_NOT_INITIALIZED);
if (!aUrl || typeof aUrl != "string")
throw Components.Exception("aURL must be a non-empty string",
Cr.NS_ERROR_INVALID_ARG);
@@ -1627,49 +1630,52 @@ var AddonManagerInternal = {
if (aBrowser && !Element.isInstance(aBrowser))
throw Components.Exception("aBrowser must be an Element or null",
Cr.NS_ERROR_INVALID_ARG);
for (let provider of this.providers) {
if (callProvider(provider, "supportsMimetype", false, aMimetype)) {
return promiseCallProvider(
provider, "getInstallForURL", aUrl, aHash, aName, aIcons,
- aVersion, aBrowser);
+ aVersion, aBrowser, aTelemetryInfo);
}
}
return Promise.resolve(null);
},
/**
* Asynchronously gets an AddonInstall for an nsIFile.
*
* @param aFile
* The nsIFile where the add-on is located
* @param aMimetype
* An optional mimetype hint for the add-on
+ * @param aTelemetryInfo
+ * An optional object which provides details about the installation source
+ * included in the addon manager telemetry events.
* @throws if the aFile or aCallback arguments are not specified
*/
- getInstallForFile(aFile, aMimetype) {
+ getInstallForFile(aFile, aMimetype, aTelemetryInfo) {
if (!gStarted)
throw Components.Exception("AddonManager is not initialized",
Cr.NS_ERROR_NOT_INITIALIZED);
if (!(aFile instanceof Ci.nsIFile))
throw Components.Exception("aFile must be a nsIFile",
Cr.NS_ERROR_INVALID_ARG);
if (aMimetype && typeof aMimetype != "string")
throw Components.Exception("aMimetype must be a string or null",
Cr.NS_ERROR_INVALID_ARG);
return (async () => {
for (let provider of this.providers) {
let install = await promiseCallProvider(
- provider, "getInstallForFile", aFile);
+ provider, "getInstallForFile", aFile, aTelemetryInfo);
if (install)
return install;
}
return null;
})();
},
@@ -2710,17 +2716,23 @@ var AddonManagerInternal = {
}
try {
checkInstallUrl(options.url);
} catch (err) {
return Promise.reject({message: err.message});
}
- return AddonManagerInternal.getInstallForURL(options.url, "application/x-xpinstall", options.hash)
+ let installTelemetryInfo = {
+ source: AddonManager.getInstallSourceFromHost(options.installSourceHost),
+ method: "amWebAPI",
+ };
+
+ return AddonManagerInternal.getInstallForURL(options.url, "application/x-xpinstall", options.hash,
+ null, null, null, null, installTelemetryInfo)
.then(install => {
AddonManagerInternal.setupPromptHandler(target, null, install, false, "AMO");
let id = this.nextInstall++;
let {listener, installPromise} = this.makeListener(id, target.messageManager);
install.addListener(listener);
this.installs.set(id, {install, target, listener, installPromise});
@@ -3006,16 +3018,22 @@ var AddonManagerPrivate = {
};
/**
* This is the public API that UI and developers should be calling. All methods
* just forward to AddonManagerInternal.
* @class
*/
var AddonManager = {
+ _installHostSource: new Map([
+ ["addons.mozilla.org", "amo"],
+ ["discovery.addons.mozilla.org", "disco"],
+ ["testpilot.firefox.com", "testpilot"],
+ ]),
+
// Constants for the AddonInstall.state property
// These will show up as AddonManager.STATE_* (eg, STATE_AVAILABLE)
_states: new Map([
// The install is available for download.
["STATE_AVAILABLE", 0],
// The install is being downloaded.
["STATE_DOWNLOADING", 1],
// The install is checking for compatibility information.
@@ -3265,24 +3283,58 @@ var AddonManager = {
stateToString(state) {
return this._stateToString.get(state);
},
errorToString(err) {
return err ? this._errorToString.get(err) : null;
},
+ getInstallSourceFromHost(host) {
+ if (this._installHostSource.has(host)) {
+ return this._installHostSource.get(host);
+ }
+
+ if (WEBAPI_TEST_INSTALL_HOSTS.includes(host)) {
+ return "test-hosts";
+ }
+
+ return "unknown";
+ },
+
+ getInstallSourceFromPrincipal(principal) {
+ if (!principal) {
+ return "no-principal";
+ }
+
+ if (principal.isSystemPrincipal) {
+ return "system-principal";
+ }
+
+ if (principal.isNullPrincipal) {
+ return "null-principal";
+ }
+
+ if (principal.isAddonOrExpandedAddonPrincipal) {
+ return "webextension";
+ }
+
+ let host = principal && principal.URI && principal.URI.host;
+
+ return this.getInstallSourceFromHost(host);
+ },
+
getInstallForURL(aUrl, aMimetype, aHash, aName, aIcons,
- aVersion, aBrowser) {
- return AddonManagerInternal.getInstallForURL(aUrl, aMimetype, aHash,
- aName, aIcons, aVersion, aBrowser);
+ aVersion, aBrowser, aTelemetryInfo) {
+ return AddonManagerInternal.getInstallForURL(
+ aUrl, aMimetype, aHash, aName, aIcons, aVersion, aBrowser, aTelemetryInfo);
},
- getInstallForFile(aFile, aMimetype) {
- return AddonManagerInternal.getInstallForFile(aFile, aMimetype);
+ getInstallForFile(aFile, aMimetype, aTelemetryInfo) {
+ return AddonManagerInternal.getInstallForFile(aFile, aMimetype, aTelemetryInfo);
},
/**
* Gets an array of add-on IDs that changed during the most recent startup.
*
* @param aType
* The type of startup change to get
* @return An array of add-on IDs
--- a/toolkit/mozapps/extensions/LightweightThemeManager.jsm
+++ b/toolkit/mozapps/extensions/LightweightThemeManager.jsm
@@ -115,16 +115,20 @@ var LightweightThemeManager = {
}
return _fallbackThemeData;
},
// Themes that can be added for an application. They can't be removed, and
// will always show up at the top of the list.
_builtInThemes: new Map(),
+ isBuiltIn(theme) {
+ return this._builtInThemes.has(theme.id);
+ },
+
get usedThemes() {
let themes = [];
try {
themes = JSON.parse(_prefs.getStringPref("usedThemes"));
} catch (e) { }
themes.push(...this._builtInThemes.values());
return themes;
@@ -677,16 +681,24 @@ AddonWrapper.prototype = {
get scope() {
return AddonManager.SCOPE_PROFILE;
},
get foreignInstall() {
return false;
},
+ get installTelemetryInfo() {
+ if (LightweightThemeManager.isBuiltIn(themeFor(this))) {
+ return {source: "builtin-theme"};
+ }
+
+ return {source: "lightweight-theme"};
+ },
+
uninstall() {
LightweightThemeManager.forgetUsedTheme(themeFor(this).id);
},
cancelUninstall() {
throw new Error("Theme is not marked to be uninstalled");
},
--- a/toolkit/mozapps/extensions/addonManager.js
+++ b/toolkit/mozapps/extensions/addonManager.js
@@ -68,62 +68,80 @@ amManager.prototype = {
case "message-manager-close":
case "message-manager-disconnect":
this.childClosed(aSubject);
break;
}
},
- installAddonFromWebpage(aMimetype, aBrowser, aInstallingPrincipal,
- aUri, aHash, aName, aIcon, aCallback) {
+ installAddonFromWebpage(aPayload, aBrowser, aCallback) {
let retval = true;
- if (!AddonManager.isInstallAllowed(aMimetype, aInstallingPrincipal)) {
+
+ const {
+ mimetype,
+ triggeringPrincipal,
+ hash,
+ icon,
+ name,
+ uri
+ } = aPayload;
+
+ if (!AddonManager.isInstallAllowed(mimetype, triggeringPrincipal)) {
aCallback = null;
retval = false;
}
- AddonManager.getInstallForURL(aUri, aMimetype, aHash, aName, aIcon, null, aBrowser).then(aInstall => {
- function callCallback(uri, status) {
+ let installTelemetryInfo = {
+ source: AddonManager.getInstallSourceFromPrincipal(triggeringPrincipal),
+ };
+
+ if ("installMethod" in aPayload) {
+ installTelemetryInfo.method = aPayload.installMethod;
+ }
+
+ AddonManager.getInstallForURL(uri, mimetype, hash, name, icon, null, aBrowser,
+ installTelemetryInfo).then(aInstall => {
+ function callCallback(status) {
try {
aCallback.onInstallEnded(uri, status);
} catch (e) {
Cu.reportError(e);
}
}
if (!aInstall) {
- aCallback.onInstallEnded(aUri, UNSUPPORTED_TYPE);
+ aCallback.onInstallEnded(uri, UNSUPPORTED_TYPE);
return;
}
if (aCallback) {
aInstall.addListener({
onDownloadCancelled(aInstall) {
- callCallback(aUri, USER_CANCELLED);
+ callCallback(USER_CANCELLED);
},
onDownloadFailed(aInstall) {
if (aInstall.error == AddonManager.ERROR_CORRUPT_FILE)
- callCallback(aUri, CANT_READ_ARCHIVE);
+ callCallback(CANT_READ_ARCHIVE);
else
- callCallback(aUri, DOWNLOAD_ERROR);
+ callCallback(DOWNLOAD_ERROR);
},
onInstallFailed(aInstall) {
- callCallback(aUri, EXECUTION_ERROR);
+ callCallback(EXECUTION_ERROR);
},
onInstallEnded(aInstall, aStatus) {
- callCallback(aUri, SUCCESS);
+ callCallback(SUCCESS);
}
});
}
- AddonManager.installAddonFromWebpage(aMimetype, aBrowser, aInstallingPrincipal, aInstall);
+ AddonManager.installAddonFromWebpage(mimetype, aBrowser, triggeringPrincipal, aInstall);
});
return retval;
},
notify(aTimer) {
AddonManagerPrivate.backgroundUpdateTimerHandler();
},
@@ -183,19 +201,17 @@ amManager.prototype = {
callbackID: payload.callbackID,
url,
status
});
},
};
}
- return this.installAddonFromWebpage(payload.mimetype,
- aMessage.target, payload.triggeringPrincipal, payload.uri,
- payload.hash, payload.name, payload.icon, callback);
+ return this.installAddonFromWebpage(payload, aMessage.target, callback);
}
case MSG_PROMISE_REQUEST: {
let mm = aMessage.target.messageManager;
let resolve = (value) => {
mm.sendAsyncMessage(MSG_PROMISE_RESULT, {
callbackID: payload.callbackID,
resolve: value
--- a/toolkit/mozapps/extensions/amContentHandler.js
+++ b/toolkit/mozapps/extensions/amContentHandler.js
@@ -44,17 +44,18 @@ amContentHandler.prototype = {
let install = {
uri: uri.spec,
hash: null,
name: null,
icon: null,
mimetype: XPI_CONTENT_TYPE,
triggeringPrincipal: aRequest.loadInfo.triggeringPrincipal,
- callbackID: -1
+ callbackID: -1,
+ installMethod: "url",
};
if (Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
// When running in the main process this might be a frame inside an
// in-content UI page, walk up to find the first frame element in a chrome
// privileged document
let element = window.frameElement;
let ssm = Services.scriptSecurityManager;
--- a/toolkit/mozapps/extensions/amInstallTrigger.js
+++ b/toolkit/mozapps/extensions/amInstallTrigger.js
@@ -174,16 +174,17 @@ InstallTrigger.prototype = {
}
}
let installData = {
uri: url.spec,
hash: item.Hash || null,
name: item.name,
icon: iconUrl ? iconUrl.spec : null,
+ installMethod: "installTrigger",
};
return this._mediator.install(installData, this._principal, callback, this._window);
},
startSoftwareUpdate(url, flags) {
let filename = Services.io.newURI(url)
.QueryInterface(Ci.nsIURL)
--- a/toolkit/mozapps/extensions/amWebAPI.js
+++ b/toolkit/mozapps/extensions/amWebAPI.js
@@ -224,17 +224,25 @@ class WebAPI extends APIObject {
return null;
}
let addon = new Addon(this.window, this.broker, addonInfo);
return this.window.Addon._create(this.window, addon);
});
}
createInstall(options) {
- return this._apiTask("createInstall", [options], installInfo => {
+ let installOptions = {
+ ...options,
+ // Provide the documentPrincipal from which the amWebAPI is being called
+ // (so that we can detect if the API is being used from the disco pane,
+ // AMO, testpilot or another unknown webpage).
+ installSourceHost: this.window.document.nodePrincipal.URI &&
+ this.window.document.nodePrincipal.URI.host,
+ };
+ return this._apiTask("createInstall", [installOptions], installInfo => {
if (!installInfo) {
return null;
}
let install = new AddonInstall(this.window, this.broker, installInfo);
this.allInstalls.push(installInfo.id);
return this.window.AddonInstall._create(this.window, install);
});
}
--- a/toolkit/mozapps/extensions/content/extensions.js
+++ b/toolkit/mozapps/extensions/content/extensions.js
@@ -1245,21 +1245,26 @@ var gViewController = {
"*.xpi;*.jar;*.zip");
fp.appendFilters(nsIFilePicker.filterAll);
} catch (e) { }
fp.open(async result => {
if (result != nsIFilePicker.returnOK)
return;
+ let installTelemetryInfo = {
+ source: "about:addons",
+ method: "install-from-file",
+ };
+
let browser = getBrowserElement();
let files = fp.files;
while (files.hasMoreElements()) {
let file = files.getNext();
- let install = await AddonManager.getInstallForFile(file);
+ let install = await AddonManager.getInstallForFile(file, null, installTelemetryInfo);
AddonManager.installAddonFromAOM(browser, document.documentURIObject, install);
}
});
}
},
cmd_debugAddons: {
isEnabled() {
@@ -3439,17 +3444,21 @@ var gDragDrop = {
} else {
let file = dataTransfer.mozGetDataAt("application/x-moz-file", i);
if (file) {
url = Services.io.newFileURI(file).spec;
}
}
if (url) {
- let install = await AddonManager.getInstallForURL(url, "application/x-xpinstall");
+ let install = await AddonManager.getInstallForURL(url, "application/x-xpinstall",
+ null, null, null, null, null, {
+ source: "about:addons",
+ method: "url",
+ });
AddonManager.installAddonFromAOM(browser, document.documentURIObject, install);
}
}
}
};
// Stub tabbrowser implementation for use by the tab-modal alert code
// when an alert/prompt/confirm method is called in a WebExtensions options_ui page
--- a/toolkit/mozapps/extensions/internal/GMPProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/GMPProvider.jsm
@@ -234,16 +234,20 @@ GMPWrapper.prototype = {
get providesUpdatesSecurely() {
return true;
},
get foreignInstall() {
return false;
},
+ get installTelemetryInfo() {
+ return {source: "gmp-plugin"};
+ },
+
isCompatibleWith(aAppVersion, aPlatformVersion) {
return true;
},
get applyBackgroundUpdates() {
if (!GMPPrefs.isSet(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, this._plugin.id)) {
return AddonManager.AUTOUPDATE_DEFAULT;
}
--- a/toolkit/mozapps/extensions/internal/PluginProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/PluginProvider.jsm
@@ -499,16 +499,20 @@ PluginWrapper.prototype = {
get providesUpdatesSecurely() {
return true;
},
get foreignInstall() {
return true;
},
+ get installTelemetryInfo() {
+ return {source: "plugin"};
+ },
+
isCompatibleWith(aAppVersion, aPlatformVersion) {
return true;
},
findUpdates(aListener, aReason, aAppVersion, aPlatformVersion) {
if ("onNoCompatibilityUpdateAvailable" in aListener)
aListener.onNoCompatibilityUpdateAvailable(this);
if ("onNoUpdateAvailable" in aListener)
--- a/toolkit/mozapps/extensions/internal/XPIDatabase.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIDatabase.jsm
@@ -89,17 +89,18 @@ const KEY_APP_PROFILE =
const KEY_APP_TEMPORARY = "app-temporary";
const DEFAULT_THEME_ID = "default-theme@mozilla.org";
// 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"];
+ "updateDate", "applyBackgroundUpdates", "compatibilityOverrides",
+ "installTelemetryInfo"];
const COMPATIBLE_BY_DEFAULT_TYPES = {
extension: true,
dictionary: true,
"webextension-dictionary": true,
};
// Properties to save in JSON file
@@ -111,17 +112,17 @@ const PROP_JSON_FIELDS = ["id", "syncGUI
"updateDate", "applyBackgroundUpdates", "path",
"skinnable", "sourceURI", "releaseNotesURI",
"softDisabled", "foreignInstall",
"strictCompatibility", "locales", "targetApplications",
"targetPlatforms", "signedState",
"seen", "dependencies", "hasEmbeddedWebExtension",
"userPermissions", "icons", "iconURL", "icon64URL",
"blocklistState", "blocklistURL", "startupData",
- "previewImage"];
+ "previewImage", "installTelemetryInfo"];
const LEGACY_TYPES = new Set([
"extension",
]);
// Some add-on types that we track internally are presented as other types
// externally
const TYPE_ALIASES = {
@@ -283,16 +284,17 @@ class AddonInternal {
this.blocklistState = Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
this.blocklistURL = null;
this.sourceURI = null;
this.releaseNotesURI = null;
this.foreignInstall = false;
this.seen = true;
this.skinnable = false;
this.startupData = null;
+ this.installTelemetryInfo = null;
this.inDatabase = false;
/**
* @property {Array<string>} dependencies
* An array of bootstrapped add-on IDs on which this add-on depends.
* The add-on will remain appDisabled if any of the dependent
* add-ons is not installed and enabled.
@@ -703,16 +705,31 @@ AddonWrapper = class {
return addonFor(this).hasEmbeddedWebExtension;
}
markAsSeen() {
addonFor(this).seen = true;
XPIDatabase.saveChanges();
}
+ get installTelemetryInfo() {
+ const addon = addonFor(this);
+ if (!addon.installTelemetryInfo && addon.location) {
+ if (addon.location.isSystem) {
+ return {source: "system-addon"};
+ }
+
+ if (addon.location.isTemporary) {
+ return {source: "temporary-addon"};
+ }
+ }
+
+ return addon.installTelemetryInfo;
+ }
+
get type() {
return XPIDatabase.getExternalType(addonFor(this).type);
}
get isWebExtension() {
return isWebExtension(addonFor(this).type);
}
@@ -2420,16 +2437,22 @@ this.XPIDatabaseReconcile = {
// Assume that add-ons in the system add-ons install location aren't
// foreign and should default to enabled.
aNewAddon.foreignInstall = isDetectedInstall && !aLocation.isSystem;
// appDisabled depends on whether the add-on is a foreignInstall so update
aNewAddon.appDisabled = !XPIDatabase.isUsableAddon(aNewAddon);
if (isDetectedInstall && aNewAddon.foreignInstall) {
+ // Add the installation source info for the sideloaded extension.
+ aNewAddon.installTelemetryInfo = {
+ source: aLocation.name,
+ method: "sideload",
+ };
+
// 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 = Services.prefs.getIntPref(PREF_EM_AUTO_DISABLED_SCOPES, 0);
if (aLocation.scope & disablingScopes) {
logger.warn(`Disabling foreign installed add-on ${aNewAddon.id} in ${aLocation.name}`);
aNewAddon.userDisabled = true;
aNewAddon.seen = false;
}
--- a/toolkit/mozapps/extensions/internal/XPIInstall.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIInstall.jsm
@@ -1303,16 +1303,21 @@ class AddonInstall {
* @param {string} [options.name]
* An optional name for the add-on
* @param {string} [options.type]
* An optional type for the add-on
* @param {object} [options.icons]
* Optional icons for the add-on
* @param {string} [options.version]
* An optional version for the add-on
+ * @param {Object?} [options.installTelemetryInfo]
+ * An optional object which provides details about the installation source
+ * included in the addon manager telemetry events.
+ * @param {boolean} [options.isUserRequestedUpdate]
+ * An optional boolean, true if the install object is related to a user triggered update.
* @param {function(string) : Promise<void>} [options.promptHandler]
* A callback to prompt the user before installing.
*/
constructor(installLocation, url, options = {}) {
this.wrapper = new AddonInstallWrapper(this);
this.location = installLocation;
this.sourceURI = url;
@@ -1345,16 +1350,18 @@ class AddonInstall {
this.maxProgress = -1;
// Giving each instance of AddonInstall a reference to the logger.
this.logger = logger;
this.name = options.name || null;
this.type = options.type || null;
this.version = options.version || null;
+ this.installTelemetryInfo = options.installTelemetryInfo || null;
+ this.isUserRequestedUpdate = options.isUserRequestedUpdate;
this.file = null;
this.ownsTempFile = null;
this.addon = null;
this.state = null;
XPIInstall.installs.add(this);
@@ -1511,16 +1518,25 @@ class AddonInstall {
*/
updateAddonURIs() {
this.addon.sourceURI = this.sourceURI.spec;
if (this.releaseNotesURI)
this.addon.releaseNotesURI = this.releaseNotesURI.spec;
}
/**
+ * Store the installTelemetryInfo into the persisted addon metadata.
+ */
+ updateInstallTelemetryInfo() {
+ if (this.installTelemetryInfo) {
+ this.addon.installTelemetryInfo = this.installTelemetryInfo;
+ }
+ }
+
+ /**
* Called after the add-on is a local file and the signature and install
* manifest can be read.
*
* @param {nsIFile} file
* The file from which to load the manifest.
* @returns {Promise<void>}
*/
async loadManifest(file) {
@@ -1571,16 +1587,17 @@ class AddonInstall {
"signature verification failed"]);
}
}
} finally {
pkg.close();
}
this.updateAddonURIs();
+ this.updateInstallTelemetryInfo();
this.addon._install = this;
this.name = this.addon.selectedLocale.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.
@@ -2400,29 +2417,36 @@ var DownloadAddonInstall = class extends
* Creates a new AddonInstall for an update.
*
* @param {function} aCallback
* The callback to pass the new AddonInstall to
* @param {AddonInternal} aAddon
* The add-on being updated
* @param {Object} aUpdate
* The metadata about the new version from the update manifest
+ * @param {boolean} isUserRequested
+ * An optional boolean, true if the install object is related to a user triggered update.
*/
-function createUpdate(aCallback, aAddon, aUpdate) {
+function createUpdate(aCallback, aAddon, aUpdate, isUserRequested) {
let url = Services.io.newURI(aUpdate.updateURL);
(async function() {
let opts = {
hash: aUpdate.updateHash,
existingAddon: aAddon,
name: aAddon.selectedLocale.name,
type: aAddon.type,
icons: aAddon.icons,
version: aUpdate.version,
+ // Inherits the installTelemetryInfo on updates (so that the source of the original
+ // installation telemetry data is being preserved across the extension updates).
+ installTelemetryInfo: aAddon.installTelemetryInfo,
+ isUserRequestedUpdate: isUserRequested,
};
+
let install;
if (url instanceof Ci.nsIFileURL) {
install = new LocalAddonInstall(aAddon.location, url, opts);
await install.init();
} else {
install = new DownloadAddonInstall(aAddon.location, url, opts);
}
try {
@@ -2476,16 +2500,24 @@ AddonInstallWrapper.prototype = {
get sourceURI() {
return installFor(this).sourceURI;
},
set promptHandler(handler) {
installFor(this).promptHandler = handler;
},
+ get installTelemetryInfo() {
+ return installFor(this).installTelemetryInfo;
+ },
+
+ get isUserRequestedUpdate() {
+ return installFor(this).isUserRequestedUpdate || false;
+ },
+
install() {
return installFor(this).install();
},
cancel() {
installFor(this).cancel();
},
@@ -2531,16 +2563,17 @@ var UpdateChecker = function(aAddon, aLi
this.addon = aAddon;
aAddon._updateCheck = this;
XPIInstall.doing(this);
this.listener = aListener;
this.appVersion = aAppVersion;
this.platformVersion = aPlatformVersion;
this.syncCompatibility = (aReason == AddonManager.UPDATE_WHEN_NEW_APP_INSTALLED);
+ this.isUserRequested = (aReason == AddonManager.UPDATE_WHEN_USER_REQUESTED);
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);
@@ -2671,17 +2704,17 @@ UpdateChecker.prototype = {
sendUpdateAvailableMessages(this, currentInstall);
} else
sendUpdateAvailableMessages(this, null);
return;
}
createUpdate(aInstall => {
sendUpdateAvailableMessages(this, aInstall);
- }, this.addon, update);
+ }, this.addon, update, this.isUserRequested);
} else {
sendUpdateAvailableMessages(this, null);
}
},
/**
* Called when AddonUpdateChecker fails the update check
*
@@ -2711,27 +2744,30 @@ UpdateChecker.prototype = {
/**
* Creates a new AddonInstall to install an add-on from a local file.
*
* @param {nsIFile} file
* The file to install
* @param {XPIStateLocation} location
* The location to install to
+ * @param {Object?} [installTelemetryInfo]
+ * An optional object which provides details about the installation source
+ * included in the addon manager telemetry events.
* @returns {Promise<AddonInstall>}
* A Promise that resolves with the new install object.
*/
-function createLocalInstall(file, location) {
+function createLocalInstall(file, location, installTelemetryInfo) {
if (!location) {
location = XPIStates.getLocation(KEY_APP_PROFILE);
}
let url = Services.io.newFileURI(file);
try {
- let install = new LocalAddonInstall(location, url);
+ let install = new LocalAddonInstall(location, url, {installTelemetryInfo});
return install.init().then(() => install);
} catch (e) {
logger.error("Error creating install", e);
XPIInstall.installs.delete(this);
return Promise.resolve(null);
}
}
@@ -3420,16 +3456,17 @@ var XPIInstall = {
* The XPI file to install the add-on from.
* @param {XPIStateLocation} location
* The install location to install the add-on to.
* @returns {AddonInternal}
* The installed Addon object, upon success.
*/
async installDistributionAddon(id, file, location) {
let addon = await loadManifestFromFile(file, location);
+ addon.installTelemetryInfo = {source: "distro"};
if (addon.id != id) {
throw new Error(`File file ${file.path} contains an add-on with an incorrect ID`);
}
let state = location.get(id);
if (state) {
@@ -3735,28 +3772,32 @@ var XPIInstall = {
* @param {string} [aName]
* A name for the install
* @param {Object} [aIcons]
* Icon URLs for the install
* @param {string} [aVersion]
* A version for the install
* @param {XULElement?} [aBrowser]
* The browser performing the install
+ * @param {Object?} [aInstallTelemetryInfo]
+ * An optional object which provides details about the installation source
+ * included in the addon manager telemetry events.
* @returns {AddonInstall}
*/
- async getInstallForURL(aUrl, aHash, aName, aIcons, aVersion, aBrowser) {
+ async getInstallForURL(aUrl, aHash, aName, aIcons, aVersion, aBrowser, aInstallTelemetryInfo) {
let location = XPIStates.getLocation(KEY_APP_PROFILE);
let url = Services.io.newURI(aUrl);
let options = {
hash: aHash,
browser: aBrowser,
name: aName,
icons: aIcons,
version: aVersion,
+ installTelemetryInfo: aInstallTelemetryInfo,
};
if (url instanceof Ci.nsIFileURL) {
let install = new LocalAddonInstall(location, url, options);
await install.init();
return install.wrapper;
}
@@ -3764,20 +3805,23 @@ var XPIInstall = {
return install.wrapper;
},
/**
* Called to get an AddonInstall to install an add-on from a local file.
*
* @param {nsIFile} aFile
* The file to be installed
+ * @param {Object?} [aInstallTelemetryInfo]
+ * An optional object which provides details about the installation source
+ * included in the addon manager telemetry events.
* @returns {AddonInstall?}
*/
- async getInstallForFile(aFile) {
- let install = await createLocalInstall(aFile);
+ async getInstallForFile(aFile, aInstallTelemetryInfo) {
+ let install = await createLocalInstall(aFile, null, aInstallTelemetryInfo);
return install ? install.wrapper : null;
},
/**
* Called to get the current AddonInstalls, optionally limiting to a list of
* types.
*
* @param {Array<string>?} aTypes
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_getInstallSourceFromPrincipal.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* globals MatchPatternSet, WebExtensionPolicy */
+
+// Test the AddonManager.getInstallSourceFromPrincipal and AddonManager.getInstallSourceFromHost
+// helpers.
+
+add_task(function test_getInstallSourceFromPrincipal_helpers() {
+ const ssm = Services.scriptSecurityManager;
+
+ const addonId = "@test-extension";
+ const uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+ const uuid = uuidGen.generateUUID().number.slice(1, -1);
+
+ let addonPolicy = new WebExtensionPolicy({
+ id: addonId,
+ mozExtensionHostname: uuid,
+ baseURL: "file:///",
+ allowedOrigins: new MatchPatternSet([]),
+ localizeCallback() {},
+ });
+ addonPolicy.active = true;
+
+ let extensionPrincipal = ssm.createCodebasePrincipalFromOrigin(`moz-extension://${uuid}`);
+
+ let addonExpandedPrincipal = Cu.getObjectPrincipal(Cu.Sandbox([
+ ssm.createCodebasePrincipalFromOrigin("https://example.com"),
+ extensionPrincipal,
+ ]));
+
+ let expandedPrincipal = Cu.getObjectPrincipal(Cu.Sandbox([
+ ssm.createCodebasePrincipalFromOrigin("https://example.com"),
+ ssm.createCodebasePrincipalFromOrigin("https://example.org"),
+ ]));
+
+ const principalTestCases = [
+ {
+ principal: ssm.createCodebasePrincipalFromOrigin("https://addons.allizom.org"),
+ installSourceFromPrincipal: "test-hosts",
+ installSourceFromHost: "test-hosts",
+ },
+ {
+ principal: ssm.createCodebasePrincipalFromOrigin("https://addons.mozilla.org"),
+ installSourceFromPrincipal: "amo",
+ installSourceFromHost: "amo",
+ },
+ {
+ principal: ssm.createCodebasePrincipalFromOrigin("https://discovery.addons.mozilla.org"),
+ installSourceFromPrincipal: "disco",
+ installSourceFromHost: "disco",
+ },
+ {
+ principal: ssm.createCodebasePrincipalFromOrigin("https://testpilot.firefox.com"),
+ installSourceFromPrincipal: "testpilot",
+ installSourceFromHost: "testpilot",
+ },
+ {
+ principal: ssm.createCodebasePrincipalFromOrigin("about:blank"),
+ installSourceFromPrincipal: "null-principal",
+ installSourceFromHost: "unknown",
+ },
+ {
+ principal: extensionPrincipal,
+ installSourceFromPrincipal: "webextension",
+ installSourceFromHost: "unknown",
+ },
+ {
+ principal: addonExpandedPrincipal,
+ installSourceFromPrincipal: "webextension",
+ installSourceFromHost: "unknown",
+ },
+ {
+ principal: expandedPrincipal,
+ installSourceFromPrincipal: "unknown",
+ installSourceFromHost: "unknown",
+ },
+ {
+ principal: Services.scriptSecurityManager.getSystemPrincipal(),
+ installSourceFromPrincipal: "system-principal",
+ installSourceFromHost: "unknown",
+ },
+ {
+ principal: null,
+ installSourceFromPrincipal: "no-principal",
+ installSourceFromHost: "unknown",
+ },
+ ];
+
+ for (let testCase of principalTestCases) {
+ let origin = testCase.principal ? testCase.principal.origin : null;
+ let host = testCase.principal && testCase.principal.URI && testCase.principal.URI.host;
+
+ equal(AddonManager.getInstallSourceFromPrincipal(testCase.principal), testCase.installSourceFromPrincipal,
+ `Got the expected result from getInstallFromPrincipal for a principal with origin ${origin}`);
+ equal(AddonManager.getInstallSourceFromHost(host), testCase.installSourceFromHost,
+ `Got the expected result from getInstallFromHost for a prinicipal with origin ${origin}`);
+ }
+});
--- a/toolkit/mozapps/extensions/test/xpcshell/test_shutdown.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_shutdown.js
@@ -7,17 +7,19 @@
const IGNORE = ["getPreferredIconURL", "escapeAddonURI",
"shouldAutoUpdate", "getStartupChanges",
"addTypeListener", "removeTypeListener",
"addAddonListener", "removeAddonListener",
"addInstallListener", "removeInstallListener",
"addManagerListener", "removeManagerListener",
"shutdown", "init",
"stateToString", "errorToString", "getUpgradeListener",
- "addUpgradeListener", "removeUpgradeListener"];
+ "addUpgradeListener", "removeUpgradeListener",
+ "getInstallSourceFromHost", "getInstallSourceFromPrincipal",
+ ];
const IGNORE_PRIVATE = ["AddonAuthor", "AddonCompatibilityOverride",
"AddonScreenshot", "AddonType", "startup", "shutdown",
"addonIsActive", "registerProvider", "unregisterProvider",
"addStartupChange", "removeStartupChange",
"getNewSideloads",
"recordTimestamp", "recordSimpleMeasure",
"recordException", "getSimpleMeasures", "simpleTimer",
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
@@ -94,16 +94,17 @@ tags = webextensions
# Bug 676992: test consistently hangs on Android
skip-if = os == "android"
[test_error.js]
[test_ext_management.js]
skip-if = appname == "thunderbird"
tags = webextensions
[test_general.js]
[test_getresource.js]
+[test_getInstallSourceFromPrincipal.js]
[test_gfxBlacklist_Device.js]
tags = blocklist
[test_gfxBlacklist_DriverNew.js]
tags = blocklist
[test_gfxBlacklist_Equal_DriverNew.js]
tags = blocklist
[test_gfxBlacklist_Equal_DriverOld.js]
tags = blocklist