UnexpectedScriptObserver.sys.mjs (5499B)
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 https://mozilla.org/MPL/2.0/. */ 4 5 const lazy = {}; 6 7 ChromeUtils.defineESModuleGetters(lazy, { 8 EveryWindow: "resource:///modules/EveryWindow.sys.mjs", 9 }); 10 11 const NOTIFICATION_VALUE = "unexpected-script-notification"; 12 13 export let UnexpectedScriptObserver = { 14 // EveryWindow has a race condition in early startup where a callback may be called twice 15 // for the same window. This WeakSet is used to track which windows have already been 16 // handled to avoid showing the notification bar multiple times. See Bug 1982859 17 _windowsHandled: new WeakSet(), 18 19 _notificationHasBeenShown: false, 20 21 async observe(aSubject, aTopic, aScriptName) { 22 if ( 23 aTopic != "UnexpectedJavaScriptLoad-Live" && 24 aTopic != "UnexpectedJavaScriptLoad-ResetNotification" && 25 aTopic != "UnexpectedJavaScriptLoad-UserTookAction" 26 ) { 27 return; 28 } 29 30 if (aTopic == "UnexpectedJavaScriptLoad-ResetNotification") { 31 this._notificationHasBeenShown = false; 32 this._windowsHandled = new WeakSet(); 33 return; 34 } 35 36 if (aTopic == "UnexpectedJavaScriptLoad-UserTookAction") { 37 lazy.EveryWindow.unregisterCallback("UnexpectedScriptLoadPanel"); 38 return; 39 } 40 41 if ( 42 Services.prefs.getBoolPref( 43 "security.hide_parent_unrestricted_js_loads_warning.temporary", 44 false 45 ) 46 ) { 47 return; 48 } 49 50 if (this._notificationHasBeenShown) { 51 // There is already a notification bar, or we showed one in the past and we 52 // won't show it again until browser restart 53 return; 54 } 55 56 GleanPings.unexpectedScriptLoad.setEnabled(true); 57 58 let scriptOrigin; 59 try { 60 let scriptUrl = new URL(aScriptName); 61 scriptOrigin = scriptUrl.hostname; 62 63 if (scriptUrl.protocol != "http:" && scriptUrl.protocol != "https:") { 64 // For this dialog, we only care about loading scripts from web origins 65 return; 66 } 67 } catch (e) { 68 console.error("Invalid scriptName URL:", aScriptName); 69 // For this dialog, we only care about loading scripts from web origins, 70 // so if we couldn't parse it, just exit. 71 return; 72 } 73 74 lazy.EveryWindow.registerCallback( 75 "UnexpectedScriptLoadPanel", 76 async window => { 77 if (this._windowsHandled.has(window)) { 78 return; 79 } 80 this._windowsHandled.add(window); 81 82 let MozXULElement = window.MozXULElement; 83 let document = window.document; 84 85 MozXULElement.insertFTLIfNeeded("browser/unexpectedScript.ftl"); 86 let messageFragment = document.createDocumentFragment(); 87 let message = document.createElement("span"); 88 document.l10n.setAttributes(message, "unexpected-script-load-message", { 89 origin: scriptOrigin, 90 }); 91 messageFragment.appendChild(message); 92 93 // ---------------------------------------------------------------- 94 let openWindow = action => { 95 let args = { 96 action, 97 scriptName: aScriptName, 98 }; 99 window.gDialogBox.open( 100 "chrome://browser/content/security/unexpectedScriptLoad.xhtml", 101 args 102 ); 103 }; 104 105 // ---------------------------------------------------------------- 106 let buttons = [ 107 { 108 supportPage: "unexpected-script-load", 109 callback: () => { 110 Glean.unexpectedScriptLoad.moreInfoOpened.record(); 111 }, 112 }, 113 ]; 114 buttons.push({ 115 "l10n-id": "unexpected-script-load-message-button-allow", 116 callback: () => { 117 openWindow("allow"); 118 return true; 119 }, 120 }); 121 buttons.push({ 122 "l10n-id": "unexpected-script-load-message-button-block", 123 callback: () => { 124 openWindow("block"); 125 return true; // Do not close the dialog bar until the user has done so explcitly or taken an action 126 }, 127 }); 128 129 let notificationBox = window.gNotificationBox; 130 131 if (!notificationBox.getNotificationWithValue(NOTIFICATION_VALUE)) { 132 await notificationBox.appendNotification( 133 NOTIFICATION_VALUE, 134 { 135 label: messageFragment, 136 priority: notificationBox.PRIORITY_WARNING_HIGH, 137 eventCallback: event => { 138 if (event == "dismissed") { 139 Glean.unexpectedScriptLoad.infobarDismissed.record(); 140 GleanPings.unexpectedScriptLoad.submit(); 141 } 142 }, 143 }, 144 buttons 145 ); 146 } 147 }, 148 // Unregister Callback: 149 // This is needed to remove the notification bar from all the windows 150 // once the user has taken action on one of them, and ensure any open 151 // dialog box is also closed. 152 async window => { 153 window.gNotificationBox 154 .getNotificationWithValue(NOTIFICATION_VALUE) 155 ?.close(); 156 if ( 157 window.gDialogBox?.dialog?._openedURL == 158 "chrome://browser/content/security/unexpectedScriptLoad.xhtml" 159 ) { 160 window.gDialogBox.dialog.close(); 161 } 162 GleanPings.unexpectedScriptLoad.submit(); 163 } 164 ); 165 166 Glean.unexpectedScriptLoad.infobarShown.record(); 167 168 this._notificationHasBeenShown = true; 169 }, 170 };