Verified Commit 4fe56d8e authored by henry's avatar henry Committed by ma1
Browse files

fixup! BB 40925: Implemented the Security Level component

TB 43782: Update security level UI for new UX flow.

In addition, we drop the distinction between the security levels in the
UI when the user has a custom security level.

I.e. we always show shield as unfilled but with a yellow dot in the
toolbar, and we just call it "Custom" rather than "Standard Custom",
etc.
parent 87bc322d
Loading
Loading
Loading
Loading
Loading
+73 −0
Original line number Diff line number Diff line
/**
 * Common methods for the desktop security level components.
 */
export const SecurityLevelUIUtils = {
  /**
   * Create an element that gives a description of the security level. To be
   * used in the settings.
   *
   * @param {string} level - The security level to describe.
   * @param {Document} doc - The document where the element will be inserted.
   *
   * @returns {Element} - The newly created element.
   */
  createDescriptionElement(level, doc) {
    const el = doc.createElement("div");
    el.classList.add("security-level-description");

    let l10nIdSummary;
    let bullets;
    switch (level) {
      case "standard":
        l10nIdSummary = "security-level-summary-standard";
        break;
      case "safer":
        l10nIdSummary = "security-level-summary-safer";
        bullets = [
          "security-level-preferences-bullet-https-only-javascript",
          "security-level-preferences-bullet-limit-font-and-symbols",
          "security-level-preferences-bullet-limit-media",
        ];
        break;
      case "safest":
        l10nIdSummary = "security-level-summary-safest";
        bullets = [
          "security-level-preferences-bullet-disabled-javascript",
          "security-level-preferences-bullet-limit-font-and-symbols-and-images",
          "security-level-preferences-bullet-limit-media",
        ];
        break;
      case "custom":
        l10nIdSummary = "security-level-summary-custom";
        break;
      default:
        throw Error(`Unhandled level: ${level}`);
    }

    const summaryEl = doc.createElement("div");
    summaryEl.classList.add("security-level-summary");
    doc.l10n.setAttributes(summaryEl, l10nIdSummary);

    el.append(summaryEl);

    if (!bullets) {
      return el;
    }

    const listEl = doc.createElement("ul");
    listEl.classList.add("security-level-description-extra");
    // Add a mozilla styling class as well:
    listEl.classList.add("privacy-extra-information");
    for (const l10nId of bullets) {
      const bulletEl = doc.createElement("li");
      bulletEl.classList.add("security-level-description-bullet");

      doc.l10n.setAttributes(bulletEl, l10nId);

      listEl.append(bulletEl);
    }

    el.append(listEl);
    return el;
  },
};
+63 −88
Original line number Diff line number Diff line
"use strict";

/* global AppConstants, Services, openPreferences, XPCOMUtils */
/* global AppConstants, Services, openPreferences, XPCOMUtils, gSubDialog */

ChromeUtils.defineESModuleGetters(this, {
  SecurityLevelPrefs: "resource://gre/modules/SecurityLevel.sys.mjs",
  SecurityLevelUIUtils: "resource:///modules/SecurityLevelUIUtils.sys.mjs",
});

