Source code

Revision control

Copy as Markdown

Other Tools

/* 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/. */
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
AddonStudies: "resource://normandy/lib/AddonStudies.sys.mjs",
BranchedAddonStudyAction:
"resource://normandy/actions/BranchedAddonStudyAction.sys.mjs",
ExperimentManager: "resource://nimbus/lib/ExperimentManager.sys.mjs",
PreferenceExperiments:
"resource://normandy/lib/PreferenceExperiments.sys.mjs",
RecipeRunner: "resource://normandy/lib/RecipeRunner.sys.mjs",
RemoteSettingsExperimentLoader:
"resource://nimbus/lib/RemoteSettingsExperimentLoader.sys.mjs",
});
const SHIELD_LEARN_MORE_URL_PREF = "app.normandy.shieldLearnMoreUrl";
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"gOptOutStudiesEnabled",
"app.shield.optoutstudies.enabled"
);
/**
* Class for managing an about: page that Normandy provides. Adapted from
* browser/components/pocket/content/AboutPocket.sys.mjs.
*
* @implements nsIFactory
* @implements nsIAboutModule
*/
class AboutPage {
constructor({ chromeUrl, aboutHost, classID, description, uriFlags }) {
this.chromeUrl = chromeUrl;
this.aboutHost = aboutHost;
this.classID = Components.ID(classID);
this.description = description;
this.uriFlags = uriFlags;
}
getURIFlags() {
return this.uriFlags;
}
newChannel(uri, loadInfo) {
const newURI = Services.io.newURI(this.chromeUrl);
const channel = Services.io.newChannelFromURIWithLoadInfo(newURI, loadInfo);
channel.originalURI = uri;
if (this.uriFlags & Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT) {
const principal = Services.scriptSecurityManager.createContentPrincipal(
uri,
{}
);
channel.owner = principal;
}
return channel;
}
}
AboutPage.prototype.QueryInterface = ChromeUtils.generateQI(["nsIAboutModule"]);
/**
* The module exported by this file.
*/
export let AboutPages = {};
/**
* The weak set that keeps track of which browsing contexts
* have an about:studies page.
*/
let BrowsingContexts = new WeakSet();
/**
* about:studies page for displaying in-progress and past Shield studies.
* @type {AboutPage}
* @implements {nsIMessageListener}
*/
ChromeUtils.defineLazyGetter(AboutPages, "aboutStudies", () => {
const aboutStudies = new AboutPage({
aboutHost: "studies",
classID: "{6ab96943-a163-482c-9622-4faedc0e827f}",
description: "Shield Study Listing",
uriFlags:
Ci.nsIAboutModule.ALLOW_SCRIPT |
Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT |
Ci.nsIAboutModule.URI_MUST_LOAD_IN_CHILD |
Ci.nsIAboutModule.IS_SECURE_CHROME_UI,
});
// Extra methods for about:study-specific behavior.
Object.assign(aboutStudies, {
getAddonStudyList() {
return lazy.AddonStudies.getAll();
},
getPreferenceStudyList() {
return lazy.PreferenceExperiments.getAll();
},
getMessagingSystemList() {
return lazy.ExperimentManager.store.getAll();
},
async optInToExperiment(data) {
try {
await lazy.RemoteSettingsExperimentLoader.optInToExperiment(data);
return {
error: false,
message: "Opt-in was successful.",
};
} catch (error) {
return {
error: true,
message: error.message,
};
}
},
/** Add a browsing context to the weak set;
* this weak set keeps track of all contexts
* that are housing an about:studies page.
*/
addToWeakSet(browsingContext) {
BrowsingContexts.add(browsingContext);
},
/** Remove a browsing context to the weak set;
* this weak set keeps track of all contexts
* that are housing an about:studies page.
*/
removeFromWeakSet(browsingContext) {
BrowsingContexts.delete(browsingContext);
},
/**
* Sends a message to every about:studies page,
* by iterating over the BrowsingContexts weakset.
* @param {string} message The message string to send to.
* @param {object} data The data object to send.
*/
_sendToAll(message, data) {
ChromeUtils.nondeterministicGetWeakSetKeys(BrowsingContexts).forEach(
browser =>
browser.currentWindowGlobal
.getActor("ShieldFrame")
.sendAsyncMessage(message, data)
);
},
/**
* Get if studies are enabled. This has to be in the parent process,
* since RecipeRunner is stateful, and can't be interacted with from
* content processes safely.
*/
async getStudiesEnabled() {
await lazy.RecipeRunner.initializedPromise.promise;
return lazy.RecipeRunner.enabled && lazy.gOptOutStudiesEnabled;
},
/**
* Disable an active add-on study and remove its add-on.
* @param {String} recipeId the id of the addon to remove
* @param {String} reason the reason for removal
*/
async removeAddonStudy(recipeId, reason) {
try {
const action = new lazy.BranchedAddonStudyAction();
await action.unenroll(recipeId, reason);
} catch (err) {
// If the exception was that the study was already removed, that's ok.
// If not, rethrow the error.
if (!err.toString().includes("already inactive")) {
throw err;
}
} finally {
// Update any open tabs with the new study list now that it has changed,
// even if the above failed.
this.getAddonStudyList().then(list =>
this._sendToAll("Shield:UpdateAddonStudyList", list)
);
}
},
/**
* Disable an active preference study.
* @param {String} experimentName the name of the experiment to remove
* @param {String} reason the reason for removal
*/
async removePreferenceStudy(experimentName, reason) {
try {
await lazy.PreferenceExperiments.stop(experimentName, {
reason,
caller: "AboutPages.removePreferenceStudy",
});
} catch (err) {
// If the exception was that the study was already removed, that's ok.
// If not, rethrow the error.
if (!err.toString().includes("already expired")) {
throw err;
}
} finally {
// Update any open tabs with the new study list now that it has changed,
// even if the above failed.
this.getPreferenceStudyList().then(list =>
this._sendToAll("Shield:UpdatePreferenceStudyList", list)
);
}
},
async removeMessagingSystemExperiment(slug, reason) {
lazy.ExperimentManager.unenroll(slug, reason);
this._sendToAll(
"Shield:UpdateMessagingSystemExperimentList",
lazy.ExperimentManager.store.getAll()
);
},
openDataPreferences() {
const browserWindow =
Services.wm.getMostRecentWindow("navigator:browser");
browserWindow.openPreferences("privacy-reports");
},
getShieldLearnMoreHref() {
return Services.urlFormatter.formatURLPref(SHIELD_LEARN_MORE_URL_PREF);
},
});
return aboutStudies;
});