forked from mirrors/gecko-dev
		
	Depends on D124391 Differential Revision: https://phabricator.services.mozilla.com/D124392
		
			
				
	
	
		
			794 lines
		
	
	
	
		
			25 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			794 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.import(
 | 
						|
  "resource://gre/modules/TelemetryController.jsm"
 | 
						|
);
 | 
						|
 | 
						|
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 | 
						|
 | 
						|
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);
 | 
						|
}
 |