/* 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} */ XPCOMUtils.defineLazyGetter(AboutPages, "aboutStudies", () => { const aboutStudies = new AboutPage({ chromeUrl: "resource://normandy-content/about-studies/about-studies.html", 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; });