extensionControlled.js (10544B)
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 file, 3 - You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 /* import-globals-from preferences.js */ 6 7 "use strict"; 8 9 var { XPCOMUtils } = ChromeUtils.importESModule( 10 "resource://gre/modules/XPCOMUtils.sys.mjs" 11 ); 12 13 // Note: we get loaded in dialogs so we need to define our 14 // own getters, separate from preferences.js . 15 ChromeUtils.defineESModuleGetters(this, { 16 AddonManager: "resource://gre/modules/AddonManager.sys.mjs", 17 BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", 18 DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs", 19 20 ExtensionPreferencesManager: 21 "resource://gre/modules/ExtensionPreferencesManager.sys.mjs", 22 23 ExtensionSettingsStore: 24 "resource://gre/modules/ExtensionSettingsStore.sys.mjs", 25 26 Management: "resource://gre/modules/Extension.sys.mjs", 27 }); 28 29 const PREF_SETTING_TYPE = "prefs"; 30 const PROXY_KEY = "proxy.settings"; 31 const API_PROXY_PREFS = [ 32 "network.proxy.type", 33 "network.proxy.http", 34 "network.proxy.http_port", 35 "network.proxy.share_proxy_settings", 36 "network.proxy.ssl", 37 "network.proxy.ssl_port", 38 "network.proxy.socks", 39 "network.proxy.socks_port", 40 "network.proxy.socks_version", 41 "network.proxy.socks_remote_dns", 42 "network.proxy.socks5_remote_dns", 43 "network.proxy.no_proxies_on", 44 "network.proxy.autoconfig_url", 45 "signon.autologin.proxy", 46 ]; 47 48 let extensionControlledContentIds = { 49 webNotificationsDisabled: "browserNotificationsPermissionExtensionContent", 50 "services.passwordSavingEnabled": "passwordManagerExtensionContent", 51 "proxy.settings": "proxyExtensionContent", 52 get "websites.trackingProtectionMode"() { 53 return { 54 button: "contentBlockingDisableTrackingProtectionExtension", 55 section: "contentBlockingTrackingProtectionExtensionContentLabel", 56 }; 57 }, 58 }; 59 60 const extensionControlledL10nKeys = { 61 webNotificationsDisabled: "extension-controlling-web-notifications", 62 "services.passwordSavingEnabled": "extension-controlling-password-saving", 63 "privacy.containers": "extension-controlling-privacy-containers", 64 "websites.trackingProtectionMode": 65 "extension-controlling-websites-content-blocking-all-trackers", 66 "proxy.settings": "extension-controlling-proxy-config", 67 }; 68 69 let extensionControlledIds = {}; 70 71 /** 72 * Check if a pref is being managed by an extension. 73 */ 74 async function getControllingExtensionInfo(type, settingName) { 75 await ExtensionSettingsStore.initialize(); 76 return ExtensionSettingsStore.getSetting(type, settingName); 77 } 78 79 function getControllingExtensionEls(settingName) { 80 let idInfo = extensionControlledContentIds[settingName]; 81 let section = document.getElementById(idInfo.section || idInfo); 82 let button = idInfo.button 83 ? document.getElementById(idInfo.button) 84 : section.querySelector("button"); 85 return { 86 section, 87 button, 88 description: section.querySelector("description"), 89 }; 90 } 91 92 async function getControllingExtension(type, settingName) { 93 let info = await getControllingExtensionInfo(type, settingName); 94 let addon = info && info.id && (await AddonManager.getAddonByID(info.id)); 95 return addon; 96 } 97 98 async function handleControllingExtension(type, settingName) { 99 let addon = await getControllingExtension(type, settingName); 100 101 // Sometimes the ExtensionSettingsStore gets in a bad state where it thinks 102 // an extension is controlling a setting but the extension has been uninstalled 103 // outside of the regular lifecycle. If the extension isn't currently installed 104 // then we should treat the setting as not being controlled. 105 // See https://bugzilla.mozilla.org/show_bug.cgi?id=1411046 for an example. 106 if (addon) { 107 extensionControlledIds[settingName] = addon.id; 108 showControllingExtension(settingName, addon); 109 } else { 110 let elements = getControllingExtensionEls(settingName); 111 if ( 112 extensionControlledIds[settingName] && 113 !document.hidden && 114 elements.button 115 ) { 116 showEnableExtensionMessage(settingName); 117 } else { 118 hideControllingExtension(settingName); 119 } 120 delete extensionControlledIds[settingName]; 121 } 122 123 return !!addon; 124 } 125 126 function settingNameToL10nID(settingName) { 127 if (!extensionControlledL10nKeys.hasOwnProperty(settingName)) { 128 throw new Error( 129 `Unknown extension controlled setting name: ${settingName}` 130 ); 131 } 132 return extensionControlledL10nKeys[settingName]; 133 } 134 135 /** 136 * Set the localization data for the description of the controlling extension. 137 * 138 * The function alters the inner DOM structure of the fragment to, depending 139 * on the `addon` argument, remove the `<img/>` element or ensure it's 140 * set to the correct src. 141 * This allows Fluent DOM Overlays to localize the fragment. 142 * 143 * @param elem {Element} 144 * <description> element to be annotated 145 * @param addon {Object?} 146 * Addon object with meta information about the addon (or null) 147 * @param settingName {String} 148 * If `addon` is set this handled the name of the setting that will be used 149 * to fetch the l10n id for the given message. 150 * If `addon` is set to null, this will be the full l10n-id assigned to the 151 * element. 152 */ 153 function setControllingExtensionDescription(elem, addon, settingName) { 154 const existingImg = elem.querySelector("img"); 155 if (addon === null) { 156 // If the element has an image child element, 157 // remove it. 158 if (existingImg) { 159 existingImg.remove(); 160 } 161 document.l10n.setAttributes(elem, settingName); 162 return; 163 } 164 165 const defaultIcon = "chrome://mozapps/skin/extensions/extensionGeneric.svg"; 166 const src = addon.iconURL || defaultIcon; 167 168 if (!existingImg) { 169 // If an element doesn't have an image child 170 // node, add it. 171 let image = document.createElementNS("http://www.w3.org/1999/xhtml", "img"); 172 image.setAttribute("src", src); 173 image.setAttribute("data-l10n-name", "icon"); 174 image.setAttribute("role", "presentation"); 175 image.classList.add("extension-controlled-icon"); 176 elem.appendChild(image); 177 } else if (existingImg.getAttribute("src") !== src) { 178 existingImg.setAttribute("src", src); 179 } 180 181 const l10nId = settingNameToL10nID(settingName); 182 document.l10n.setAttributes(elem, l10nId, { 183 name: addon.name, 184 }); 185 } 186 187 async function showControllingExtension(settingName, addon) { 188 // Tell the user what extension is controlling the setting. 189 let elements = getControllingExtensionEls(settingName); 190 191 elements.section.classList.remove("extension-controlled-disabled"); 192 let description = elements.description; 193 194 setControllingExtensionDescription(description, addon, settingName); 195 196 if (elements.button) { 197 elements.button.hidden = false; 198 } 199 200 // Show the controlling extension row and hide the old label. 201 elements.section.hidden = false; 202 } 203 204 function hideControllingExtension(settingName) { 205 let elements = getControllingExtensionEls(settingName); 206 elements.section.hidden = true; 207 if (elements.button) { 208 elements.button.hidden = true; 209 } 210 } 211 212 function showEnableExtensionMessage(settingName) { 213 let elements = getControllingExtensionEls(settingName); 214 215 elements.button.hidden = true; 216 elements.section.classList.add("extension-controlled-disabled"); 217 218 elements.description.textContent = ""; 219 220 // We replace localization of the <description> with a DOM Fragment containing 221 // the enable-extension-enable message. That means a change from: 222 // 223 // <description data-l10n-id="..."/> 224 // 225 // to: 226 // 227 // <description> 228 // <img/> 229 // <label data-l10n-id="..."/> 230 // </description> 231 // 232 // We need to remove the l10n-id annotation from the <description> to prevent 233 // Fluent from overwriting the element in case of any retranslation. 234 elements.description.removeAttribute("data-l10n-id"); 235 236 let icon = (url, name) => { 237 let img = document.createElementNS("http://www.w3.org/1999/xhtml", "img"); 238 img.src = url; 239 img.setAttribute("data-l10n-name", name); 240 img.setAttribute("role", "presentation"); 241 img.className = "extension-controlled-icon"; 242 return img; 243 }; 244 let label = document.createXULElement("label"); 245 let addonIcon = icon( 246 "chrome://mozapps/skin/extensions/extensionGeneric.svg", 247 "addons-icon" 248 ); 249 let toolbarIcon = icon("chrome://browser/skin/menu.svg", "menu-icon"); 250 label.appendChild(addonIcon); 251 label.appendChild(toolbarIcon); 252 document.l10n.setAttributes(label, "extension-controlled-enable"); 253 elements.description.appendChild(label); 254 let dismissButton = document.createXULElement("image"); 255 dismissButton.setAttribute("class", "extension-controlled-icon close-icon"); 256 dismissButton.addEventListener("click", function dismissHandler() { 257 hideControllingExtension(settingName); 258 dismissButton.removeEventListener("click", dismissHandler); 259 }); 260 elements.description.appendChild(dismissButton); 261 } 262 263 function makeDisableControllingExtension(type, settingName) { 264 return async function disableExtension() { 265 let { id } = await getControllingExtensionInfo(type, settingName); 266 let addon = await AddonManager.getAddonByID(id); 267 await addon.disable(); 268 }; 269 } 270 271 /** 272 * Initialize listeners though the Management API to update the UI 273 * when an extension is controlling a pref. 274 * 275 * @param {string} type 276 * @param {string} prefId The unique id of the setting 277 * @param {HTMLElement} controlledElement 278 */ 279 async function initListenersForPrefChange(type, prefId, controlledElement) { 280 await Management.asyncLoadSettingsModules(); 281 282 let managementObserver = async () => { 283 let managementControlled = await handleControllingExtension(type, prefId); 284 // Enterprise policy may have locked the pref, so we need to preserve that 285 controlledElement.disabled = 286 managementControlled || Services.prefs.prefIsLocked(prefId); 287 }; 288 managementObserver(); 289 Management.on(`extension-setting-changed:${prefId}`, managementObserver); 290 291 window.addEventListener("unload", () => { 292 Management.off(`extension-setting-changed:${prefId}`, managementObserver); 293 }); 294 } 295 296 function initializeProxyUI(container) { 297 let deferredUpdate = new DeferredTask(() => { 298 container.updateProxySettingsUI(); 299 }, 10); 300 let proxyObserver = { 301 observe: (subject, topic, data) => { 302 if (API_PROXY_PREFS.includes(data)) { 303 deferredUpdate.arm(); 304 } 305 }, 306 }; 307 Services.prefs.addObserver("", proxyObserver); 308 window.addEventListener("unload", () => { 309 Services.prefs.removeObserver("", proxyObserver); 310 }); 311 }