/* - This Source Code Form is subject to the terms of the Mozilla Public - License, v. 2.0. If a copy of the MPL was not distributed with this file, - You can obtain one at http://mozilla.org/MPL/2.0/. */ /* import-globals-from preferences.js */ "use strict"; var { XPCOMUtils } = ChromeUtils.importESModule( "resource://gre/modules/XPCOMUtils.sys.mjs" ); // Note: we get loaded in dialogs so we need to define our // own getters, separate from preferences.js . ChromeUtils.defineESModuleGetters(this, { AddonManager: "resource://gre/modules/AddonManager.sys.mjs", BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs", ExtensionPreferencesManager: "resource://gre/modules/ExtensionPreferencesManager.sys.mjs", ExtensionSettingsStore: "resource://gre/modules/ExtensionSettingsStore.sys.mjs", Management: "resource://gre/modules/Extension.sys.mjs", }); const PREF_SETTING_TYPE = "prefs"; const PROXY_KEY = "proxy.settings"; const API_PROXY_PREFS = [ "network.proxy.type", "network.proxy.http", "network.proxy.http_port", "network.proxy.share_proxy_settings", "network.proxy.ssl", "network.proxy.ssl_port", "network.proxy.socks", "network.proxy.socks_port", "network.proxy.socks_version", "network.proxy.socks_remote_dns", "network.proxy.no_proxies_on", "network.proxy.autoconfig_url", "signon.autologin.proxy", ]; let extensionControlledContentIds = { "privacy.containers": "browserContainersExtensionContent", webNotificationsDisabled: "browserNotificationsPermissionExtensionContent", "services.passwordSavingEnabled": "passwordManagerExtensionContent", "proxy.settings": "proxyExtensionContent", get "websites.trackingProtectionMode"() { return { button: "contentBlockingDisableTrackingProtectionExtension", section: "contentBlockingTrackingProtectionExtensionContentLabel", }; }, }; const extensionControlledL10nKeys = { webNotificationsDisabled: "web-notifications", "services.passwordSavingEnabled": "password-saving", "privacy.containers": "privacy-containers", "websites.trackingProtectionMode": "websites-content-blocking-all-trackers", "proxy.settings": "proxy-config", }; let extensionControlledIds = {}; /** * Check if a pref is being managed by an extension. */ async function getControllingExtensionInfo(type, settingName) { await ExtensionSettingsStore.initialize(); return ExtensionSettingsStore.getSetting(type, settingName); } function getControllingExtensionEls(settingName) { let idInfo = extensionControlledContentIds[settingName]; let section = document.getElementById(idInfo.section || idInfo); let button = idInfo.button ? document.getElementById(idInfo.button) : section.querySelector("button"); return { section, button, description: section.querySelector("description"), }; } async function getControllingExtension(type, settingName) { let info = await getControllingExtensionInfo(type, settingName); let addon = info && info.id && (await AddonManager.getAddonByID(info.id)); return addon; } async function handleControllingExtension(type, settingName) { let addon = await getControllingExtension(type, settingName); // Sometimes the ExtensionSettingsStore gets in a bad state where it thinks // an extension is controlling a setting but the extension has been uninstalled // outside of the regular lifecycle. If the extension isn't currently installed // then we should treat the setting as not being controlled. // See https://bugzilla.mozilla.org/show_bug.cgi?id=1411046 for an example. if (addon) { extensionControlledIds[settingName] = addon.id; showControllingExtension(settingName, addon); } else { let elements = getControllingExtensionEls(settingName); if ( extensionControlledIds[settingName] && !document.hidden && elements.button ) { showEnableExtensionMessage(settingName); } else { hideControllingExtension(settingName); } delete extensionControlledIds[settingName]; } return !!addon; } function settingNameToL10nID(settingName) { if (!extensionControlledL10nKeys.hasOwnProperty(settingName)) { throw new Error( `Unknown extension controlled setting name: ${settingName}` ); } return `extension-controlling-${extensionControlledL10nKeys[settingName]}`; } /** * Set the localization data for the description of the controlling extension. * * The function alters the inner DOM structure of the fragment to, depending * on the `addon` argument, remove the `` element or ensure it's * set to the correct src. * This allows Fluent DOM Overlays to localize the fragment. * * @param elem {Element} * element to be annotated * @param addon {Object?} * Addon object with meta information about the addon (or null) * @param settingName {String} * If `addon` is set this handled the name of the setting that will be used * to fetch the l10n id for the given message. * If `addon` is set to null, this will be the full l10n-id assigned to the * element. */ function setControllingExtensionDescription(elem, addon, settingName) { const existingImg = elem.querySelector("img"); if (addon === null) { // If the element has an image child element, // remove it. if (existingImg) { existingImg.remove(); } document.l10n.setAttributes(elem, settingName); return; } const defaultIcon = "chrome://mozapps/skin/extensions/extensionGeneric.svg"; const src = addon.iconURL || defaultIcon; if (!existingImg) { // If an element doesn't have an image child // node, add it. let image = document.createElementNS("http://www.w3.org/1999/xhtml", "img"); image.setAttribute("src", src); image.setAttribute("data-l10n-name", "icon"); image.setAttribute("role", "presentation"); image.classList.add("extension-controlled-icon"); elem.appendChild(image); } else if (existingImg.getAttribute("src") !== src) { existingImg.setAttribute("src", src); } const l10nId = settingNameToL10nID(settingName); document.l10n.setAttributes(elem, l10nId, { name: addon.name, }); } async function showControllingExtension(settingName, addon) { // Tell the user what extension is controlling the setting. let elements = getControllingExtensionEls(settingName); elements.section.classList.remove("extension-controlled-disabled"); let description = elements.description; setControllingExtensionDescription(description, addon, settingName); if (elements.button) { elements.button.hidden = false; } // Show the controlling extension row and hide the old label. elements.section.hidden = false; } function hideControllingExtension(settingName) { let elements = getControllingExtensionEls(settingName); elements.section.hidden = true; if (elements.button) { elements.button.hidden = true; } } function showEnableExtensionMessage(settingName) { let elements = getControllingExtensionEls(settingName); elements.button.hidden = true; elements.section.classList.add("extension-controlled-disabled"); elements.description.textContent = ""; // We replace localization of the with a DOM Fragment containing // the enable-extension-enable message. That means a change from: // // // // to: // // // // // // We need to remove the l10n-id annotation from the to prevent // Fluent from overwriting the element in case of any retranslation. elements.description.removeAttribute("data-l10n-id"); let icon = (url, name) => { let img = document.createElementNS("http://www.w3.org/1999/xhtml", "img"); img.src = url; img.setAttribute("data-l10n-name", name); img.setAttribute("role", "presentation"); img.className = "extension-controlled-icon"; return img; }; let label = document.createXULElement("label"); let addonIcon = icon( "chrome://mozapps/skin/extensions/extensionGeneric.svg", "addons-icon" ); let toolbarIcon = icon("chrome://browser/skin/menu.svg", "menu-icon"); label.appendChild(addonIcon); label.appendChild(toolbarIcon); document.l10n.setAttributes(label, "extension-controlled-enable"); elements.description.appendChild(label); let dismissButton = document.createXULElement("image"); dismissButton.setAttribute("class", "extension-controlled-icon close-icon"); dismissButton.addEventListener("click", function dismissHandler() { hideControllingExtension(settingName); dismissButton.removeEventListener("click", dismissHandler); }); elements.description.appendChild(dismissButton); } function makeDisableControllingExtension(type, settingName) { return async function disableExtension() { let { id } = await getControllingExtensionInfo(type, settingName); let addon = await AddonManager.getAddonByID(id); await addon.disable(); }; } /** * Initialize listeners though the Management API to update the UI * when an extension is controlling a pref. * @param {string} type * @param {string} prefId The unique id of the setting * @param {HTMLElement} controlledElement */ async function initListenersForPrefChange(type, prefId, controlledElement) { await Management.asyncLoadSettingsModules(); let managementObserver = async () => { let managementControlled = await handleControllingExtension(type, prefId); // Enterprise policy may have locked the pref, so we need to preserve that controlledElement.disabled = managementControlled || Services.prefs.prefIsLocked(prefId); }; managementObserver(); Management.on(`extension-setting-changed:${prefId}`, managementObserver); window.addEventListener("unload", () => { Management.off(`extension-setting-changed:${prefId}`, managementObserver); }); } function initializeProxyUI(container) { let deferredUpdate = new DeferredTask(() => { container.updateProxySettingsUI(); }, 10); let proxyObserver = { observe: (subject, topic, data) => { if (API_PROXY_PREFS.includes(data)) { deferredUpdate.arm(); } }, }; Services.prefs.addObserver("", proxyObserver); window.addEventListener("unload", () => { Services.prefs.removeObserver("", proxyObserver); }); }