forked from mirrors/gecko-dev
Backed out changeset 17d4c013ed92 (bug 1817183) Backed out changeset cfed8d9c23f3 (bug 1817183) Backed out changeset 62fe2f589efe (bug 1817182) Backed out changeset 557bd773fb85 (bug 1817179) Backed out changeset 7f8a7865868b (bug 1816934) Backed out changeset d6c1d4c0d2a0 (bug 1816934)
792 lines
25 KiB
JavaScript
792 lines
25 KiB
JavaScript
/* 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/. */
|
|
|
|
/**
|
|
* Control panel for the Ion project, formerly known as Pioneer.
|
|
* This lives in `about:ion` and provides a UI for users to un/enroll in the
|
|
* overall program, and to un/enroll from individual studies.
|
|
*
|
|
* NOTE - prefs and Telemetry both still mention Pioneer for backwards-compatibility,
|
|
* this may change in the future.
|
|
*/
|
|
|
|
const { AddonManager } = ChromeUtils.import(
|
|
"resource://gre/modules/AddonManager.jsm"
|
|
);
|
|
|
|
const { RemoteSettings } = ChromeUtils.import(
|
|
"resource://services-settings/remote-settings.js"
|
|
);
|
|
|
|
const { TelemetryController } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/TelemetryController.sys.mjs"
|
|
);
|
|
|
|
let parserUtils = Cc["@mozilla.org/parserutils;1"].getService(
|
|
Ci.nsIParserUtils
|
|
);
|
|
|
|
const PREF_ION_ID = "toolkit.telemetry.pioneerId";
|
|
const PREF_ION_NEW_STUDIES_AVAILABLE =
|
|
"toolkit.telemetry.pioneer-new-studies-available";
|
|
const PREF_ION_COMPLETED_STUDIES =
|
|
"toolkit.telemetry.pioneer-completed-studies";
|
|
|
|
/**
|
|
* Remote Settings keys for general content, and available studies.
|
|
*/
|
|
const CONTENT_COLLECTION_KEY = "pioneer-content-v2";
|
|
const STUDY_ADDON_COLLECTION_KEY = "pioneer-study-addons-v2";
|
|
|
|
const STUDY_LEAVE_REASONS = {
|
|
USER_ABANDONED: "user-abandoned",
|
|
STUDY_ENDED: "study-ended",
|
|
};
|
|
|
|
const PREF_TEST_CACHED_CONTENT = "toolkit.pioneer.testCachedContent";
|
|
const PREF_TEST_CACHED_ADDONS = "toolkit.pioneer.testCachedAddons";
|
|
const PREF_TEST_ADDONS = "toolkit.pioneer.testAddons";
|
|
|
|
/**
|
|
* Use the in-tree HTML Sanitizer to ensure that HTML from remote-settings is safe to use.
|
|
* Note that RS does use content-signing, we're doing this extra step as an in-depth security measure.
|
|
*
|
|
* @param {string} htmlString - unsanitized HTML (content-signed by remote-settings)
|
|
* @returns {DocumentFragment} - sanitized DocumentFragment
|
|
*/
|
|
function sanitizeHtml(htmlString) {
|
|
const content = document.createElement("div");
|
|
const contentFragment = parserUtils.parseFragment(
|
|
htmlString,
|
|
Ci.nsIParserUtils.SanitizerDropForms |
|
|
Ci.nsIParserUtils.SanitizerAllowStyle |
|
|
Ci.nsIParserUtils.SanitizerLogRemovals,
|
|
false,
|
|
Services.io.newURI("about:ion"),
|
|
content
|
|
);
|
|
|
|
return contentFragment;
|
|
}
|
|
|
|
function showEnrollmentStatus() {
|
|
const ionId = Services.prefs.getStringPref(PREF_ION_ID, null);
|
|
|
|
const enrollmentButton = document.getElementById("enrollment-button");
|
|
|
|
document.l10n.setAttributes(
|
|
enrollmentButton,
|
|
`ion-${ionId ? "un" : ""}enrollment-button`
|
|
);
|
|
enrollmentButton.classList.toggle("primary", !ionId);
|
|
|
|
// collapse content above the fold if enrolled, otherwise open it.
|
|
for (const section of ["details", "data"]) {
|
|
const details = document.getElementById(section);
|
|
if (ionId) {
|
|
details.removeAttribute("open");
|
|
} else {
|
|
details.setAttribute("open", true);
|
|
}
|
|
}
|
|
}
|
|
|
|
function toggleContentBasedOnLocale() {
|
|
const requestedLocale = Services.locale.requestedLocale;
|
|
if (requestedLocale !== "en-US") {
|
|
const localeNotificationBar = document.getElementById(
|
|
"locale-notification"
|
|
);
|
|
localeNotificationBar.style.display = "block";
|
|
|
|
const reportContent = document.getElementById("report-content");
|
|
reportContent.style.display = "none";
|
|
}
|
|
}
|
|
|
|
async function toggleEnrolled(studyAddonId, cachedAddons) {
|
|
let addon;
|
|
let install;
|
|
|
|
const cachedAddon = cachedAddons.find(a => a.addon_id == studyAddonId);
|
|
|
|
if (Cu.isInAutomation) {
|
|
install = {
|
|
install: async () => {
|
|
let testAddons = Services.prefs.getStringPref(PREF_TEST_ADDONS, "[]");
|
|
testAddons = JSON.parse(testAddons);
|
|
|
|
testAddons.push(studyAddonId);
|
|
Services.prefs.setStringPref(
|
|
PREF_TEST_ADDONS,
|
|
JSON.stringify(testAddons)
|
|
);
|
|
},
|
|
};
|
|
|
|
let testAddons = Services.prefs.getStringPref(PREF_TEST_ADDONS, "[]");
|
|
testAddons = JSON.parse(testAddons);
|
|
|
|
for (const testAddon of testAddons) {
|
|
if (testAddon == studyAddonId) {
|
|
addon = {};
|
|
addon.uninstall = () => {
|
|
Services.prefs.setStringPref(
|
|
PREF_TEST_ADDONS,
|
|
JSON.stringify(testAddons.filter(a => a.id != testAddon.id))
|
|
);
|
|
};
|
|
}
|
|
}
|
|
} else {
|
|
addon = await AddonManager.getAddonByID(studyAddonId);
|
|
install = await AddonManager.getInstallForURL(cachedAddon.sourceURI.spec);
|
|
}
|
|
|
|
const completedStudies = Services.prefs.getStringPref(
|
|
PREF_ION_COMPLETED_STUDIES,
|
|
"{}"
|
|
);
|
|
|
|
const study = document.querySelector(`.card[id="${cachedAddon.addon_id}"`);
|
|
const joinBtn = study.querySelector(".join-button");
|
|
|
|
if (addon) {
|
|
joinBtn.disabled = true;
|
|
await addon.uninstall();
|
|
await sendDeletionPing(studyAddonId);
|
|
|
|
document.l10n.setAttributes(joinBtn, "ion-join-study");
|
|
joinBtn.disabled = false;
|
|
|
|
// Record that the user abandoned this study, since it may not be re-join-able.
|
|
if (completedStudies) {
|
|
const studies = JSON.parse(completedStudies);
|
|
studies[studyAddonId] = STUDY_LEAVE_REASONS.USER_ABANDONED;
|
|
Services.prefs.setStringPref(
|
|
PREF_ION_COMPLETED_STUDIES,
|
|
JSON.stringify(studies)
|
|
);
|
|
}
|
|
} else {
|
|
// Check if this study is re-join-able before enrollment.
|
|
const studies = JSON.parse(completedStudies);
|
|
if (studyAddonId in studies) {
|
|
if (
|
|
"canRejoin" in cachedAddons[studyAddonId] &&
|
|
cachedAddons[studyAddonId].canRejoin === false
|
|
) {
|
|
console.error(
|
|
`Cannot rejoin ended study ${studyAddonId}, reason: ${studies[studyAddonId]}`
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
joinBtn.disabled = true;
|
|
await install.install();
|
|
document.l10n.setAttributes(joinBtn, "ion-leave-study");
|
|
joinBtn.disabled = false;
|
|
|
|
// Send an enrollment ping for this study. Note that this could be sent again
|
|
// if we are re-joining.
|
|
await sendEnrollmentPing(studyAddonId);
|
|
}
|
|
|
|
await updateStudy(cachedAddon.addon_id);
|
|
}
|
|
|
|
async function showAvailableStudies(cachedAddons) {
|
|
const ionId = Services.prefs.getStringPref(PREF_ION_ID, null);
|
|
const defaultAddons = cachedAddons.filter(a => a.isDefault);
|
|
if (ionId) {
|
|
for (const defaultAddon of defaultAddons) {
|
|
let addon;
|
|
let install;
|
|
if (Cu.isInAutomation) {
|
|
install = {
|
|
install: async () => {
|
|
if (
|
|
defaultAddon.addon_id == "ion-v2-bad-default-example@mozilla.org"
|
|
) {
|
|
throw new Error("Bad test default add-on");
|
|
}
|
|
},
|
|
};
|
|
} else {
|
|
addon = await AddonManager.getAddonByID(defaultAddon.addon_id);
|
|
install = await AddonManager.getInstallForURL(
|
|
defaultAddon.sourceURI.spec
|
|
);
|
|
}
|
|
|
|
if (!addon) {
|
|
// Any default add-ons are required, try to reinstall.
|
|
await install.install();
|
|
}
|
|
}
|
|
}
|
|
|
|
const studyAddons = cachedAddons.filter(a => !a.isDefault);
|
|
for (const cachedAddon of studyAddons) {
|
|
if (!cachedAddon) {
|
|
console.error(
|
|
`about:ion - Study addon ID not found in cache: ${studyAddonId}`
|
|
);
|
|
return;
|
|
}
|
|
|
|
const studyAddonId = cachedAddon.addon_id;
|
|
|
|
const study = document.createElement("div");
|
|
study.setAttribute("id", studyAddonId);
|
|
study.setAttribute("class", "card card-no-hover");
|
|
|
|
if (cachedAddon.icons && 32 in cachedAddon.icons) {
|
|
const iconName = document.createElement("img");
|
|
iconName.setAttribute("class", "card-icon");
|
|
iconName.setAttribute("src", cachedAddon.icons[32]);
|
|
study.appendChild(iconName);
|
|
}
|
|
|
|
const studyBody = document.createElement("div");
|
|
studyBody.classList.add("card-body");
|
|
study.appendChild(studyBody);
|
|
|
|
const studyName = document.createElement("h3");
|
|
studyName.setAttribute("class", "card-name");
|
|
studyName.textContent = cachedAddon.name;
|
|
studyBody.appendChild(studyName);
|
|
|
|
const studyAuthor = document.createElement("span");
|
|
studyAuthor.setAttribute("class", "card-author");
|
|
studyAuthor.textContent = cachedAddon.authors.name;
|
|
studyBody.appendChild(studyAuthor);
|
|
|
|
const actions = document.createElement("div");
|
|
actions.classList.add("card-actions");
|
|
study.appendChild(actions);
|
|
|
|
const joinBtn = document.createElement("button");
|
|
joinBtn.setAttribute("id", `${studyAddonId}-join-button`);
|
|
joinBtn.classList.add("primary");
|
|
joinBtn.classList.add("join-button");
|
|
document.l10n.setAttributes(joinBtn, "ion-join-study");
|
|
|
|
joinBtn.addEventListener("click", async () => {
|
|
let addon;
|
|
if (Cu.isInAutomation) {
|
|
const testAddons = Services.prefs.getStringPref(PREF_TEST_ADDONS, "[]");
|
|
for (const testAddon of JSON.parse(testAddons)) {
|
|
if (testAddon == studyAddonId) {
|
|
addon = {};
|
|
addon.uninstall = () => {
|
|
Services.prefs.setStringPref(PREF_TEST_ADDONS, "[]");
|
|
};
|
|
}
|
|
}
|
|
} else {
|
|
addon = await AddonManager.getAddonByID(studyAddonId);
|
|
}
|
|
let joinOrLeave = addon ? "leave" : "join";
|
|
let dialog = document.getElementById(
|
|
`${joinOrLeave}-study-consent-dialog`
|
|
);
|
|
dialog.setAttribute("addon-id", cachedAddon.addon_id);
|
|
const consentText = dialog.querySelector(
|
|
`[id=${joinOrLeave}-study-consent]`
|
|
);
|
|
|
|
// Clears out any existing children with a single #text node
|
|
consentText.textContent = "";
|
|
|
|
const contentFragment = sanitizeHtml(
|
|
cachedAddon[`${joinOrLeave}StudyConsent`]
|
|
);
|
|
consentText.appendChild(contentFragment);
|
|
|
|
dialog.showModal();
|
|
dialog.scrollTop = 0;
|
|
|
|
const openEvent = new Event("open");
|
|
dialog.dispatchEvent(openEvent);
|
|
});
|
|
actions.appendChild(joinBtn);
|
|
|
|
const studyDesc = document.createElement("div");
|
|
studyDesc.setAttribute("class", "card-description");
|
|
|
|
const contentFragment = sanitizeHtml(cachedAddon.description);
|
|
studyDesc.appendChild(contentFragment);
|
|
|
|
study.appendChild(studyDesc);
|
|
|
|
const studyDataCollected = document.createElement("div");
|
|
studyDataCollected.setAttribute("class", "card-data-collected");
|
|
study.appendChild(studyDataCollected);
|
|
|
|
const dataCollectionDetailsHeader = document.createElement("p");
|
|
dataCollectionDetailsHeader.textContent = "This study will collect:";
|
|
studyDataCollected.appendChild(dataCollectionDetailsHeader);
|
|
|
|
const dataCollectionDetails = document.createElement("ul");
|
|
for (const dataCollectionDetail of cachedAddon.dataCollectionDetails) {
|
|
const detailsBullet = document.createElement("li");
|
|
detailsBullet.textContent = dataCollectionDetail;
|
|
dataCollectionDetails.append(detailsBullet);
|
|
}
|
|
studyDataCollected.appendChild(dataCollectionDetails);
|
|
|
|
const availableStudies = document.getElementById("available-studies");
|
|
availableStudies.appendChild(study);
|
|
|
|
await updateStudy(studyAddonId);
|
|
}
|
|
|
|
const availableStudies = document.getElementById("header-available-studies");
|
|
document.l10n.setAttributes(availableStudies, "ion-current-studies");
|
|
}
|
|
|
|
async function updateStudy(studyAddonId) {
|
|
let addon;
|
|
if (Cu.isInAutomation) {
|
|
const testAddons = Services.prefs.getStringPref(PREF_TEST_ADDONS, "[]");
|
|
for (const testAddon of JSON.parse(testAddons)) {
|
|
if (testAddon == studyAddonId) {
|
|
addon = {
|
|
uninstall() {},
|
|
};
|
|
}
|
|
}
|
|
} else {
|
|
addon = await AddonManager.getAddonByID(studyAddonId);
|
|
}
|
|
|
|
const study = document.querySelector(`.card[id="${studyAddonId}"`);
|
|
|
|
const joinBtn = study.querySelector(".join-button");
|
|
|
|
const ionId = Services.prefs.getStringPref(PREF_ION_ID, null);
|
|
|
|
const completedStudies = Services.prefs.getStringPref(
|
|
PREF_ION_COMPLETED_STUDIES,
|
|
"{}"
|
|
);
|
|
|
|
const studies = JSON.parse(completedStudies);
|
|
if (studyAddonId in studies) {
|
|
study.style.opacity = 0.5;
|
|
joinBtn.disabled = true;
|
|
document.l10n.setAttributes(joinBtn, "ion-ended-study");
|
|
return;
|
|
}
|
|
|
|
if (ionId) {
|
|
study.style.opacity = 1;
|
|
joinBtn.disabled = false;
|
|
|
|
if (addon) {
|
|
document.l10n.setAttributes(joinBtn, "ion-leave-study");
|
|
} else {
|
|
document.l10n.setAttributes(joinBtn, "ion-join-study");
|
|
}
|
|
} else {
|
|
document.l10n.setAttributes(joinBtn, "ion-study-prompt");
|
|
study.style.opacity = 0.5;
|
|
joinBtn.disabled = true;
|
|
}
|
|
}
|
|
|
|
// equivalent to what we use for Telemetry IDs
|
|
// https://searchfox.org/mozilla-central/rev/9193635dca8cfdcb68f114306194ffc860456044/toolkit/components/telemetry/app/TelemetryUtils.jsm#222
|
|
function generateUUID() {
|
|
let str = Services.uuid.generateUUID().toString();
|
|
return str.substring(1, str.length - 1);
|
|
}
|
|
|
|
async function setup(cachedAddons) {
|
|
document
|
|
.getElementById("enrollment-button")
|
|
.addEventListener("click", async () => {
|
|
const ionId = Services.prefs.getStringPref(PREF_ION_ID, null);
|
|
|
|
if (ionId) {
|
|
let dialog = document.getElementById("leave-ion-consent-dialog");
|
|
dialog.showModal();
|
|
dialog.scrollTop = 0;
|
|
} else {
|
|
let dialog = document.getElementById("join-ion-consent-dialog");
|
|
dialog.showModal();
|
|
dialog.scrollTop = 0;
|
|
}
|
|
});
|
|
|
|
document
|
|
.getElementById("join-ion-cancel-dialog-button")
|
|
.addEventListener("click", () =>
|
|
document.getElementById("join-ion-consent-dialog").close()
|
|
);
|
|
document
|
|
.getElementById("leave-ion-cancel-dialog-button")
|
|
.addEventListener("click", () =>
|
|
document.getElementById("leave-ion-consent-dialog").close()
|
|
);
|
|
document
|
|
.getElementById("join-study-cancel-dialog-button")
|
|
.addEventListener("click", () =>
|
|
document.getElementById("join-study-consent-dialog").close()
|
|
);
|
|
document
|
|
.getElementById("leave-study-cancel-dialog-button")
|
|
.addEventListener("click", () =>
|
|
document.getElementById("leave-study-consent-dialog").close()
|
|
);
|
|
|
|
document
|
|
.getElementById("join-ion-accept-dialog-button")
|
|
.addEventListener("click", async event => {
|
|
const ionId = Services.prefs.getStringPref(PREF_ION_ID, null);
|
|
|
|
if (!ionId) {
|
|
let uuid = generateUUID();
|
|
Services.prefs.setStringPref(PREF_ION_ID, uuid);
|
|
for (const cachedAddon of cachedAddons) {
|
|
if (cachedAddon.isDefault) {
|
|
let install;
|
|
if (Cu.isInAutomation) {
|
|
install = {
|
|
install: async () => {
|
|
if (
|
|
cachedAddon.addon_id ==
|
|
"ion-v2-bad-default-example@mozilla.org"
|
|
) {
|
|
throw new Error("Bad test default add-on");
|
|
}
|
|
},
|
|
};
|
|
} else {
|
|
install = await AddonManager.getInstallForURL(
|
|
cachedAddon.sourceURI.spec
|
|
);
|
|
}
|
|
|
|
try {
|
|
await install.install();
|
|
} catch (ex) {
|
|
// No need to throw here, we'll try again before letting users enroll in any studies.
|
|
console.error(
|
|
`Could not install default add-on ${cachedAddon.addon_id}`
|
|
);
|
|
const availableStudies = document.getElementById(
|
|
"available-studies"
|
|
);
|
|
document.l10n.setAttributes(
|
|
availableStudies,
|
|
"ion-no-current-studies"
|
|
);
|
|
}
|
|
}
|
|
const study = document.getElementById(cachedAddon.addon_id);
|
|
if (study) {
|
|
await updateStudy(cachedAddon.addon_id);
|
|
}
|
|
}
|
|
document.querySelector("dialog").close();
|
|
}
|
|
// A this point we should have a valid ion id, so we should be able to send
|
|
// the enrollment ping.
|
|
await sendEnrollmentPing();
|
|
|
|
showEnrollmentStatus();
|
|
});
|
|
|
|
document
|
|
.getElementById("leave-ion-accept-dialog-button")
|
|
.addEventListener("click", async event => {
|
|
const completedStudies = Services.prefs.getStringPref(
|
|
PREF_ION_COMPLETED_STUDIES,
|
|
"{}"
|
|
);
|
|
const studies = JSON.parse(completedStudies);
|
|
|
|
// Send a deletion ping for all completed studies the user has been a part of.
|
|
for (const studyAddonId in studies) {
|
|
await sendDeletionPing(studyAddonId);
|
|
}
|
|
|
|
Services.prefs.clearUserPref(PREF_ION_COMPLETED_STUDIES);
|
|
|
|
for (const cachedAddon of cachedAddons) {
|
|
// Record any studies that have been marked as concluded on the server, in case they re-enroll.
|
|
if ("studyEnded" in cachedAddon && cachedAddon.studyEnded === true) {
|
|
studies[cachedAddon.addon_id] = STUDY_LEAVE_REASONS.STUDY_ENDED;
|
|
|
|
Services.prefs.setStringPref(
|
|
PREF_ION_COMPLETED_STUDIES,
|
|
JSON.stringify(studies)
|
|
);
|
|
}
|
|
|
|
let addon;
|
|
if (Cu.isInAutomation) {
|
|
addon = {};
|
|
addon.id = cachedAddon.addon_id;
|
|
addon.uninstall = () => {
|
|
let testAddons = Services.prefs.getStringPref(
|
|
PREF_TEST_ADDONS,
|
|
"[]"
|
|
);
|
|
testAddons = JSON.parse(testAddons);
|
|
|
|
Services.prefs.setStringPref(
|
|
PREF_TEST_ADDONS,
|
|
JSON.stringify(
|
|
testAddons.filter(a => a.id != cachedAddon.addon_id)
|
|
)
|
|
);
|
|
};
|
|
} else {
|
|
addon = await AddonManager.getAddonByID(cachedAddon.addon_id);
|
|
}
|
|
if (addon) {
|
|
await sendDeletionPing(addon.id);
|
|
await addon.uninstall();
|
|
}
|
|
}
|
|
|
|
Services.prefs.clearUserPref(PREF_ION_ID);
|
|
for (const cachedAddon of cachedAddons) {
|
|
const study = document.getElementById(cachedAddon.addon_id);
|
|
if (study) {
|
|
await updateStudy(cachedAddon.addon_id);
|
|
}
|
|
}
|
|
|
|
document.getElementById("leave-ion-consent-dialog").close();
|
|
showEnrollmentStatus();
|
|
});
|
|
|
|
document
|
|
.getElementById("join-study-accept-dialog-button")
|
|
.addEventListener("click", async event => {
|
|
const dialog = document.getElementById("join-study-consent-dialog");
|
|
const studyAddonId = dialog.getAttribute("addon-id");
|
|
toggleEnrolled(studyAddonId, cachedAddons).then(dialog.close());
|
|
});
|
|
|
|
document
|
|
.getElementById("leave-study-accept-dialog-button")
|
|
.addEventListener("click", async event => {
|
|
const dialog = document.getElementById("leave-study-consent-dialog");
|
|
const studyAddonId = dialog.getAttribute("addon-id");
|
|
await toggleEnrolled(studyAddonId, cachedAddons).then(dialog.close());
|
|
});
|
|
|
|
const onAddonEvent = async addon => {
|
|
for (const cachedAddon of cachedAddons) {
|
|
if (cachedAddon.addon_id == addon.id) {
|
|
await updateStudy(addon.id);
|
|
}
|
|
}
|
|
};
|
|
|
|
const addonsListener = {
|
|
onEnabled: onAddonEvent,
|
|
onDisabled: onAddonEvent,
|
|
onInstalled: onAddonEvent,
|
|
onUninstalled: onAddonEvent,
|
|
};
|
|
AddonManager.addAddonListener(addonsListener);
|
|
|
|
window.addEventListener("unload", event => {
|
|
AddonManager.removeAddonListener(addonsListener);
|
|
});
|
|
}
|
|
|
|
function removeBadge() {
|
|
Services.prefs.setBoolPref(PREF_ION_NEW_STUDIES_AVAILABLE, false);
|
|
|
|
for (let win of Services.wm.getEnumerator("navigator:browser")) {
|
|
const badge = win.document
|
|
.getElementById("ion-button")
|
|
.querySelector(".toolbarbutton-badge");
|
|
badge.classList.remove("feature-callout");
|
|
}
|
|
}
|
|
|
|
// Updates Ion HTML page contents from RemoteSettings.
|
|
function updateContents(contents) {
|
|
for (const section of [
|
|
"title",
|
|
"summary",
|
|
"details",
|
|
"data",
|
|
"joinIonConsent",
|
|
"leaveIonConsent",
|
|
]) {
|
|
if (contents && section in contents) {
|
|
// Generate a corresponding dom-id style ID for a camel-case domId style JS attribute.
|
|
// Dynamically set the tag type based on which section is getting updated.
|
|
const domId = section
|
|
.split(/(?=[A-Z])/)
|
|
.join("-")
|
|
.toLowerCase();
|
|
// Clears out any existing children with a single #text node.
|
|
document.getElementById(domId).textContent = "";
|
|
|
|
const contentFragment = sanitizeHtml(contents[section]);
|
|
document.getElementById(domId).appendChild(contentFragment);
|
|
}
|
|
}
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", async domEvent => {
|
|
toggleContentBasedOnLocale();
|
|
|
|
showEnrollmentStatus();
|
|
|
|
document.addEventListener("focus", removeBadge);
|
|
removeBadge();
|
|
|
|
const privacyPolicyLinks = document.querySelectorAll(
|
|
".privacy-policy,.privacy-notice"
|
|
);
|
|
for (const privacyPolicyLink of privacyPolicyLinks) {
|
|
const privacyPolicyFormattedLink = Services.urlFormatter.formatURL(
|
|
privacyPolicyLink.href
|
|
);
|
|
privacyPolicyLink.href = privacyPolicyFormattedLink;
|
|
}
|
|
|
|
let cachedContent;
|
|
let cachedAddons;
|
|
if (Cu.isInAutomation) {
|
|
let testCachedAddons = Services.prefs.getStringPref(
|
|
PREF_TEST_CACHED_ADDONS,
|
|
null
|
|
);
|
|
if (testCachedAddons) {
|
|
cachedAddons = JSON.parse(testCachedAddons);
|
|
}
|
|
|
|
let testCachedContent = Services.prefs.getStringPref(
|
|
PREF_TEST_CACHED_CONTENT,
|
|
null
|
|
);
|
|
if (testCachedContent) {
|
|
cachedContent = JSON.parse(testCachedContent);
|
|
}
|
|
} else {
|
|
cachedContent = await RemoteSettings(CONTENT_COLLECTION_KEY).get();
|
|
cachedAddons = await RemoteSettings(STUDY_ADDON_COLLECTION_KEY).get();
|
|
}
|
|
|
|
// Replace existing contents immediately on page load.
|
|
for (const contents of cachedContent) {
|
|
updateContents(contents);
|
|
}
|
|
|
|
for (const cachedAddon of cachedAddons) {
|
|
// Record any studies that have been marked as concluded on the server.
|
|
if ("studyEnded" in cachedAddon && cachedAddon.studyEnded === true) {
|
|
const completedStudies = Services.prefs.getStringPref(
|
|
PREF_ION_COMPLETED_STUDIES,
|
|
"{}"
|
|
);
|
|
const studies = JSON.parse(completedStudies);
|
|
studies[cachedAddon.addon_id] = STUDY_LEAVE_REASONS.STUDY_ENDED;
|
|
|
|
Services.prefs.setStringPref(
|
|
PREF_ION_COMPLETED_STUDIES,
|
|
JSON.stringify(studies)
|
|
);
|
|
}
|
|
}
|
|
|
|
await setup(cachedAddons);
|
|
|
|
try {
|
|
await showAvailableStudies(cachedAddons);
|
|
} catch (ex) {
|
|
// No need to throw here, we'll try again before letting users enroll in any studies.
|
|
console.error(`Could not show available studies`, ex);
|
|
}
|
|
});
|
|
|
|
async function sendDeletionPing(studyAddonId) {
|
|
const type = "pioneer-study";
|
|
|
|
const options = {
|
|
studyName: studyAddonId,
|
|
addPioneerId: true,
|
|
useEncryption: true,
|
|
// NOTE - while we're not actually sending useful data in this payload, the current Pioneer v2 Telemetry
|
|
// pipeline requires that pings are shaped this way so they are routed to the correct environment.
|
|
//
|
|
// At the moment, the public key used here isn't important but we do need to use *something*.
|
|
encryptionKeyId: "discarded",
|
|
publicKey: {
|
|
crv: "P-256",
|
|
kty: "EC",
|
|
x: "XLkI3NaY3-AF2nRMspC63BT1u0Y3moXYSfss7VuQ0mk",
|
|
y: "SB0KnIW-pqk85OIEYZenoNkEyOOp5GeWQhS1KeRtEUE",
|
|
},
|
|
schemaName: "deletion-request",
|
|
schemaVersion: 1,
|
|
// The schema namespace needs to be the study addon id, as we
|
|
// want to route the ping to the specific study table.
|
|
schemaNamespace: studyAddonId,
|
|
};
|
|
|
|
const payload = {
|
|
encryptedData: "",
|
|
};
|
|
|
|
await TelemetryController.submitExternalPing(type, payload, options);
|
|
}
|
|
|
|
/**
|
|
* Sends a Pioneer enrollment ping.
|
|
*
|
|
* The `creationDate` provided by the telemetry APIs will be used as the timestamp for
|
|
* considering the user enrolled in pioneer and/or the study.
|
|
*
|
|
* @param [studyAddonid=undefined] - optional study id. It's sent in the ping, if present,
|
|
* to signal that user enroled in the study.
|
|
*/
|
|
async function sendEnrollmentPing(studyAddonId) {
|
|
let options = {
|
|
studyName: "pioneer-meta",
|
|
addPioneerId: true,
|
|
useEncryption: true,
|
|
// NOTE - while we're not actually sending useful data in this payload, the current Pioneer v2 Telemetry
|
|
// pipeline requires that pings are shaped this way so they are routed to the correct environment.
|
|
//
|
|
// At the moment, the public key used here isn't important but we do need to use *something*.
|
|
encryptionKeyId: "discarded",
|
|
publicKey: {
|
|
crv: "P-256",
|
|
kty: "EC",
|
|
x: "XLkI3NaY3-AF2nRMspC63BT1u0Y3moXYSfss7VuQ0mk",
|
|
y: "SB0KnIW-pqk85OIEYZenoNkEyOOp5GeWQhS1KeRtEUE",
|
|
},
|
|
schemaName: "pioneer-enrollment",
|
|
schemaVersion: 1,
|
|
// Note that the schema namespace directly informs how data is segregated after ingestion.
|
|
// If this is an enrollment ping for the pioneer program (in contrast to the enrollment to
|
|
// a specific study), use a meta namespace.
|
|
schemaNamespace: "pioneer-meta",
|
|
};
|
|
|
|
// If we were provided with a study id, then this is an enrollment to a study.
|
|
// Send the id alongside with the data and change the schema namespace to simplify
|
|
// the work on the ingestion pipeline.
|
|
if (typeof studyAddonId != "undefined") {
|
|
options.studyName = studyAddonId;
|
|
// The schema namespace needs to be the study addon id, as we
|
|
// want to route the ping to the specific study table.
|
|
options.schemaNamespace = studyAddonId;
|
|
}
|
|
|
|
await TelemetryController.submitExternalPing("pioneer-study", {}, options);
|
|
}
|