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();