tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

unexpectedScriptLoad.js (8902B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 /**
      6 * This singleton class controls the Unexpected Script Load Dialog.
      7 */
      8 var UnexpectedScriptLoadPanel = new (class {
      9  /** @type {Console?} */
     10  #console;
     11 
     12  /**
     13   * The URL of the script being handled by the panel.
     14   *
     15   * @type {string}
     16   */
     17  #scriptName = "";
     18 
     19  get console() {
     20    if (!this.#console) {
     21      this.#console = console.createInstance({
     22        maxLogLevelPref: "browser.unexpectedScriptLoad.logLevel",
     23        prefix: "UnexpectedScriptLoad",
     24      });
     25    }
     26    return this.#console;
     27  }
     28 
     29  /**
     30   * Where the lazy elements are stored.
     31   *
     32   * @type {Record<string, Element>?}
     33   */
     34  #lazyElements;
     35 
     36  /**
     37   * Lazily creates the dom elements, and lazily selects them.
     38   *
     39   * @returns {Record<string, Element>}
     40   */
     41  get elements() {
     42    if (!this.#lazyElements) {
     43      this.#lazyElements = {
     44        dialogCloseButton: document.querySelector(".dialogClose"),
     45        reportCheckbox: document.querySelector("#reportCheckbox"),
     46        emailCheckbox: document.querySelector("#emailCheckbox"),
     47        emailInput: document.querySelector("#emailInput"),
     48        allowButton: document.querySelector("#allow-button"),
     49        blockButton: document.querySelector("#block-button"),
     50        scriptUrl: document.querySelector(".scriptUrl"),
     51        unexpectedScriptLoadDetail1: document.querySelector(
     52          "#unexpected-script-load-detail-1"
     53        ),
     54        moreInfoLink: document.querySelector("#more-info-link"),
     55        learnMoreLink: document.querySelector("#learn-more-link"),
     56        telemetryDisabledMessage: document.querySelector(
     57          "#telemetry-disabled-message"
     58        ),
     59      };
     60    }
     61 
     62    return this.#lazyElements;
     63  }
     64 
     65  /**
     66   * Initializes the panel when the script loads.
     67   */
     68  init() {
     69    this.console?.log("UnexpectedScriptLoadPanel initialized");
     70 
     71    let args = window.arguments[0];
     72    let action = args.action;
     73    this.#scriptName = args.scriptName;
     74    this.elements.scriptUrl.textContent = this.#scriptName;
     75 
     76    let uploadEnabled = Services.prefs.getBoolPref(
     77      "datareporting.healthreport.uploadEnabled",
     78      false
     79    );
     80 
     81    if (action === "allow") {
     82      this.setupAllowLayout();
     83      Glean.unexpectedScriptLoad.scriptAllowedOpened.record();
     84    } else if (action === "block") {
     85      Glean.unexpectedScriptLoad.scriptBlockedOpened.record();
     86      this.setupBlockLayout(uploadEnabled);
     87    }
     88    this.setupEventHandlers();
     89 
     90    if (uploadEnabled) {
     91      this.elements.telemetryDisabledMessage.setAttribute("hidden", "true");
     92    } else {
     93      this.elements.telemetryDisabledMessage.removeAttribute("hidden");
     94    }
     95    this.elements.reportCheckbox.disabled = !uploadEnabled;
     96    this.elements.emailCheckbox.disabled = !uploadEnabled;
     97    this.elements.emailInput.disabled = !uploadEnabled;
     98    this.elements.emailInput.readOnly = !uploadEnabled;
     99  }
    100 
    101  setupEventHandlers() {
    102    this.elements.dialogCloseButton.addEventListener("click", () => {
    103      this.close(true);
    104    });
    105    // This is needed because a simple <a> element on the page run afoul
    106    // of the "Content windows may never have chrome windows as their openers"
    107    // error, so we use openTrustedLinkIn instead."
    108    this.elements.moreInfoLink.addEventListener("click", () => {
    109      this.onLearnMoreLink();
    110    });
    111    this.elements.learnMoreLink.addEventListener("click", () => {
    112      this.onLearnMoreLink();
    113    });
    114    this.elements.allowButton.addEventListener("click", () => {
    115      this.onAllow();
    116    });
    117    this.elements.blockButton.addEventListener("click", () => {
    118      this.onBlock();
    119    });
    120    // If the user has filled in their email, but not checked the report checkbox,
    121    // we automatically check both report checkboxes when the email input loses focus.
    122    this.elements.emailInput.addEventListener("change", e => {
    123      const hasEmail = this.elements.emailInput.value.trim() !== "";
    124      if (!hasEmail) {
    125        return;
    126      }
    127 
    128      // If the user has typed in the email field, and clicks the (unchecked)
    129      // email checkbox, on blur we would set the email checkbox to checked,
    130      // then the click event would toggle it back to unchecked. So we need to
    131      // defer the check to the next event loop tick.
    132      setTimeout(() => {
    133        this.console?.warn(`Rechecking checkboxes`);
    134        if (this.elements.emailInput.value.trim()) {
    135          this.elements.emailCheckbox.checked = true;
    136          this.elements.reportCheckbox.checked = true;
    137        }
    138      }, 0);
    139 
    140      // The email input field is _inside_ the email checkbox, so we need to
    141      // stop the click event from propagating to the checkbox
    142      e.stopPropagation();
    143    });
    144    // If the user unchecks the report email checkbox, clear the email field
    145    // This is a little complicated because
    146    this.elements.emailCheckbox.addEventListener("change", () => {
    147      if (!this.elements.emailCheckbox.checked) {
    148        this.elements.emailInput.value = "";
    149      }
    150    });
    151    // If the user unchecks the report checkbox, clear the email field
    152    this.elements.reportCheckbox.addEventListener("change", () => {
    153      if (!this.elements.reportCheckbox.checked) {
    154        this.elements.emailCheckbox.checked = false;
    155        this.elements.emailInput.value = "";
    156      }
    157    });
    158  }
    159 
    160  setupAllowLayout() {
    161    this.elements.unexpectedScriptLoadDetail1.setAttribute(
    162      "data-l10n-id",
    163      "unexpected-script-load-detail-1-allow"
    164    );
    165    this.elements.allowButton.setAttribute("type", "primary");
    166    this.elements.blockButton.setAttribute("type", "");
    167  }
    168 
    169  setupBlockLayout(uploadEnabled) {
    170    this.elements.unexpectedScriptLoadDetail1.setAttribute(
    171      "data-l10n-id",
    172      "unexpected-script-load-detail-1-block"
    173    );
    174    this.elements.reportCheckbox.checked = uploadEnabled;
    175    this.elements.allowButton.setAttribute("type", "");
    176    this.elements.blockButton.setAttribute("type", "primary");
    177  }
    178 
    179  /**
    180   * Hide the pop up (for event handlers).
    181   *
    182   * @param {boolean} userDismissed
    183   */
    184  close(userDismissed) {
    185    this.console?.log("UnexpectedScriptLoadPanel is closing");
    186    if (userDismissed) {
    187      Glean.unexpectedScriptLoad.dialogDismissed.record();
    188    }
    189    window.close();
    190    GleanPings.unexpectedScriptLoad.submit();
    191  }
    192 
    193  /*
    194   * Handler for clicking the learn more link from linked text
    195   * within the translations panel.
    196   */
    197  onLearnMoreLink() {
    198    Glean.unexpectedScriptLoad.moreInfoOpened.record();
    199    this.close(false);
    200 
    201    // This is an ugly hack.
    202    // If a modal is open, we will not focus the tab we are opening, even if we ask to
    203    //  ref: https://searchfox.org/mozilla-central/rev/fcb776c1d580000af961677f6df3aeef67168a6f/browser/components/tabbrowser/content/tabbrowser.js#438
    204    // However we do not remove the window-modal-open until _after_ the dialog is closed
    205    // which is after we open the tab.
    206    //  ref: https://searchfox.org/mozilla-central/rev/fcb776c1d580000af961677f6df3aeef67168a6f/browser/base/content/browser.js#5180
    207    window.top.document.documentElement.removeAttribute("window-modal-open");
    208 
    209    window.browsingContext.top.window.openTrustedLinkIn(
    210      "https://support.mozilla.org/kb/unexpected-script-load",
    211      "tab"
    212    );
    213  }
    214 
    215  maybeReport() {
    216    if (this.elements.reportCheckbox.checked) {
    217      let extra = {
    218        script_url: this.#scriptName,
    219      };
    220 
    221      if (this.elements.emailCheckbox.checked) {
    222        extra.user_email = this.elements.emailInput.value.trim();
    223      }
    224 
    225      Glean.unexpectedScriptLoad.scriptReported.record(extra);
    226    }
    227  }
    228 
    229  onBlock() {
    230    this.console?.log("UnexpectedScriptLoadPanel.onBlock() called");
    231    Glean.unexpectedScriptLoad.scriptBlocked.record();
    232    this.maybeReport();
    233 
    234    Services.prefs.setBoolPref(
    235      "security.block_parent_unrestricted_js_loads.temporary",
    236      true
    237    );
    238 
    239    window.browsingContext.top.window.gNotificationBox
    240      .getNotificationWithValue("unexpected-script-notification")
    241      ?.close();
    242 
    243    Services.obs.notifyObservers(
    244      null,
    245      "UnexpectedJavaScriptLoad-UserTookAction"
    246    );
    247 
    248    this.close(false);
    249  }
    250 
    251  onAllow() {
    252    this.console?.log("UnexpectedScriptLoadPanel.onAllow() called");
    253    Glean.unexpectedScriptLoad.scriptAllowed.record();
    254    this.maybeReport();
    255 
    256    Services.prefs.setBoolPref(
    257      "security.allow_parent_unrestricted_js_loads",
    258      true
    259    );
    260 
    261    window.browsingContext.top.window.gNotificationBox
    262      .getNotificationWithValue("unexpected-script-notification")
    263      ?.close();
    264 
    265    Services.obs.notifyObservers(
    266      null,
    267      "UnexpectedJavaScriptLoad-UserTookAction"
    268    );
    269 
    270    this.close(false);
    271  }
    272 })();
    273 
    274 // Call the init method when the script loads
    275 UnexpectedScriptLoadPanel.init();