/*
@@ -35,12 +36,8 @@ var SecurityLevelButton = {
  _anchorButton: null,

  _configUIFromPrefs() {
    const level = SecurityLevelPrefs.securityLevel;
    if (!level) {
      return;
    }
    const custom = SecurityLevelPrefs.securityCustom;
    this._button.setAttribute("level", custom ? `${level}_custom` : level);
    const level = SecurityLevelPrefs.securityLevelSummary;
    this._button.setAttribute("level", level);

    let l10nIdLevel;
    switch (level) {
@@ -53,15 +50,12 @@ var SecurityLevelButton = {
      case "safest":
        l10nIdLevel = "security-level-toolbar-button-safest";
        break;
      case "custom":
        l10nIdLevel = "security-level-toolbar-button-custom";
        break;
      default:
        throw Error(`Unhandled level: ${level}`);
    }
    if (custom) {
      // Don't distinguish between the different levels when in the custom
      // state. We just want to emphasise that it is custom rather than any
      // specific level.
      l10nIdLevel = "security-level-toolbar-button-custom";
    }
    document.l10n.setAttributes(this._button, l10nIdLevel);
  },

@@ -164,12 +158,7 @@ var SecurityLevelPanel = {
      panel: document.getElementById("securityLevel-panel"),
      background: document.getElementById("securityLevel-background"),
      levelName: document.getElementById("securityLevel-level"),
      customName: document.getElementById("securityLevel-custom"),
      summary: document.getElementById("securityLevel-summary"),
      restoreDefaultsButton: document.getElementById(
        "securityLevel-restoreDefaults"
      ),
      settingsButton: document.getElementById("securityLevel-settings"),
    };

    const learnMoreEl = document.getElementById("securityLevel-learnMore");
@@ -177,10 +166,9 @@ var SecurityLevelPanel = {
      this.hide();
    });

    this._elements.restoreDefaultsButton.addEventListener("command", () => {
      this.restoreDefaults();
    });
    this._elements.settingsButton.addEventListener("command", () => {
    document
      .getElementById("securityLevel-settings")
      .addEventListener("command", () => {
        this.openSecuritySettings();
      });

@@ -199,19 +187,7 @@ var SecurityLevelPanel = {
    }

    // get security prefs
    const level = SecurityLevelPrefs.securityLevel;
    const custom = SecurityLevelPrefs.securityCustom;

    // only visible when user is using custom settings
    this._elements.customName.hidden = !custom;
    this._elements.restoreDefaultsButton.hidden = !custom;
    if (custom) {
      this._elements.settingsButton.removeAttribute("default");
      this._elements.restoreDefaultsButton.setAttribute("default", "true");
    } else {
      this._elements.settingsButton.setAttribute("default", "true");
      this._elements.restoreDefaultsButton.removeAttribute("default");
    }
    const level = SecurityLevelPrefs.securityLevelSummary;

    // Descriptions change based on security level
    this._elements.background.setAttribute("level", level);
@@ -230,12 +206,13 @@ var SecurityLevelPanel = {
        l10nIdLevel = "security-level-panel-level-safest";
        l10nIdSummary = "security-level-summary-safest";
        break;
      case "custom":
        l10nIdLevel = "security-level-panel-level-custom";
        l10nIdSummary = "security-level-summary-custom";
        break;
      default:
        throw Error(`Unhandled level: ${level}`);
    }
    if (custom) {
      l10nIdSummary = "security-level-summary-custom";
    }

    document.l10n.setAttributes(this._elements.levelName, l10nIdLevel);
    document.l10n.setAttributes(this._elements.summary, l10nIdSummary);
@@ -269,13 +246,6 @@ var SecurityLevelPanel = {
    this._elements.panel.hidePopup();
  },

  restoreDefaults() {
    SecurityLevelPrefs.securityCustom = false;
    // Move focus to the settings button since restore defaults button will
    // become hidden.
    this._elements.settingsButton.focus();
  },

  openSecuritySettings() {
    openPreferences("privacy-securitylevel");
    this.hide();
@@ -301,59 +271,64 @@ var SecurityLevelPanel = {

var SecurityLevelPreferences = {
  _securityPrefsBranch: null,

  /**
   * The notification box shown when the user has a custom security setting.
   *
   * @type {Element}
   */
  _customNotification: null,
  /**
   * The radiogroup for this preference.
   *
   * @type {Element}
   */
  _radiogroup: null,
  /**
   * A list of radio options and their containers.
   * The element that shows the current security level.
   *
   * @type {Array<object>}
   * @type {?Element}
   */
  _radioOptions: null,
  _currentEl: null,

  _populateXUL() {
    this._customNotification = document.getElementById(
      "securityLevel-customNotification"
    this._currentEl = document.getElementById("security-level-current");
    const changeButton = document.getElementById("security-level-change");
    const badgeEl = this._currentEl.querySelector(
      ".security-level-current-badge"
    );
    document
      .getElementById("securityLevel-restoreDefaults")
      .addEventListener("command", () => {
        SecurityLevelPrefs.securityCustom = false;
      });

    this._radiogroup = document.getElementById("securityLevel-radiogroup");
    for (const { level, nameId } of [
      { level: "standard", nameId: "security-level-panel-level-standard" },
      { level: "safer", nameId: "security-level-panel-level-safer" },
      { level: "safest", nameId: "security-level-panel-level-safest" },
      { level: "custom", nameId: "security-level-panel-level-custom" },
    ]) {
      // Classes that control visibility:
      // security-level-current-standard
      // security-level-current-safer
      // security-level-current-safest
      // security-level-current-custom
      const visibilityClass = `security-level-current-${level}`;
      const nameEl = document.createElement("div");
      nameEl.classList.add("security-level-name", visibilityClass);
      document.l10n.setAttributes(nameEl, nameId);

      const descriptionEl = SecurityLevelUIUtils.createDescriptionElement(
        level,
        document
      );
      descriptionEl.classList.add(visibilityClass);

    this._radioOptions = Array.from(
      this._radiogroup.querySelectorAll(".securityLevel-radio-option"),
      container => {
        return { container, radio: container.querySelector("radio") };
      this._currentEl.insertBefore(nameEl, badgeEl);
      this._currentEl.insertBefore(descriptionEl, changeButton);
    }
    );

    this._radiogroup.addEventListener("select", () => {
      SecurityLevelPrefs.securityLevel = this._radiogroup.value;
    changeButton.addEventListener("click", () => {
      this._openDialog();
    });
  },

  _openDialog() {
    gSubDialog.open(
      "chrome://browser/content/securitylevel/securityLevelDialog.xhtml",
      { features: "resizable=yes" }
    );
  },

  _configUIFromPrefs() {
    this._radiogroup.value = SecurityLevelPrefs.securityLevel;
    const isCustom = SecurityLevelPrefs.securityCustom;
    this._radiogroup.disabled = isCustom;
    this._customNotification.hidden = !isCustom;
    // Have the container's selection CSS class match the selection state of the
    // radio elements.
    for (const { container, radio } of this._radioOptions) {
      container.classList.toggle("selected", radio.selected);
    }
    // Set a data-current-level attribute for showing the current security
    // level, and hiding the rest.
    this._currentEl.dataset.currentLevel =
      SecurityLevelPrefs.securityLevelSummary;
  },

  init() {
+1 −7
Original line number Diff line number Diff line
@@ -7,12 +7,6 @@ toolbarbutton#security-level-button[level="safer"] {
toolbarbutton#security-level-button[level="safest"] {
  list-style-image: url("chrome://browser/content/securitylevel/securityLevelIcon.svg#safest");
}
toolbarbutton#security-level-button[level="standard_custom"] {
toolbarbutton#security-level-button[level="custom"] {
  list-style-image: url("chrome://browser/content/securitylevel/securityLevelIcon.svg#standard_custom");
}
toolbarbutton#security-level-button[level="safer_custom"] {
  list-style-image: url("chrome://browser/content/securitylevel/securityLevelIcon.svg#safer_custom");
}
toolbarbutton#security-level-button[level="safest_custom"] {
  list-style-image: url("chrome://browser/content/securitylevel/securityLevelIcon.svg#safest_custom");
}
+188 −0
Original line number Diff line number Diff line
"use strict";

const { SecurityLevelPrefs } = ChromeUtils.importESModule(
  "resource://gre/modules/SecurityLevel.sys.mjs"
);
const { SecurityLevelUIUtils } = ChromeUtils.importESModule(
  "resource:///modules/SecurityLevelUIUtils.sys.mjs"
);

const gSecurityLevelDialog = {
  /**
   * The security level when this dialog was opened.
   *
   * @type {string}
   */
  _prevLevel: SecurityLevelPrefs.securityLevelSummary,
  /**
   * The security level currently selected.
   *
   * @type {string}
   */
  _selectedLevel: "",
  /**
   * The radiogroup for this preference.
   *
   * @type {?Element}
   */
  _radiogroup: null,
  /**
   * A list of radio options and their containers.
   *
   * @type {?Array<{ container: Element, radio: Element }>}
   */
  _radioOptions: null,

  /**
   * Initialise the dialog.
   */
  async init() {
    const dialog = document.getElementById("security-level-dialog");
    dialog.addEventListener("dialogaccept", event => {
      if (this._acceptButton.disabled) {
        event.preventDefault();
        return;
      }
      this._commitChange();
    });

    this._acceptButton = dialog.getButton("accept");

    document.l10n.setAttributes(
      this._acceptButton,
      "security-level-dialog-save-restart"
    );

    this._radiogroup = document.getElementById("security-level-radiogroup");

    this._radioOptions = Array.from(
      this._radiogroup.querySelectorAll(".security-level-radio-container"),
      container => {
        return {
          container,
          radio: container.querySelector(".security-level-radio"),
        };
      }
    );

    for (const { container, radio } of this._radioOptions) {
      const level = radio.value;
      radio.id = `security-level-radio-${level}`;
      const currentEl = container.querySelector(
        ".security-level-current-badge"
      );
      currentEl.id = `security-level-current-badge-${level}`;
      const descriptionEl = SecurityLevelUIUtils.createDescriptionElement(
        level,
        document
      );
      descriptionEl.classList.add("indent");
      descriptionEl.id = `security-level-description-${level}`;

      // Wait for the full translation of the element before adding it to the
      // DOM. In particular, we want to make sure the elements have text before
      // we measure the maxHeight below.
      await document.l10n.translateFragment(descriptionEl);
      document.l10n.pauseObserving();
      container.append(descriptionEl);
      document.l10n.resumeObserving();

      if (level === this._prevLevel) {
        currentEl.hidden = false;
        // When the currentEl is visible, include it in the accessible name for
        // the radio option.
        // NOTE: The currentEl has an accessible name which includes punctuation
        // to help separate it's content from the security level name.
        // E.g. "Standard (Current level)".
        radio.setAttribute("aria-labelledby", `${radio.id} ${currentEl.id}`);
      } else {
        currentEl.hidden = true;
      }
      // We point the accessible description to the wrapping
      // .security-level-description element, rather than its children
      // that define the actual text content. This means that when the
      // privacy-extra-information is shown or hidden, its text content is
      // included or excluded from the accessible description, respectively.
      radio.setAttribute("aria-describedby", descriptionEl.id);
    }

    // We want to reserve the maximum height of the radiogroup so that the
    // dialog has enough height when the user switches options. So we cycle
    // through the options and measure the height when they are selected to set
    // a minimum height that fits all of them.
    // NOTE: At the time of implementation, at this point the dialog may not
    // yet have the "subdialog" attribute, which means it is missing the
    // common.css stylesheet from its shadow root, which effects the size of the
    // .radio-check element and the font. Therefore, we have duplicated the
    // import of common.css in SecurityLevelDialog.xhtml to ensure it is applied
    // at this earlier stage.
    let maxHeight = 0;
    for (const { container } of this._radioOptions) {
      container.classList.add("selected");
      maxHeight = Math.max(
        maxHeight,
        this._radiogroup.getBoundingClientRect().height
      );
      container.classList.remove("selected");
    }
    this._radiogroup.style.minHeight = `${maxHeight}px`;

    if (this._prevLevel !== "custom") {
      this._selectedLevel = this._prevLevel;
      this._radiogroup.value = this._prevLevel;
    } else {
      this._radiogroup.selectedItem = null;
    }

    this._radiogroup.addEventListener("select", () => {
      this._selectedLevel = this._radiogroup.value;
      this._updateSelected();
    });

    this._updateSelected();
  },

  /**
   * Update the UI in response to a change in selection.
   */
  _updateSelected() {
    this._acceptButton.disabled =
      !this._selectedLevel || this._selectedLevel === this._prevLevel;
    // Have the container's `selected` CSS class match the selection state of
    // the radio elements.
    for (const { container, radio } of this._radioOptions) {
      container.classList.toggle("selected", radio.selected);
    }
  },

  /**
   * Commit the change in security level and restart the browser.
   */
  _commitChange() {
    SecurityLevelPrefs.setSecurityLevelBeforeRestart(this._selectedLevel);
    Services.startup.quit(
      Services.startup.eAttemptQuit | Services.startup.eRestart
    );
  },
};

// Initial focus is not visible, even if opened with a keyboard. We avoid the
// default handler and manage the focus ourselves, which will paint the focus
// ring by default.
// NOTE: A side effect is that the focus ring will show even if the user opened
// with a mouse event.
// TODO: Remove this once bugzilla bug 1708261 is resolved.
document.subDialogSetDefaultFocus = () => {
  document.getElementById("security-level-radiogroup").focus();
};

// Delay showing and sizing the subdialog until it is fully initialised.
document.mozSubdialogReady = new Promise(resolve => {
  window.addEventListener(
    "DOMContentLoaded",
    () => {
      gSecurityLevelDialog.init().finally(resolve);
    },
    { once: true }
  );
});
+86 −0
Original line number Diff line number Diff line
<?xml version="1.0"?>

<window
  xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
  xmlns:html="http://www.w3.org/1999/xhtml"
  data-l10n-id="security-level-dialog-window"
>
  <dialog id="security-level-dialog" buttons="accept,cancel">
    <linkset>
      <html:link rel="stylesheet" href="chrome://global/skin/global.css" />
      <!-- NOTE: We include common.css explicitly, rather than relying on
         - the dialog's shadowroot importing it, which is late loaded in
         - response to the dialog's "subdialog" attribute, which is set
         - in response to DOMFrameContentLoaded.
         - In particular, we need the .radio-check rule and font rules from
         - common-shared.css to be in place when gSecurityLevelDialog.init is
         - called, which will help ensure that the radio element has the correct
         - size when we measure its bounding box. -->
      <html:link
        rel="stylesheet"
        href="chrome://global/skin/in-content/common.css"
      />
      <html:link
        rel="stylesheet"
        href="chrome://browser/skin/preferences/preferences.css"
      />
      <html:link
        rel="stylesheet"
        href="chrome://browser/skin/preferences/privacy.css"
      />
      <html:link
        rel="stylesheet"
        href="chrome://browser/content/securitylevel/securityLevelPreferences.css"
      />

      <html:link rel="localization" href="branding/brand.ftl" />
      <html:link rel="localization" href="toolkit/global/base-browser.ftl" />
    </linkset>

    <script src="chrome://browser/content/securitylevel/securityLevelDialog.js" />

    <description data-l10n-id="security-level-dialog-restart-description" />

    <radiogroup id="security-level-radiogroup" class="highlighting-group">
      <html:div
        class="security-level-radio-container security-level-grid privacy-detailedoption info-box-container"
      >
        <radio
          class="security-level-radio security-level-name"
          value="standard"
          data-l10n-id="security-level-preferences-level-standard"
        />
        <html:div
          class="security-level-current-badge"
          data-l10n-id="security-level-preferences-current-badge"
        ></html:div>
      </html:div>
      <html:div
        class="security-level-radio-container security-level-grid privacy-detailedoption info-box-container"
      >
        <radio
          class="security-level-radio security-level-name"
          value="safer"
          data-l10n-id="security-level-preferences-level-safer"
        />
        <html:div
          class="security-level-current-badge"
          data-l10n-id="security-level-preferences-current-badge"
        ></html:div>
      </html:div>
      <html:div
        class="security-level-radio-container security-level-grid privacy-detailedoption info-box-container"
      >
        <radio
          class="security-level-radio security-level-name"
          value="safest"
          data-l10n-id="security-level-preferences-level-safest"
        />
        <html:div
          class="security-level-current-badge"
          data-l10n-id="security-level-preferences-current-badge"
        ></html:div>
      </html:div>
    </radiogroup>
  </dialog>
</window>
Loading