diff options
Diffstat (limited to 'browser/components/enterprisepolicies/Policies.sys.mjs')
-rw-r--r-- | browser/components/enterprisepolicies/Policies.sys.mjs | 2885 |
1 files changed, 2885 insertions, 0 deletions
diff --git a/browser/components/enterprisepolicies/Policies.sys.mjs b/browser/components/enterprisepolicies/Policies.sys.mjs new file mode 100644 index 0000000000..24f2f9d516 --- /dev/null +++ b/browser/components/enterprisepolicies/Policies.sys.mjs @@ -0,0 +1,2885 @@ +/* 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 { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; + +const lazy = {}; + +XPCOMUtils.defineLazyServiceGetters(lazy, { + gCertDB: ["@mozilla.org/security/x509certdb;1", "nsIX509CertDB"], + gExternalProtocolService: [ + "@mozilla.org/uriloader/external-protocol-service;1", + "nsIExternalProtocolService", + ], + gHandlerService: [ + "@mozilla.org/uriloader/handler-service;1", + "nsIHandlerService", + ], + gMIMEService: ["@mozilla.org/mime;1", "nsIMIMEService"], +}); + +ChromeUtils.defineESModuleGetters(lazy, { + AddonManager: "resource://gre/modules/AddonManager.sys.mjs", + BookmarksPolicies: "resource:///modules/policies/BookmarksPolicies.sys.mjs", + CustomizableUI: "resource:///modules/CustomizableUI.sys.mjs", + FileUtils: "resource://gre/modules/FileUtils.sys.mjs", + PdfJsDefaultPreferences: "resource://pdf.js/PdfJsDefaultPreferences.sys.mjs", + ProxyPolicies: "resource:///modules/policies/ProxyPolicies.sys.mjs", + UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs", + WebsiteFilter: "resource:///modules/policies/WebsiteFilter.sys.mjs", +}); + +const PREF_LOGLEVEL = "browser.policies.loglevel"; +const BROWSER_DOCUMENT_URL = AppConstants.BROWSER_CHROME_URL; +const ABOUT_CONTRACT = "@mozilla.org/network/protocol/about;1?what="; + +const isXpcshell = Services.env.exists("XPCSHELL_TEST_PROFILE_DIR"); + +ChromeUtils.defineLazyGetter(lazy, "log", () => { + let { ConsoleAPI } = ChromeUtils.importESModule( + "resource://gre/modules/Console.sys.mjs" + ); + return new ConsoleAPI({ + prefix: "Policies.jsm", + // tip: set maxLogLevel to "debug" and use log.debug() to create detailed + // messages during development. See LOG_LEVELS in Console.sys.mjs for details. + maxLogLevel: "error", + maxLogLevelPref: PREF_LOGLEVEL, + }); +}); + +/* + * ============================ + * = POLICIES IMPLEMENTATIONS = + * ============================ + * + * The Policies object below is where the implementation for each policy + * happens. An object for each policy should be defined, containing + * callback functions that will be called by the engine. + * + * See the _callbacks object in EnterprisePolicies.js for the list of + * possible callbacks and an explanation of each. + * + * Each callback will be called with two parameters: + * - manager + * This is the EnterprisePoliciesManager singleton object from + * EnterprisePolicies.js + * + * - param + * The parameter defined for this policy in policies-schema.json. + * It will be different for each policy. It could be a boolean, + * a string, an array or a complex object. All parameters have + * been validated according to the schema, and no unknown + * properties will be present on them. + * + * The callbacks will be bound to their parent policy object. + */ +export var Policies = { + // Used for cleaning up policies. + // Use the same timing that you used for setting up the policy. + _cleanup: { + onBeforeAddons(manager) { + if (Cu.isInAutomation || isXpcshell) { + console.log("_cleanup from onBeforeAddons"); + clearBlockedAboutPages(); + } + }, + onProfileAfterChange(manager) { + if (Cu.isInAutomation || isXpcshell) { + console.log("_cleanup from onProfileAfterChange"); + } + }, + onBeforeUIStartup(manager) { + if (Cu.isInAutomation || isXpcshell) { + console.log("_cleanup from onBeforeUIStartup"); + } + }, + onAllWindowsRestored(manager) { + if (Cu.isInAutomation || isXpcshell) { + console.log("_cleanup from onAllWindowsRestored"); + } + }, + }, + + "3rdparty": { + onBeforeAddons(manager, param) { + manager.setExtensionPolicies(param.Extensions); + }, + }, + + AllowedDomainsForApps: { + onBeforeAddons(manager, param) { + Services.obs.addObserver(function (subject, topic, data) { + let channel = subject.QueryInterface(Ci.nsIHttpChannel); + if (channel.URI.host.endsWith(".google.com")) { + channel.setRequestHeader("X-GoogApps-Allowed-Domains", param, true); + } + }, "http-on-modify-request"); + }, + }, + + AllowFileSelectionDialogs: { + onBeforeUIStartup(manager, param) { + if (!param) { + setAndLockPref("widget.disable_file_pickers", true); + setAndLockPref("browser.download.useDownloadDir", true); + manager.disallowFeature("filepickers"); + } + }, + }, + + AppAutoUpdate: { + onBeforeUIStartup(manager, param) { + // Logic feels a bit reversed here, but it's correct. If AppAutoUpdate is + // true, we disallow turning off auto updating, and visa versa. + if (param) { + manager.disallowFeature("app-auto-updates-off"); + } else { + manager.disallowFeature("app-auto-updates-on"); + } + }, + }, + + AppUpdatePin: { + validate(param) { + // This is the version when pinning was introduced. Attempting to set a + // pin before this will not work, because Balrog's pinning table will + // never have the necessary entry. + const earliestPinMajorVersion = 102; + const earliestPinMinorVersion = 0; + + let pinParts = param.split("."); + + if (pinParts.length < 2) { + lazy.log.error("AppUpdatePin has too few dots."); + return false; + } + if (pinParts.length > 3) { + lazy.log.error("AppUpdatePin has too many dots."); + return false; + } + + const trailingPinPart = pinParts.pop(); + if (trailingPinPart != "") { + lazy.log.error("AppUpdatePin does not end with a trailing dot."); + return false; + } + + const pinMajorVersionStr = pinParts.shift(); + if (!pinMajorVersionStr.length) { + lazy.log.error("AppUpdatePin's major version is empty."); + return false; + } + if (!/^\d+$/.test(pinMajorVersionStr)) { + lazy.log.error( + "AppUpdatePin's major version contains a non-numeric character." + ); + return false; + } + if (/^0/.test(pinMajorVersionStr)) { + lazy.log.error("AppUpdatePin's major version contains a leading 0."); + return false; + } + const pinMajorVersionInt = parseInt(pinMajorVersionStr, 10); + if (isNaN(pinMajorVersionInt)) { + lazy.log.error( + "AppUpdatePin's major version could not be parsed to an integer." + ); + return false; + } + if (pinMajorVersionInt < earliestPinMajorVersion) { + lazy.log.error( + `AppUpdatePin must not be earlier than '${earliestPinMajorVersion}.${earliestPinMinorVersion}.'.` + ); + return false; + } + + if (pinParts.length) { + const pinMinorVersionStr = pinParts.shift(); + if (!pinMinorVersionStr.length) { + lazy.log.error("AppUpdatePin's minor version is empty."); + return false; + } + if (!/^\d+$/.test(pinMinorVersionStr)) { + lazy.log.error( + "AppUpdatePin's minor version contains a non-numeric character." + ); + return false; + } + if (/^0\d/.test(pinMinorVersionStr)) { + lazy.log.error("AppUpdatePin's minor version contains a leading 0."); + return false; + } + const pinMinorVersionInt = parseInt(pinMinorVersionStr, 10); + if (isNaN(pinMinorVersionInt)) { + lazy.log.error( + "AppUpdatePin's minor version could not be parsed to an integer." + ); + return false; + } + if ( + pinMajorVersionInt == earliestPinMajorVersion && + pinMinorVersionInt < earliestPinMinorVersion + ) { + lazy.log.error( + `AppUpdatePin must not be earlier than '${earliestPinMajorVersion}.${earliestPinMinorVersion}.'.` + ); + return false; + } + } + + return true; + }, + // No additional implementation needed here. UpdateService.jsm will check + // for this policy directly when determining the update URL. + }, + + AppUpdateURL: { + // No implementation needed here. UpdateService.jsm will check for this + // policy directly when determining the update URL. + }, + + Authentication: { + onBeforeAddons(manager, param) { + // When Authentication was originally implemented, it was always + // locked, so it defaults to locked. + let locked = true; + if ("Locked" in param) { + locked = param.Locked; + } + if ("SPNEGO" in param) { + PoliciesUtils.setDefaultPref( + "network.negotiate-auth.trusted-uris", + param.SPNEGO.join(", "), + locked + ); + } + if ("Delegated" in param) { + PoliciesUtils.setDefaultPref( + "network.negotiate-auth.delegation-uris", + param.Delegated.join(", "), + locked + ); + } + if ("NTLM" in param) { + PoliciesUtils.setDefaultPref( + "network.automatic-ntlm-auth.trusted-uris", + param.NTLM.join(", "), + locked + ); + } + if ("AllowNonFQDN" in param) { + if ("NTLM" in param.AllowNonFQDN) { + PoliciesUtils.setDefaultPref( + "network.automatic-ntlm-auth.allow-non-fqdn", + param.AllowNonFQDN.NTLM, + locked + ); + } + if ("SPNEGO" in param.AllowNonFQDN) { + PoliciesUtils.setDefaultPref( + "network.negotiate-auth.allow-non-fqdn", + param.AllowNonFQDN.SPNEGO, + locked + ); + } + } + if ("AllowProxies" in param) { + if ("NTLM" in param.AllowProxies) { + PoliciesUtils.setDefaultPref( + "network.automatic-ntlm-auth.allow-proxies", + param.AllowProxies.NTLM, + locked + ); + } + if ("SPNEGO" in param.AllowProxies) { + PoliciesUtils.setDefaultPref( + "network.negotiate-auth.allow-proxies", + param.AllowProxies.SPNEGO, + locked + ); + } + } + if ("PrivateBrowsing" in param) { + PoliciesUtils.setDefaultPref( + "network.auth.private-browsing-sso", + param.PrivateBrowsing, + locked + ); + } + }, + }, + + AutoLaunchProtocolsFromOrigins: { + onBeforeAddons(manager, param) { + for (let info of param) { + addAllowDenyPermissions( + `open-protocol-handler^${info.protocol}`, + info.allowed_origins + ); + } + }, + }, + + BackgroundAppUpdate: { + onBeforeAddons(manager, param) { + if (param) { + manager.disallowFeature("app-background-update-off"); + } else { + manager.disallowFeature("app-background-update-on"); + } + }, + }, + + BlockAboutAddons: { + onBeforeUIStartup(manager, param) { + if (param) { + blockAboutPage(manager, "about:addons", true); + } + }, + }, + + BlockAboutConfig: { + onBeforeUIStartup(manager, param) { + if (param) { + blockAboutPage(manager, "about:config"); + setAndLockPref("devtools.chrome.enabled", false); + } + }, + }, + + BlockAboutProfiles: { + onBeforeUIStartup(manager, param) { + if (param) { + blockAboutPage(manager, "about:profiles"); + } + }, + }, + + BlockAboutSupport: { + onBeforeUIStartup(manager, param) { + if (param) { + blockAboutPage(manager, "about:support"); + manager.disallowFeature("aboutSupport"); + } + }, + }, + + Bookmarks: { + onAllWindowsRestored(manager, param) { + lazy.BookmarksPolicies.processBookmarks(param); + }, + }, + + CaptivePortal: { + onBeforeAddons(manager, param) { + setAndLockPref("network.captive-portal-service.enabled", param); + }, + }, + + Certificates: { + onBeforeAddons(manager, param) { + if ("ImportEnterpriseRoots" in param) { + setAndLockPref( + "security.enterprise_roots.enabled", + param.ImportEnterpriseRoots + ); + } + if ("Install" in param) { + (async () => { + let dirs = []; + let platform = AppConstants.platform; + if (platform == "win") { + dirs = [ + // Ugly, but there is no official way to get %USERNAME\AppData\Roaming\Mozilla. + Services.dirsvc.get("XREUSysExt", Ci.nsIFile).parent, + // Even more ugly, but there is no official way to get %USERNAME\AppData\Local\Mozilla. + Services.dirsvc.get("DefProfLRt", Ci.nsIFile).parent.parent, + ]; + } else if (platform == "macosx" || platform == "linux") { + dirs = [ + // These two keys are named wrong. They return the Mozilla directory. + Services.dirsvc.get("XREUserNativeManifests", Ci.nsIFile), + Services.dirsvc.get("XRESysNativeManifests", Ci.nsIFile), + ]; + } + dirs.unshift(Services.dirsvc.get("XREAppDist", Ci.nsIFile)); + for (let certfilename of param.Install) { + let certfile; + try { + certfile = Cc["@mozilla.org/file/local;1"].createInstance( + Ci.nsIFile + ); + certfile.initWithPath(certfilename); + } catch (e) { + for (let dir of dirs) { + certfile = dir.clone(); + certfile.append( + platform == "linux" ? "certificates" : "Certificates" + ); + certfile.append(certfilename); + if (certfile.exists()) { + break; + } + } + } + let file; + try { + file = await File.createFromNsIFile(certfile); + } catch (e) { + lazy.log.error(`Unable to find certificate - ${certfilename}`); + continue; + } + let reader = new FileReader(); + reader.onloadend = function () { + if (reader.readyState != reader.DONE) { + lazy.log.error(`Unable to read certificate - ${certfile.path}`); + return; + } + let certFile = reader.result; + let certFileArray = []; + for (let i = 0; i < certFile.length; i++) { + certFileArray.push(certFile.charCodeAt(i)); + } + let cert; + try { + cert = lazy.gCertDB.constructX509(certFileArray); + } catch (e) { + lazy.log.debug( + `constructX509 failed with error '${e}' - trying constructX509FromBase64.` + ); + try { + // It might be PEM instead of DER. + cert = lazy.gCertDB.constructX509FromBase64( + pemToBase64(certFile) + ); + } catch (ex) { + lazy.log.error( + `Unable to add certificate - ${certfile.path}`, + ex + ); + } + } + if (cert) { + if ( + lazy.gCertDB.isCertTrusted( + cert, + Ci.nsIX509Cert.CA_CERT, + Ci.nsIX509CertDB.TRUSTED_SSL + ) + ) { + // Certificate is already installed. + return; + } + try { + lazy.gCertDB.addCert(certFile, "CT,CT,"); + } catch (e) { + // It might be PEM instead of DER. + lazy.gCertDB.addCertFromBase64( + pemToBase64(certFile), + "CT,CT," + ); + } + } + }; + reader.readAsBinaryString(file); + } + })(); + } + }, + }, + + Containers: { + // Queried directly by ContextualIdentityService.sys.mjs + }, + + ContentAnalysis: { + onBeforeUIStartup(manager, param) { + if ("Enabled" in param) { + let enabled = !!param.Enabled; + setAndLockPref("browser.contentanalysis.enabled", enabled); + } + }, + }, + + Cookies: { + onBeforeUIStartup(manager, param) { + addAllowDenyPermissions("cookie", param.Allow, param.Block); + + if (param.AllowSession) { + for (let origin of param.AllowSession) { + try { + Services.perms.addFromPrincipal( + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + origin + ), + "cookie", + Ci.nsICookiePermission.ACCESS_SESSION, + Ci.nsIPermissionManager.EXPIRE_POLICY + ); + } catch (ex) { + lazy.log.error( + `Unable to add cookie session permission - ${origin.href}` + ); + } + } + } + + if (param.Block) { + const hosts = param.Block.map(url => url.hostname) + .sort() + .join("\n"); + runOncePerModification("clearCookiesForBlockedHosts", hosts, () => { + for (let blocked of param.Block) { + Services.cookies.removeCookiesWithOriginAttributes( + "{}", + blocked.hostname + ); + } + }); + } + + if (param.ExpireAtSessionEnd != undefined) { + lazy.log.error( + "'ExpireAtSessionEnd' has been deprecated and it has no effect anymore." + ); + } + + // New Cookie Behavior option takes precendence + let defaultPref = Services.prefs.getDefaultBranch(""); + let newCookieBehavior = defaultPref.getIntPref( + "network.cookie.cookieBehavior" + ); + let newCookieBehaviorPB = defaultPref.getIntPref( + "network.cookie.cookieBehavior.pbmode" + ); + if ("Behavior" in param || "BehaviorPrivateBrowsing" in param) { + let behaviors = { + accept: Ci.nsICookieService.BEHAVIOR_ACCEPT, + "reject-foreign": Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + reject: Ci.nsICookieService.BEHAVIOR_REJECT, + "limit-foreign": Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN, + "reject-tracker": Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + "reject-tracker-and-partition-foreign": + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + }; + if ("Behavior" in param) { + newCookieBehavior = behaviors[param.Behavior]; + } + if ("BehaviorPrivateBrowsing" in param) { + newCookieBehaviorPB = behaviors[param.BehaviorPrivateBrowsing]; + } + } else { + // Default, AcceptThirdParty, and RejectTracker are being + // deprecated in favor of Behavior. They will continue + // to be supported, though. + if ( + param.Default !== undefined || + param.AcceptThirdParty !== undefined || + param.RejectTracker !== undefined || + param.Locked + ) { + newCookieBehavior = Ci.nsICookieService.BEHAVIOR_ACCEPT; + if (param.Default !== undefined && !param.Default) { + newCookieBehavior = Ci.nsICookieService.BEHAVIOR_REJECT; + } else if (param.AcceptThirdParty) { + if (param.AcceptThirdParty == "never") { + newCookieBehavior = Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN; + } else if (param.AcceptThirdParty == "from-visited") { + newCookieBehavior = Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN; + } + } else if (param.RejectTracker) { + newCookieBehavior = Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER; + } + } + // With the old cookie policy, we made private browsing the same. + newCookieBehaviorPB = newCookieBehavior; + } + // We set the values no matter what just in case the policy was only used to lock. + PoliciesUtils.setDefaultPref( + "network.cookie.cookieBehavior", + newCookieBehavior, + param.Locked + ); + PoliciesUtils.setDefaultPref( + "network.cookie.cookieBehavior.pbmode", + newCookieBehaviorPB, + param.Locked + ); + }, + }, + + DefaultDownloadDirectory: { + onBeforeAddons(manager, param) { + PoliciesUtils.setDefaultPref( + "browser.download.dir", + replacePathVariables(param) + ); + // If a custom download directory is being used, just lock folder list to 2. + setAndLockPref("browser.download.folderList", 2); + }, + }, + + DisableAccounts: { + onBeforeAddons(manager, param) { + if (param) { + setAndLockPref("identity.fxaccounts.enabled", false); + setAndLockPref("browser.aboutwelcome.enabled", false); + } + }, + }, + + DisableAppUpdate: { + onBeforeAddons(manager, param) { + if (param) { + manager.disallowFeature("appUpdate"); + } + }, + }, + + DisableBuiltinPDFViewer: { + onBeforeAddons(manager, param) { + if (param) { + setAndLockPref("pdfjs.disabled", true); + } + }, + }, + + DisabledCiphers: { + onBeforeAddons(manager, param) { + let cipherPrefs = { + TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256: + "security.ssl3.ecdhe_rsa_aes_128_gcm_sha256", + TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256: + "security.ssl3.ecdhe_ecdsa_aes_128_gcm_sha256", + TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256: + "security.ssl3.ecdhe_ecdsa_chacha20_poly1305_sha256", + TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256: + "security.ssl3.ecdhe_rsa_chacha20_poly1305_sha256", + TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384: + "security.ssl3.ecdhe_ecdsa_aes_256_gcm_sha384", + TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384: + "security.ssl3.ecdhe_rsa_aes_256_gcm_sha384", + TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA: + "security.ssl3.ecdhe_rsa_aes_128_sha", + TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA: + "security.ssl3.ecdhe_ecdsa_aes_128_sha", + TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA: + "security.ssl3.ecdhe_rsa_aes_256_sha", + TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA: + "security.ssl3.ecdhe_ecdsa_aes_256_sha", + TLS_DHE_RSA_WITH_AES_128_CBC_SHA: "security.ssl3.dhe_rsa_aes_128_sha", + TLS_DHE_RSA_WITH_AES_256_CBC_SHA: "security.ssl3.dhe_rsa_aes_256_sha", + TLS_RSA_WITH_AES_128_GCM_SHA256: "security.ssl3.rsa_aes_128_gcm_sha256", + TLS_RSA_WITH_AES_256_GCM_SHA384: "security.ssl3.rsa_aes_256_gcm_sha384", + TLS_RSA_WITH_AES_128_CBC_SHA: "security.ssl3.rsa_aes_128_sha", + TLS_RSA_WITH_AES_256_CBC_SHA: "security.ssl3.rsa_aes_256_sha", + TLS_RSA_WITH_3DES_EDE_CBC_SHA: + "security.ssl3.deprecated.rsa_des_ede3_sha", + }; + + for (let cipher in param) { + setAndLockPref(cipherPrefs[cipher], !param[cipher]); + } + }, + }, + + DisableDefaultBrowserAgent: { + // The implementation of this policy is in the default browser agent itself + // (/toolkit/mozapps/defaultagent); we need an entry for it here so that it + // shows up in about:policies as a real policy and not as an error. + }, + + DisableDeveloperTools: { + onBeforeAddons(manager, param) { + if (param) { + setAndLockPref("devtools.policy.disabled", true); + setAndLockPref("devtools.chrome.enabled", false); + + manager.disallowFeature("devtools"); + blockAboutPage(manager, "about:debugging"); + blockAboutPage(manager, "about:devtools-toolbox"); + blockAboutPage(manager, "about:profiling"); + } + }, + }, + + DisableFeedbackCommands: { + onBeforeUIStartup(manager, param) { + if (param) { + manager.disallowFeature("feedbackCommands"); + } + }, + }, + + DisableFirefoxAccounts: { + onBeforeAddons(manager, param) { + // If DisableAccounts is set, let it take precedence. + if ("DisableAccounts" in manager.getActivePolicies()) { + return; + } + + if (param) { + setAndLockPref("identity.fxaccounts.enabled", false); + setAndLockPref("browser.aboutwelcome.enabled", false); + } + }, + }, + + DisableFirefoxScreenshots: { + onBeforeAddons(manager, param) { + if (param) { + setAndLockPref("extensions.screenshots.disabled", true); + } + }, + }, + + DisableFirefoxStudies: { + onBeforeAddons(manager, param) { + if (param) { + manager.disallowFeature("Shield"); + setAndLockPref( + "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons", + false + ); + setAndLockPref( + "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features", + false + ); + } + }, + }, + + DisableForgetButton: { + onProfileAfterChange(manager, param) { + if (param) { + setAndLockPref("privacy.panicButton.enabled", false); + } + }, + }, + + DisableFormHistory: { + onBeforeUIStartup(manager, param) { + if (param) { + setAndLockPref("browser.formfill.enable", false); + } + }, + }, + + DisableMasterPasswordCreation: { + onBeforeUIStartup(manager, param) { + if (param) { + manager.disallowFeature("createMasterPassword"); + } + }, + }, + + DisablePasswordReveal: { + onBeforeUIStartup(manager, param) { + if (param) { + manager.disallowFeature("passwordReveal"); + } + }, + }, + + DisablePocket: { + onBeforeAddons(manager, param) { + if (param) { + setAndLockPref("extensions.pocket.enabled", false); + } + }, + }, + + DisablePrivateBrowsing: { + onBeforeAddons(manager, param) { + if (param) { + manager.disallowFeature("privatebrowsing"); + blockAboutPage(manager, "about:privatebrowsing", true); + setAndLockPref("browser.privatebrowsing.autostart", false); + } + }, + }, + + DisableProfileImport: { + onBeforeUIStartup(manager, param) { + if (param) { + manager.disallowFeature("profileImport"); + setAndLockPref( + "browser.newtabpage.activity-stream.migrationExpired", + true + ); + } + }, + }, + + DisableProfileRefresh: { + onBeforeUIStartup(manager, param) { + if (param) { + manager.disallowFeature("profileRefresh"); + setAndLockPref("browser.disableResetPrompt", true); + } + }, + }, + + DisableSafeMode: { + onBeforeUIStartup(manager, param) { + if (param) { + manager.disallowFeature("safeMode"); + } + }, + }, + + DisableSecurityBypass: { + onBeforeUIStartup(manager, param) { + if ("InvalidCertificate" in param) { + setAndLockPref( + "security.certerror.hideAddException", + param.InvalidCertificate + ); + } + + if ("SafeBrowsing" in param) { + setAndLockPref( + "browser.safebrowsing.allowOverride", + !param.SafeBrowsing + ); + } + }, + }, + + DisableSetDesktopBackground: { + onBeforeUIStartup(manager, param) { + if (param) { + manager.disallowFeature("setDesktopBackground"); + } + }, + }, + + DisableSystemAddonUpdate: { + onBeforeAddons(manager, param) { + if (param) { + manager.disallowFeature("SysAddonUpdate"); + } + }, + }, + + DisableTelemetry: { + onBeforeAddons(manager, param) { + if (param) { + setAndLockPref("datareporting.healthreport.uploadEnabled", false); + setAndLockPref("datareporting.policy.dataSubmissionEnabled", false); + setAndLockPref("toolkit.telemetry.archive.enabled", false); + blockAboutPage(manager, "about:telemetry"); + } + }, + }, + + DisableThirdPartyModuleBlocking: { + onBeforeUIStartup(manager, param) { + if (param) { + manager.disallowFeature("thirdPartyModuleBlocking"); + } + }, + }, + + DisplayBookmarksToolbar: { + onBeforeUIStartup(manager, param) { + let visibility; + if (typeof param === "boolean") { + visibility = param ? "always" : "newtab"; + } else { + visibility = param; + } + // This policy is meant to change the default behavior, not to force it. + // If this policy was already applied and the user chose to re-hide the + // bookmarks toolbar, do not show it again. + runOncePerModification("displayBookmarksToolbar", visibility, () => { + let visibilityPref = "browser.toolbars.bookmarks.visibility"; + Services.prefs.setCharPref(visibilityPref, visibility); + }); + }, + }, + + DisplayMenuBar: { + onBeforeUIStartup(manager, param) { + let value; + if ( + typeof param === "boolean" || + param == "default-on" || + param == "default-off" + ) { + switch (param) { + case "default-on": + value = "false"; + break; + case "default-off": + value = "true"; + break; + default: + value = (!param).toString(); + break; + } + // This policy is meant to change the default behavior, not to force it. + // If this policy was already applied and the user chose to re-hide the + // menu bar, do not show it again. + runOncePerModification("displayMenuBar", value, () => { + Services.xulStore.setValue( + BROWSER_DOCUMENT_URL, + "toolbar-menubar", + "autohide", + value + ); + }); + } else { + switch (param) { + case "always": + value = "false"; + break; + case "never": + // Make sure Alt key doesn't show the menubar + setAndLockPref("ui.key.menuAccessKeyFocuses", false); + value = "true"; + break; + } + Services.xulStore.setValue( + BROWSER_DOCUMENT_URL, + "toolbar-menubar", + "autohide", + value + ); + manager.disallowFeature("hideShowMenuBar"); + } + }, + }, + + DNSOverHTTPS: { + onBeforeAddons(manager, param) { + if ("Enabled" in param) { + let mode = param.Enabled ? 2 : 5; + // Fallback only matters if DOH is enabled. + if (param.Fallback === false) { + mode = 3; + } + PoliciesUtils.setDefaultPref("network.trr.mode", mode, param.Locked); + } + if ("ProviderURL" in param) { + PoliciesUtils.setDefaultPref( + "network.trr.uri", + param.ProviderURL.href, + param.Locked + ); + } + if ("ExcludedDomains" in param) { + PoliciesUtils.setDefaultPref( + "network.trr.excluded-domains", + param.ExcludedDomains.join(","), + param.Locked + ); + } + }, + }, + + DontCheckDefaultBrowser: { + onBeforeUIStartup(manager, param) { + setAndLockPref("browser.shell.checkDefaultBrowser", !param); + }, + }, + + DownloadDirectory: { + onBeforeAddons(manager, param) { + setAndLockPref("browser.download.dir", replacePathVariables(param)); + // If a custom download directory is being used, just lock folder list to 2. + setAndLockPref("browser.download.folderList", 2); + // Per Chrome spec, user can't choose to download every time + // if this is set. + setAndLockPref("browser.download.useDownloadDir", true); + }, + }, + + EnableTrackingProtection: { + onBeforeUIStartup(manager, param) { + if (param.Value) { + PoliciesUtils.setDefaultPref( + "privacy.trackingprotection.enabled", + true, + param.Locked + ); + PoliciesUtils.setDefaultPref( + "privacy.trackingprotection.pbmode.enabled", + true, + param.Locked + ); + } else { + setAndLockPref("privacy.trackingprotection.enabled", false); + setAndLockPref("privacy.trackingprotection.pbmode.enabled", false); + } + if ("Cryptomining" in param) { + PoliciesUtils.setDefaultPref( + "privacy.trackingprotection.cryptomining.enabled", + param.Cryptomining, + param.Locked + ); + } + if ("Fingerprinting" in param) { + PoliciesUtils.setDefaultPref( + "privacy.trackingprotection.fingerprinting.enabled", + param.Fingerprinting, + param.Locked + ); + } + if ("EmailTracking" in param) { + PoliciesUtils.setDefaultPref( + "privacy.trackingprotection.emailtracking.enabled", + param.EmailTracking, + param.Locked + ); + PoliciesUtils.setDefaultPref( + "privacy.trackingprotection.emailtracking.pbmode.enabled", + param.EmailTracking, + param.Locked + ); + } + if ("Exceptions" in param) { + addAllowDenyPermissions("trackingprotection", param.Exceptions); + } + }, + }, + + EncryptedMediaExtensions: { + onBeforeAddons(manager, param) { + if ("Enabled" in param) { + PoliciesUtils.setDefaultPref( + "media.eme.enabled", + param.Enabled, + param.Locked + ); + } + }, + }, + + ExemptDomainFileTypePairsFromFileTypeDownloadWarnings: { + // This policy is handled directly in EnterprisePoliciesParent.jsm + // and requires no validation (It's done by the schema). + }, + + Extensions: { + onBeforeUIStartup(manager, param) { + let uninstallingPromise = Promise.resolve(); + if ("Uninstall" in param) { + uninstallingPromise = runOncePerModification( + "extensionsUninstall", + JSON.stringify(param.Uninstall), + async () => { + // If we're uninstalling add-ons, re-run the extensionsInstall runOnce even if it hasn't + // changed, which will allow add-ons to be updated. + Services.prefs.clearUserPref( + "browser.policies.runOncePerModification.extensionsInstall" + ); + let addons = await lazy.AddonManager.getAddonsByIDs( + param.Uninstall + ); + for (let addon of addons) { + if (addon) { + try { + await addon.uninstall(); + } catch (e) { + // This can fail for add-ons that can't be uninstalled. + lazy.log.debug( + `Add-on ID (${addon.id}) couldn't be uninstalled.` + ); + } + } + } + } + ); + } + if ("Install" in param) { + runOncePerModification( + "extensionsInstall", + JSON.stringify(param.Install), + async () => { + await uninstallingPromise; + for (let location of param.Install) { + let uri; + try { + // We need to try as a file first because + // Windows paths are valid URIs. + // This is done for legacy support (old API) + let xpiFile = new lazy.FileUtils.File(location); + uri = Services.io.newFileURI(xpiFile); + } catch (e) { + uri = Services.io.newURI(location); + } + installAddonFromURL(uri.spec); + } + } + ); + } + if ("Locked" in param) { + for (let ID of param.Locked) { + manager.disallowFeature(`uninstall-extension:${ID}`); + manager.disallowFeature(`disable-extension:${ID}`); + } + } + }, + }, + + ExtensionSettings: { + onBeforeAddons(manager, param) { + try { + manager.setExtensionSettings(param); + } catch (e) { + lazy.log.error("Invalid ExtensionSettings"); + } + }, + async onBeforeUIStartup(manager, param) { + let extensionSettings = param; + let blockAllExtensions = false; + if ("*" in extensionSettings) { + if ( + "installation_mode" in extensionSettings["*"] && + extensionSettings["*"].installation_mode == "blocked" + ) { + blockAllExtensions = true; + // Turn off discovery pane in about:addons + setAndLockPref("extensions.getAddons.showPane", false); + // Turn off recommendations + setAndLockPref( + "extensions.htmlaboutaddons.recommendations.enable", + false + ); + // Block about:debugging + blockAboutPage(manager, "about:debugging"); + } + if ("restricted_domains" in extensionSettings["*"]) { + let restrictedDomains = Services.prefs + .getCharPref("extensions.webextensions.restrictedDomains") + .split(","); + setAndLockPref( + "extensions.webextensions.restrictedDomains", + restrictedDomains + .concat(extensionSettings["*"].restricted_domains) + .join(",") + ); + } + } + let addons = await lazy.AddonManager.getAllAddons(); + let allowedExtensions = []; + for (let extensionID in extensionSettings) { + if (extensionID == "*") { + // Ignore global settings + continue; + } + if ("installation_mode" in extensionSettings[extensionID]) { + if ( + extensionSettings[extensionID].installation_mode == + "force_installed" || + extensionSettings[extensionID].installation_mode == + "normal_installed" + ) { + if (!extensionSettings[extensionID].install_url) { + throw new Error(`Missing install_url for ${extensionID}`); + } + installAddonFromURL( + extensionSettings[extensionID].install_url, + extensionID, + addons.find(addon => addon.id == extensionID) + ); + manager.disallowFeature(`uninstall-extension:${extensionID}`); + if ( + extensionSettings[extensionID].installation_mode == + "force_installed" + ) { + manager.disallowFeature(`disable-extension:${extensionID}`); + } + allowedExtensions.push(extensionID); + } else if ( + extensionSettings[extensionID].installation_mode == "allowed" + ) { + allowedExtensions.push(extensionID); + } else if ( + extensionSettings[extensionID].installation_mode == "blocked" + ) { + if (addons.find(addon => addon.id == extensionID)) { + // Can't use the addon from getActiveAddons since it doesn't have uninstall. + let addon = await lazy.AddonManager.getAddonByID(extensionID); + try { + await addon.uninstall(); + } catch (e) { + // This can fail for add-ons that can't be uninstalled. + lazy.log.debug( + `Add-on ID (${addon.id}) couldn't be uninstalled.` + ); + } + } + } + } + } + if (blockAllExtensions) { + for (let addon of addons) { + if ( + addon.isSystem || + addon.isBuiltin || + !(addon.scope & lazy.AddonManager.SCOPE_PROFILE) + ) { + continue; + } + if (!allowedExtensions.includes(addon.id)) { + try { + // Can't use the addon from getActiveAddons since it doesn't have uninstall. + let addonToUninstall = await lazy.AddonManager.getAddonByID( + addon.id + ); + await addonToUninstall.uninstall(); + } catch (e) { + // This can fail for add-ons that can't be uninstalled. + lazy.log.debug( + `Add-on ID (${addon.id}) couldn't be uninstalled.` + ); + } + } + } + } + }, + }, + + ExtensionUpdate: { + onBeforeAddons(manager, param) { + if (!param) { + setAndLockPref("extensions.update.enabled", param); + } + }, + }, + + FirefoxHome: { + onBeforeAddons(manager, param) { + if ("Search" in param) { + PoliciesUtils.setDefaultPref( + "browser.newtabpage.activity-stream.showSearch", + param.Search, + param.Locked + ); + } + if ("TopSites" in param) { + PoliciesUtils.setDefaultPref( + "browser.newtabpage.activity-stream.feeds.topsites", + param.TopSites, + param.Locked + ); + } + if ("SponsoredTopSites" in param) { + PoliciesUtils.setDefaultPref( + "browser.newtabpage.activity-stream.showSponsoredTopSites", + param.SponsoredTopSites, + param.Locked + ); + } + if ("Highlights" in param) { + PoliciesUtils.setDefaultPref( + "browser.newtabpage.activity-stream.feeds.section.highlights", + param.Highlights, + param.Locked + ); + } + if ("Pocket" in param) { + PoliciesUtils.setDefaultPref( + "browser.newtabpage.activity-stream.feeds.system.topstories", + param.Pocket, + param.Locked + ); + PoliciesUtils.setDefaultPref( + "browser.newtabpage.activity-stream.feeds.section.topstories", + param.Pocket, + param.Locked + ); + } + if ("SponsoredPocket" in param) { + PoliciesUtils.setDefaultPref( + "browser.newtabpage.activity-stream.showSponsored", + param.SponsoredPocket, + param.Locked + ); + } + }, + }, + + FirefoxSuggest: { + onBeforeAddons(manager, param) { + (async () => { + await lazy.UrlbarPrefs.firefoxSuggestScenarioStartupPromise; + if ("WebSuggestions" in param) { + PoliciesUtils.setDefaultPref( + "browser.urlbar.suggest.quicksuggest.nonsponsored", + param.WebSuggestions, + param.Locked + ); + } + if ("SponsoredSuggestions" in param) { + PoliciesUtils.setDefaultPref( + "browser.urlbar.suggest.quicksuggest.sponsored", + param.SponsoredSuggestions, + param.Locked + ); + } + if ("ImproveSuggest" in param) { + PoliciesUtils.setDefaultPref( + "browser.urlbar.quicksuggest.dataCollection.enabled", + param.ImproveSuggest, + param.Locked + ); + } + })(); + }, + }, + + GoToIntranetSiteForSingleWordEntryInAddressBar: { + onBeforeAddons(manager, param) { + setAndLockPref("browser.fixup.dns_first_for_single_words", param); + }, + }, + + Handlers: { + onBeforeAddons(manager, param) { + if ("mimeTypes" in param) { + for (let mimeType in param.mimeTypes) { + let mimeInfo = param.mimeTypes[mimeType]; + let realMIMEInfo = lazy.gMIMEService.getFromTypeAndExtension( + mimeType, + "" + ); + processMIMEInfo(mimeInfo, realMIMEInfo); + } + } + if ("extensions" in param) { + for (let extension in param.extensions) { + let mimeInfo = param.extensions[extension]; + try { + let realMIMEInfo = lazy.gMIMEService.getFromTypeAndExtension( + "", + extension + ); + processMIMEInfo(mimeInfo, realMIMEInfo); + } catch (e) { + lazy.log.error(`Invalid file extension (${extension})`); + } + } + } + if ("schemes" in param) { + for (let scheme in param.schemes) { + let handlerInfo = param.schemes[scheme]; + let realHandlerInfo = + lazy.gExternalProtocolService.getProtocolHandlerInfo(scheme); + processMIMEInfo(handlerInfo, realHandlerInfo); + } + } + }, + }, + + HardwareAcceleration: { + onBeforeAddons(manager, param) { + if (!param) { + setAndLockPref("layers.acceleration.disabled", true); + } + }, + }, + + Homepage: { + onBeforeUIStartup(manager, param) { + if ("StartPage" in param && param.StartPage == "none") { + // For blank startpage, we use about:blank rather + // than messing with browser.startup.page + param.URL = new URL("about:blank"); + } + // |homepages| will be a string containing a pipe-separated ('|') list of + // URLs because that is what the "Home page" section of about:preferences + // (and therefore what the pref |browser.startup.homepage|) accepts. + if ("URL" in param) { + let homepages = param.URL.href; + if (param.Additional && param.Additional.length) { + homepages += "|" + param.Additional.map(url => url.href).join("|"); + } + PoliciesUtils.setDefaultPref( + "browser.startup.homepage", + homepages, + param.Locked + ); + if (param.Locked) { + setAndLockPref( + "pref.browser.homepage.disable_button.current_page", + true + ); + setAndLockPref( + "pref.browser.homepage.disable_button.bookmark_page", + true + ); + setAndLockPref( + "pref.browser.homepage.disable_button.restore_default", + true + ); + } else { + // Clear out old run once modification that is no longer used. + clearRunOnceModification("setHomepage"); + } + // If a homepage has been set via policy, show the home button + if (param.URL != "about:blank") { + manager.disallowFeature("removeHomeButtonByDefault"); + } + } + if (param.StartPage) { + let prefValue; + switch (param.StartPage) { + case "homepage": + case "homepage-locked": + case "none": + prefValue = 1; + break; + case "previous-session": + prefValue = 3; + break; + } + PoliciesUtils.setDefaultPref( + "browser.startup.page", + prefValue, + param.StartPage == "homepage-locked" + ); + } + }, + }, + + InstallAddonsPermission: { + onBeforeUIStartup(manager, param) { + if ("Allow" in param) { + addAllowDenyPermissions("install", param.Allow, null); + } + if ("Default" in param) { + setAndLockPref("xpinstall.enabled", param.Default); + if (!param.Default) { + blockAboutPage(manager, "about:debugging"); + setAndLockPref( + "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons", + false + ); + setAndLockPref( + "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features", + false + ); + manager.disallowFeature("xpinstall"); + } + } + }, + }, + + LegacyProfiles: { + // Handled in nsToolkitProfileService.cpp (Windows only) + }, + + LegacySameSiteCookieBehaviorEnabled: { + onBeforeAddons(manager, param) { + PoliciesUtils.setDefaultPref( + "network.cookie.sameSite.laxByDefault", + !param + ); + }, + }, + + LegacySameSiteCookieBehaviorEnabledForDomainList: { + onBeforeAddons(manager, param) { + PoliciesUtils.setDefaultPref( + "network.cookie.sameSite.laxByDefault.disabledHosts", + param.join(",") + ); + }, + }, + + LocalFileLinks: { + onBeforeAddons(manager, param) { + // If there are existing capabilities, lock them with the policy pref. + let policyNames = Services.prefs + .getCharPref("capability.policy.policynames", "") + .split(" "); + policyNames.push("localfilelinks_policy"); + setAndLockPref("capability.policy.policynames", policyNames.join(" ")); + setAndLockPref( + "capability.policy.localfilelinks_policy.checkloaduri.enabled", + "allAccess" + ); + setAndLockPref( + "capability.policy.localfilelinks_policy.sites", + param.join(" ") + ); + }, + }, + + ManagedBookmarks: {}, + + ManualAppUpdateOnly: { + onBeforeAddons(manager, param) { + if (param) { + manager.disallowFeature("autoAppUpdateChecking"); + } + }, + }, + + NetworkPrediction: { + onBeforeAddons(manager, param) { + setAndLockPref("network.dns.disablePrefetch", !param); + setAndLockPref("network.dns.disablePrefetchFromHTTPS", !param); + }, + }, + + NewTabPage: { + onBeforeAddons(manager, param) { + setAndLockPref("browser.newtabpage.enabled", param); + }, + }, + + NoDefaultBookmarks: { + onProfileAfterChange(manager, param) { + if (param) { + manager.disallowFeature("defaultBookmarks"); + } + }, + }, + + OfferToSaveLogins: { + onBeforeUIStartup(manager, param) { + setAndLockPref("signon.rememberSignons", param); + setAndLockPref("services.passwordSavingEnabled", param); + }, + }, + + OfferToSaveLoginsDefault: { + onBeforeUIStartup(manager, param) { + let policies = Services.policies.getActivePolicies(); + if ("OfferToSaveLogins" in policies) { + lazy.log.error( + `OfferToSaveLoginsDefault ignored because OfferToSaveLogins is present.` + ); + } else { + PoliciesUtils.setDefaultPref("signon.rememberSignons", param); + } + }, + }, + + OverrideFirstRunPage: { + onProfileAfterChange(manager, param) { + let url = param ? param : ""; + setAndLockPref("startup.homepage_welcome_url", url); + setAndLockPref("browser.aboutwelcome.enabled", false); + }, + }, + + OverridePostUpdatePage: { + onProfileAfterChange(manager, param) { + let url = param ? param.href : ""; + setAndLockPref("startup.homepage_override_url", url); + // The pref startup.homepage_override_url is only used + // as a fallback when the update.xml file hasn't provided + // a specific post-update URL. + manager.disallowFeature("postUpdateCustomPage"); + }, + }, + + PasswordManagerEnabled: { + onBeforeUIStartup(manager, param) { + if (!param) { + blockAboutPage(manager, "about:logins", true); + setAndLockPref("pref.privacy.disable_button.view_passwords", true); + } + setAndLockPref("signon.rememberSignons", param); + }, + }, + + PasswordManagerExceptions: { + onBeforeUIStartup(manager, param) { + addAllowDenyPermissions("login-saving", null, param); + }, + }, + + PDFjs: { + onBeforeAddons(manager, param) { + if ("Enabled" in param) { + setAndLockPref("pdfjs.disabled", !param.Enabled); + } + if ("EnablePermissions" in param) { + setAndLockPref("pdfjs.enablePermissions", param.EnablePermissions); + } + }, + }, + + Permissions: { + onBeforeUIStartup(manager, param) { + if (param.Camera) { + addAllowDenyPermissions( + "camera", + param.Camera.Allow, + param.Camera.Block + ); + setDefaultPermission("camera", param.Camera); + } + + if (param.Microphone) { + addAllowDenyPermissions( + "microphone", + param.Microphone.Allow, + param.Microphone.Block + ); + setDefaultPermission("microphone", param.Microphone); + } + + if (param.Autoplay) { + addAllowDenyPermissions( + "autoplay-media", + param.Autoplay.Allow, + param.Autoplay.Block + ); + if ("Default" in param.Autoplay) { + let prefValue; + switch (param.Autoplay.Default) { + case "allow-audio-video": + prefValue = 0; + break; + case "block-audio": + prefValue = 1; + break; + case "block-audio-video": + prefValue = 5; + break; + } + PoliciesUtils.setDefaultPref( + "media.autoplay.default", + prefValue, + param.Autoplay.Locked + ); + } + } + + if (param.Location) { + addAllowDenyPermissions( + "geo", + param.Location.Allow, + param.Location.Block + ); + setDefaultPermission("geo", param.Location); + } + + if (param.Notifications) { + addAllowDenyPermissions( + "desktop-notification", + param.Notifications.Allow, + param.Notifications.Block + ); + setDefaultPermission("desktop-notification", param.Notifications); + } + + if ("VirtualReality" in param) { + addAllowDenyPermissions( + "xr", + param.VirtualReality.Allow, + param.VirtualReality.Block + ); + setDefaultPermission("xr", param.VirtualReality); + } + }, + }, + + PictureInPicture: { + onBeforeAddons(manager, param) { + if ("Enabled" in param) { + PoliciesUtils.setDefaultPref( + "media.videocontrols.picture-in-picture.video-toggle.enabled", + param.Enabled + ); + } + if (param.Locked) { + Services.prefs.lockPref( + "media.videocontrols.picture-in-picture.video-toggle.enabled" + ); + } + }, + }, + + PopupBlocking: { + onBeforeUIStartup(manager, param) { + addAllowDenyPermissions("popup", param.Allow, null); + + if (param.Locked) { + let blockValue = true; + if (param.Default !== undefined && !param.Default) { + blockValue = false; + } + setAndLockPref("dom.disable_open_during_load", blockValue); + } else if (param.Default !== undefined) { + PoliciesUtils.setDefaultPref( + "dom.disable_open_during_load", + !!param.Default + ); + } + }, + }, + + Preferences: { + onBeforeAddons(manager, param) { + let allowedPrefixes = [ + "accessibility.", + "alerts.", + "app.update.", + "browser.", + "datareporting.policy.", + "dom.", + "extensions.", + "general.autoScroll", + "general.smoothScroll", + "geo.", + "gfx.", + "intl.", + "keyword.enabled", + "layers.", + "layout.", + "media.", + "network.", + "pdfjs.", + "places.", + "pref.", + "print.", + "signon.", + "spellchecker.", + "toolkit.legacyUserProfileCustomizations.stylesheets", + "ui.", + "widget.", + "xpinstall.whitelist.required", + ]; + if (!AppConstants.MOZ_REQUIRE_SIGNING) { + allowedPrefixes.push("xpinstall.signatures.required"); + } + const allowedSecurityPrefs = [ + "security.block_fileuri_script_with_wrong_mime", + "security.default_personal_cert", + "security.disable_button.openCertManager", + "security.disable_button.openDeviceManager", + "security.insecure_connection_text.enabled", + "security.insecure_connection_text.pbmode.enabled", + "security.mixed_content.block_active_content", + "security.osclientcerts.assume_rsa_pss_support", + "security.osclientcerts.autoload", + "security.OCSP.enabled", + "security.OCSP.require", + "security.ssl.enable_ocsp_stapling", + "security.ssl.errorReporting.enabled", + "security.ssl.require_safe_negotiation", + "security.tls.enable_0rtt_data", + "security.tls.hello_downgrade_check", + "security.tls.version.enable-deprecated", + "security.warn_submit_secure_to_insecure", + ]; + const blockedPrefs = [ + "app.update.channel", + "app.update.lastUpdateTime", + "app.update.migrated", + "browser.vpn_promo.disallowed_regions", + ]; + + for (let preference in param) { + if (blockedPrefs.includes(preference)) { + lazy.log.error( + `Unable to set preference ${preference}. Preference not allowed for security reasons.` + ); + continue; + } + if (preference.startsWith("security.")) { + if (!allowedSecurityPrefs.includes(preference)) { + lazy.log.error( + `Unable to set preference ${preference}. Preference not allowed for security reasons.` + ); + continue; + } + } else if ( + !allowedPrefixes.some(prefix => preference.startsWith(prefix)) + ) { + lazy.log.error( + `Unable to set preference ${preference}. Preference not allowed for stability reasons.` + ); + continue; + } + if (typeof param[preference] != "object") { + // Legacy policy preferences + setAndLockPref(preference, param[preference]); + } else { + if (param[preference].Status == "clear") { + Services.prefs.clearUserPref(preference); + continue; + } + + let prefBranch; + if (param[preference].Status == "user") { + prefBranch = Services.prefs; + } else { + prefBranch = Services.prefs.getDefaultBranch(""); + } + + // Prefs that were previously locked should stay locked, + // but policy can update the value. + let prefWasLocked = Services.prefs.prefIsLocked(preference); + if (prefWasLocked) { + Services.prefs.unlockPref(preference); + } + try { + let prefType = + param[preference].Type || typeof param[preference].Value; + switch (prefType) { + case "boolean": + prefBranch.setBoolPref(preference, param[preference].Value); + break; + + case "number": + if (!Number.isInteger(param[preference].Value)) { + throw new Error(`Non-integer value for ${preference}`); + } + + // Because pdfjs prefs are set async, we can't check the + // default pref branch to see if they are int or bool, so we + // have to get their type from PdfJsDefaultPreferences. + if (preference.startsWith("pdfjs.")) { + let preferenceTail = preference.replace("pdfjs.", ""); + if ( + preferenceTail in lazy.PdfJsDefaultPreferences && + typeof lazy.PdfJsDefaultPreferences[preferenceTail] == + "number" + ) { + prefBranch.setIntPref(preference, param[preference].Value); + } else { + prefBranch.setBoolPref( + preference, + !!param[preference].Value + ); + } + break; + } + + // This is ugly, but necessary. On Windows GPO and macOS + // configs, booleans are converted to 0/1. In the previous + // Preferences implementation, the schema took care of + // automatically converting these values to booleans. + // Since we allow arbitrary prefs now, we have to do + // something different. See bug 1666836, 1668374, and 1872267. + + // We only set something as int if it was explicit in policy, + // the same type as the default pref, or NOT 0/1. Otherwise + // we set it as bool. + if ( + param[preference].Type == "number" || + prefBranch.getPrefType(preference) == prefBranch.PREF_INT || + ![0, 1].includes(param[preference].Value) + ) { + prefBranch.setIntPref(preference, param[preference].Value); + } else { + prefBranch.setBoolPref(preference, !!param[preference].Value); + } + break; + + case "string": + prefBranch.setStringPref(preference, param[preference].Value); + break; + } + } catch (e) { + lazy.log.error( + `Unable to set preference ${preference}. Probable type mismatch.` + ); + } + + if (param[preference].Status == "locked" || prefWasLocked) { + Services.prefs.lockPref(preference); + } + } + } + }, + }, + + PrimaryPassword: { + onAllWindowsRestored(manager, param) { + if (param) { + manager.disallowFeature("removeMasterPassword"); + } else { + manager.disallowFeature("createMasterPassword"); + } + }, + }, + + PrintingEnabled: { + onBeforeUIStartup(manager, param) { + setAndLockPref("print.enabled", param); + }, + }, + + PromptForDownloadLocation: { + onBeforeAddons(manager, param) { + setAndLockPref("browser.download.useDownloadDir", !param); + }, + }, + + Proxy: { + onBeforeAddons(manager, param) { + if (param.Locked) { + manager.disallowFeature("changeProxySettings"); + lazy.ProxyPolicies.configureProxySettings(param, setAndLockPref); + } else { + lazy.ProxyPolicies.configureProxySettings( + param, + PoliciesUtils.setDefaultPref + ); + } + }, + }, + + RequestedLocales: { + onBeforeAddons(manager, param) { + let requestedLocales; + if (Array.isArray(param)) { + requestedLocales = param; + } else if (param) { + requestedLocales = param.split(","); + } else { + requestedLocales = []; + } + runOncePerModification( + "requestedLocales", + JSON.stringify(requestedLocales), + () => { + Services.locale.requestedLocales = requestedLocales; + } + ); + }, + }, + + SanitizeOnShutdown: { + onBeforeUIStartup(manager, param) { + if (typeof param === "boolean") { + setAndLockPref("privacy.sanitize.sanitizeOnShutdown", param); + setAndLockPref("privacy.clearOnShutdown.cache", param); + setAndLockPref("privacy.clearOnShutdown.cookies", param); + setAndLockPref("privacy.clearOnShutdown.downloads", param); + setAndLockPref("privacy.clearOnShutdown.formdata", param); + setAndLockPref("privacy.clearOnShutdown.history", param); + setAndLockPref("privacy.clearOnShutdown.sessions", param); + setAndLockPref("privacy.clearOnShutdown.siteSettings", param); + setAndLockPref("privacy.clearOnShutdown.offlineApps", param); + } else { + let locked = true; + // Needed to preserve original behavior in perpetuity. + let lockDefaultPrefs = true; + if ("Locked" in param) { + locked = param.Locked; + lockDefaultPrefs = false; + } + PoliciesUtils.setDefaultPref( + "privacy.sanitize.sanitizeOnShutdown", + true, + locked + ); + if ("Cache" in param) { + PoliciesUtils.setDefaultPref( + "privacy.clearOnShutdown.cache", + param.Cache, + locked + ); + } else { + PoliciesUtils.setDefaultPref( + "privacy.clearOnShutdown.cache", + false, + lockDefaultPrefs + ); + } + if ("Cookies" in param) { + PoliciesUtils.setDefaultPref( + "privacy.clearOnShutdown.cookies", + param.Cookies, + locked + ); + } else { + PoliciesUtils.setDefaultPref( + "privacy.clearOnShutdown.cookies", + false, + lockDefaultPrefs + ); + } + if ("Downloads" in param) { + PoliciesUtils.setDefaultPref( + "privacy.clearOnShutdown.downloads", + param.Downloads, + locked + ); + } else { + PoliciesUtils.setDefaultPref( + "privacy.clearOnShutdown.downloads", + false, + lockDefaultPrefs + ); + } + if ("FormData" in param) { + PoliciesUtils.setDefaultPref( + "privacy.clearOnShutdown.formdata", + param.FormData, + locked + ); + } else { + PoliciesUtils.setDefaultPref( + "privacy.clearOnShutdown.formdata", + false, + lockDefaultPrefs + ); + } + if ("History" in param) { + PoliciesUtils.setDefaultPref( + "privacy.clearOnShutdown.history", + param.History, + locked + ); + } else { + PoliciesUtils.setDefaultPref( + "privacy.clearOnShutdown.history", + false, + lockDefaultPrefs + ); + } + if ("Sessions" in param) { + PoliciesUtils.setDefaultPref( + "privacy.clearOnShutdown.sessions", + param.Sessions, + locked + ); + } else { + PoliciesUtils.setDefaultPref( + "privacy.clearOnShutdown.sessions", + false, + lockDefaultPrefs + ); + } + if ("SiteSettings" in param) { + PoliciesUtils.setDefaultPref( + "privacy.clearOnShutdown.siteSettings", + param.SiteSettings, + locked + ); + } + if ("OfflineApps" in param) { + PoliciesUtils.setDefaultPref( + "privacy.clearOnShutdown.offlineApps", + param.OfflineApps, + locked + ); + } + } + }, + }, + + SearchBar: { + onAllWindowsRestored(manager, param) { + // This policy is meant to change the default behavior, not to force it. + // If this policy was already applied and the user chose move the search + // bar, don't move it again. + runOncePerModification("searchInNavBar", param, () => { + if (param == "separate") { + lazy.CustomizableUI.addWidgetToArea( + "search-container", + lazy.CustomizableUI.AREA_NAVBAR, + lazy.CustomizableUI.getPlacementOfWidget("urlbar-container") + .position + 1 + ); + } else if (param == "unified") { + lazy.CustomizableUI.removeWidgetFromArea("search-container"); + } + }); + }, + }, + + SearchEngines: { + onBeforeUIStartup(manager, param) { + if (param.PreventInstalls) { + manager.disallowFeature("installSearchEngine", true); + } + }, + onAllWindowsRestored(manager, param) { + Services.search.init().then(async () => { + // Adding of engines is handled by the SearchService in the init(). + // Remove can happen after those are added - no engines are allowed + // to replace the application provided engines, even if they have been + // removed. + if (param.Remove) { + // Only rerun if the list of engine names has changed. + await runOncePerModification( + "removeSearchEngines", + JSON.stringify(param.Remove), + async function () { + for (let engineName of param.Remove) { + let engine = Services.search.getEngineByName(engineName); + if (engine) { + try { + await Services.search.removeEngine(engine); + } catch (ex) { + lazy.log.error("Unable to remove the search engine", ex); + } + } + } + } + ); + } + if (param.Default) { + await runOncePerModification( + "setDefaultSearchEngine", + param.Default, + async () => { + let defaultEngine; + try { + defaultEngine = Services.search.getEngineByName(param.Default); + if (!defaultEngine) { + throw new Error("No engine by that name could be found"); + } + } catch (ex) { + lazy.log.error( + `Search engine lookup failed when attempting to set ` + + `the default engine. Requested engine was ` + + `"${param.Default}".`, + ex + ); + } + if (defaultEngine) { + try { + await Services.search.setDefault( + defaultEngine, + Ci.nsISearchService.CHANGE_REASON_ENTERPRISE + ); + } catch (ex) { + lazy.log.error("Unable to set the default search engine", ex); + } + } + } + ); + } + if (param.DefaultPrivate) { + await runOncePerModification( + "setDefaultPrivateSearchEngine", + param.DefaultPrivate, + async () => { + let defaultPrivateEngine; + try { + defaultPrivateEngine = Services.search.getEngineByName( + param.DefaultPrivate + ); + if (!defaultPrivateEngine) { + throw new Error("No engine by that name could be found"); + } + } catch (ex) { + lazy.log.error( + `Search engine lookup failed when attempting to set ` + + `the default private engine. Requested engine was ` + + `"${param.DefaultPrivate}".`, + ex + ); + } + if (defaultPrivateEngine) { + try { + await Services.search.setDefaultPrivate( + defaultPrivateEngine, + Ci.nsISearchService.CHANGE_REASON_ENTERPRISE + ); + } catch (ex) { + lazy.log.error( + "Unable to set the default private search engine", + ex + ); + } + } + } + ); + } + }); + }, + }, + + SearchSuggestEnabled: { + onBeforeAddons(manager, param) { + setAndLockPref("browser.urlbar.suggest.searches", param); + setAndLockPref("browser.search.suggest.enabled", param); + }, + }, + + SecurityDevices: { + onProfileAfterChange(manager, param) { + let pkcs11db = Cc["@mozilla.org/security/pkcs11moduledb;1"].getService( + Ci.nsIPKCS11ModuleDB + ); + let securityDevices; + if (param.Add || param.Delete) { + // We're using the new syntax. + securityDevices = param.Add; + if (param.Delete) { + for (let deviceName of param.Delete) { + try { + pkcs11db.deleteModule(deviceName); + } catch (e) { + // Ignoring errors here since it might stick around in policy + // after removing. Alternative would be to listModules and + // make sure it's there before removing, but that seems + // like unnecessary work. + } + } + } + } else { + securityDevices = param; + } + if (!securityDevices) { + return; + } + for (let deviceName in securityDevices) { + let foundModule = false; + for (let module of pkcs11db.listModules()) { + if (module && module.libName === securityDevices[deviceName]) { + foundModule = true; + break; + } + } + if (foundModule) { + continue; + } + try { + pkcs11db.addModule(deviceName, securityDevices[deviceName], 0, 0); + } catch (ex) { + lazy.log.error(`Unable to add security device ${deviceName}`); + lazy.log.debug(ex); + } + } + }, + }, + + ShowHomeButton: { + onBeforeAddons(manager, param) { + if (param) { + manager.disallowFeature("removeHomeButtonByDefault"); + } + }, + onAllWindowsRestored(manager, param) { + if (param) { + let homeButtonPlacement = + lazy.CustomizableUI.getPlacementOfWidget("home-button"); + if (!homeButtonPlacement) { + let placement = + lazy.CustomizableUI.getPlacementOfWidget("forward-button"); + lazy.CustomizableUI.addWidgetToArea( + "home-button", + lazy.CustomizableUI.AREA_NAVBAR, + placement.position + 2 + ); + } + } else { + lazy.CustomizableUI.removeWidgetFromArea("home-button"); + } + }, + }, + + SSLVersionMax: { + onBeforeAddons(manager, param) { + let tlsVersion; + switch (param) { + case "tls1": + tlsVersion = 1; + break; + case "tls1.1": + tlsVersion = 2; + break; + case "tls1.2": + tlsVersion = 3; + break; + case "tls1.3": + tlsVersion = 4; + break; + } + setAndLockPref("security.tls.version.max", tlsVersion); + }, + }, + + SSLVersionMin: { + onBeforeAddons(manager, param) { + let tlsVersion; + switch (param) { + case "tls1": + tlsVersion = 1; + break; + case "tls1.1": + tlsVersion = 2; + break; + case "tls1.2": + tlsVersion = 3; + break; + case "tls1.3": + tlsVersion = 4; + break; + } + setAndLockPref("security.tls.version.min", tlsVersion); + }, + }, + + StartDownloadsInTempDirectory: { + onBeforeAddons(manager, param) { + setAndLockPref("browser.download.start_downloads_in_tmp_dir", param); + }, + }, + + SupportMenu: { + onProfileAfterChange(manager, param) { + manager.setSupportMenu(param); + }, + }, + + UserMessaging: { + onBeforeAddons(manager, param) { + if ("WhatsNew" in param) { + PoliciesUtils.setDefaultPref( + "browser.messaging-system.whatsNewPanel.enabled", + param.WhatsNew, + param.Locked + ); + } + if ("ExtensionRecommendations" in param) { + PoliciesUtils.setDefaultPref( + "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons", + param.ExtensionRecommendations, + param.Locked + ); + } + if ("FeatureRecommendations" in param) { + PoliciesUtils.setDefaultPref( + "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features", + param.FeatureRecommendations, + param.Locked + ); + PoliciesUtils.setDefaultPref( + "browser.translations.panelShown", + !param.FeatureRecommendations, + param.Locked + ); + } + if ("UrlbarInterventions" in param && !param.UrlbarInterventions) { + manager.disallowFeature("urlbarinterventions"); + } + if ("SkipOnboarding" in param) { + PoliciesUtils.setDefaultPref( + "browser.aboutwelcome.enabled", + !param.SkipOnboarding, + param.Locked + ); + } + if ("MoreFromMozilla" in param) { + PoliciesUtils.setDefaultPref( + "browser.preferences.moreFromMozilla", + param.MoreFromMozilla, + param.Locked + ); + } + }, + }, + + UseSystemPrintDialog: { + onBeforeAddons(manager, param) { + setAndLockPref("print.prefer_system_dialog", param); + }, + }, + + WebsiteFilter: { + onBeforeUIStartup(manager, param) { + lazy.WebsiteFilter.init(param.Block || [], param.Exceptions || []); + }, + }, + + WindowsSSO: { + onBeforeAddons(manager, param) { + setAndLockPref("network.http.windows-sso.enabled", param); + }, + }, +}; + +/* + * ==================== + * = HELPER FUNCTIONS = + * ==================== + * + * The functions below are helpers to be used by several policies. + */ + +/** + * setAndLockPref + * + * Sets the _default_ value of a pref, and locks it (meaning that + * the default value will always be returned, independent from what + * is stored as the user value). + * The value is only changed in memory, and not stored to disk. + * + * @param {string} prefName + * The pref to be changed + * @param {boolean|number|string} prefValue + * The value to set and lock + */ +export function setAndLockPref(prefName, prefValue) { + PoliciesUtils.setDefaultPref(prefName, prefValue, true); +} + +/** + * setDefaultPref + * + * Sets the _default_ value of a pref and optionally locks it. + * The value is only changed in memory, and not stored to disk. + * + * @param {string} prefName + * The pref to be changed + * @param {boolean|number|string} prefValue + * The value to set + * @param {boolean} locked + * Optionally lock the pref + */ + +export var PoliciesUtils = { + setDefaultPref(prefName, prefValue, locked) { + let prefWasLocked = Services.prefs.prefIsLocked(prefName); + if (prefWasLocked) { + Services.prefs.unlockPref(prefName); + } + + let defaults = Services.prefs.getDefaultBranch(""); + + switch (typeof prefValue) { + case "boolean": + defaults.setBoolPref(prefName, prefValue); + break; + + case "number": + if (!Number.isInteger(prefValue)) { + throw new Error(`Non-integer value for ${prefName}`); + } + + // This is ugly, but necessary. On Windows GPO and macOS + // configs, booleans are converted to 0/1. In the previous + // Preferences implementation, the schema took care of + // automatically converting these values to booleans. + // Since we allow arbitrary prefs now, we have to do + // something different. See bug 1666836. + if ( + defaults.getPrefType(prefName) == defaults.PREF_INT || + ![0, 1].includes(prefValue) + ) { + defaults.setIntPref(prefName, prefValue); + } else { + defaults.setBoolPref(prefName, !!prefValue); + } + break; + + case "string": + defaults.setStringPref(prefName, prefValue); + break; + } + + // Prefs can only be unlocked explicitly. + // If they were locked before, they stay locked. + if (locked || (prefWasLocked && locked !== false)) { + Services.prefs.lockPref(prefName); + } + }, +}; + +/** + * setDefaultPermission + * + * Helper function to set preferences appropriately for the policy + * + * @param {string} policyName + * The name of the policy to set + * @param {object} policyParam + * The object containing param for the policy + */ +function setDefaultPermission(policyName, policyParam) { + if ("BlockNewRequests" in policyParam) { + let prefName = "permissions.default." + policyName; + + if (policyParam.BlockNewRequests) { + PoliciesUtils.setDefaultPref(prefName, 2, policyParam.Locked); + } else { + PoliciesUtils.setDefaultPref(prefName, 0, policyParam.Locked); + } + } +} + +/** + * addAllowDenyPermissions + * + * Helper function to call the permissions manager (Services.perms.addFromPrincipal) + * for two arrays of URLs. + * + * @param {string} permissionName + * The name of the permission to change + * @param {Array} allowList + * The list of URLs to be set as ALLOW_ACTION for the chosen permission. + * @param {Array} blockList + * The list of URLs to be set as DENY_ACTION for the chosen permission. + */ +function addAllowDenyPermissions(permissionName, allowList, blockList) { + allowList = allowList || []; + blockList = blockList || []; + + for (let origin of allowList) { + try { + Services.perms.addFromPrincipal( + Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin), + permissionName, + Ci.nsIPermissionManager.ALLOW_ACTION, + Ci.nsIPermissionManager.EXPIRE_POLICY + ); + } catch (ex) { + // It's possible if the origin was invalid, we'll have a string instead of an origin. + lazy.log.error( + `Unable to add ${permissionName} permission for ${ + origin.href || origin + }` + ); + } + } + + for (let origin of blockList) { + Services.perms.addFromPrincipal( + Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin), + permissionName, + Ci.nsIPermissionManager.DENY_ACTION, + Ci.nsIPermissionManager.EXPIRE_POLICY + ); + } +} + +/** + * runOnce + * + * Helper function to run a callback only once per policy. + * + * @param {string} actionName + * A given name which will be used to track if this callback has run. + * @param {Functon} callback + * The callback to run only once. + */ +// eslint-disable-next-line no-unused-vars +export function runOnce(actionName, callback) { + let prefName = `browser.policies.runonce.${actionName}`; + if (Services.prefs.getBoolPref(prefName, false)) { + lazy.log.debug( + `Not running action ${actionName} again because it has already run.` + ); + return; + } + Services.prefs.setBoolPref(prefName, true); + callback(); +} + +/** + * runOncePerModification + * + * Helper function similar to runOnce. The difference is that runOnce runs the + * callback once when the policy is set, then never again. + * runOncePerModification runs the callback once each time the policy value + * changes from its previous value. + * If the callback that was passed is an async function, you can await on this + * function to await for the callback. + * + * @param {string} actionName + * A given name which will be used to track if this callback has run. + * This string will be part of a pref name. + * @param {string} policyValue + * The current value of the policy. This will be compared to previous + * values given to this function to determine if the policy value has + * changed. Regardless of the data type of the policy, this must be a + * string. + * @param {Function} callback + * The callback to be run when the pref value changes + * @returns {Promise} + * A promise that will resolve once the callback finishes running. + * + */ +async function runOncePerModification(actionName, policyValue, callback) { + let prefName = `browser.policies.runOncePerModification.${actionName}`; + let oldPolicyValue = Services.prefs.getStringPref(prefName, undefined); + if (policyValue === oldPolicyValue) { + lazy.log.debug( + `Not running action ${actionName} again because the policy's value is unchanged` + ); + return Promise.resolve(); + } + Services.prefs.setStringPref(prefName, policyValue); + return callback(); +} + +/** + * clearRunOnceModification + * + * Helper function that clears a runOnce policy. + */ +function clearRunOnceModification(actionName) { + let prefName = `browser.policies.runOncePerModification.${actionName}`; + Services.prefs.clearUserPref(prefName); +} + +function replacePathVariables(path) { + if (path.includes("${home}")) { + return path.replace( + "${home}", + Services.dirsvc.get("Home", Ci.nsIFile).path + ); + } + return path; +} + +/** + * installAddonFromURL + * + * Helper function that installs an addon from a URL + * and verifies that the addon ID matches. + */ +function installAddonFromURL(url, extensionID, addon) { + if ( + addon && + addon.sourceURI && + addon.sourceURI.spec == url && + !addon.sourceURI.schemeIs("file") + ) { + // It's the same addon, don't reinstall. + return; + } + lazy.AddonManager.getInstallForURL(url, { + telemetryInfo: { source: "enterprise-policy" }, + }).then(install => { + if (install.addon && install.addon.appDisabled) { + lazy.log.error(`Incompatible add-on - ${install.addon.id}`); + install.cancel(); + return; + } + let listener = { + /* eslint-disable-next-line no-shadow */ + onDownloadEnded: install => { + // Install failed, error will be reported elsewhere. + if (!install.addon) { + return; + } + if (extensionID && install.addon.id != extensionID) { + lazy.log.error( + `Add-on downloaded from ${url} had unexpected id (got ${install.addon.id} expected ${extensionID})` + ); + install.removeListener(listener); + install.cancel(); + } + if (install.addon.appDisabled) { + lazy.log.error(`Incompatible add-on - ${url}`); + install.removeListener(listener); + install.cancel(); + } + if ( + addon && + Services.vc.compare(addon.version, install.addon.version) == 0 + ) { + lazy.log.debug( + "Installation cancelled because versions are the same" + ); + install.removeListener(listener); + install.cancel(); + } + }, + onDownloadFailed: () => { + install.removeListener(listener); + lazy.log.error( + `Download failed - ${lazy.AddonManager.errorToString( + install.error + )} - ${url}` + ); + clearRunOnceModification("extensionsInstall"); + }, + onInstallFailed: () => { + install.removeListener(listener); + lazy.log.error( + `Installation failed - ${lazy.AddonManager.errorToString( + install.error + )} - {url}` + ); + }, + /* eslint-disable-next-line no-shadow */ + onInstallEnded: (install, addon) => { + if (addon.type == "theme") { + addon.enable(); + } + install.removeListener(listener); + lazy.log.debug(`Installation succeeded - ${url}`); + }, + }; + // If it's a local file install, onDownloadEnded is never called. + // So we call it manually, to handle some error cases. + if (url.startsWith("file:")) { + listener.onDownloadEnded(install); + if (install.state == lazy.AddonManager.STATE_CANCELLED) { + return; + } + } + install.addListener(listener); + install.install(); + }); +} + +let gBlockedAboutPages = []; + +function clearBlockedAboutPages() { + gBlockedAboutPages = []; +} + +function blockAboutPage(manager, feature, neededOnContentProcess = false) { + addChromeURLBlocker(); + gBlockedAboutPages.push(feature); + + try { + let aboutModule = Cc[ABOUT_CONTRACT + feature.split(":")[1]].getService( + Ci.nsIAboutModule + ); + let chromeURL = aboutModule.getChromeURI(Services.io.newURI(feature)).spec; + gBlockedAboutPages.push(chromeURL); + } catch (e) { + // Some about pages don't have chrome URLS (compat) + } +} + +let ChromeURLBlockPolicy = { + shouldLoad(contentLocation, loadInfo) { + let contentType = loadInfo.externalContentPolicyType; + if ( + (contentLocation.scheme != "chrome" && + contentLocation.scheme != "about") || + (contentType != Ci.nsIContentPolicy.TYPE_DOCUMENT && + contentType != Ci.nsIContentPolicy.TYPE_SUBDOCUMENT) + ) { + return Ci.nsIContentPolicy.ACCEPT; + } + let contentLocationSpec = contentLocation.spec.toLowerCase(); + if ( + gBlockedAboutPages.some(function (aboutPage) { + return contentLocationSpec.startsWith(aboutPage.toLowerCase()); + }) + ) { + return Ci.nsIContentPolicy.REJECT_POLICY; + } + return Ci.nsIContentPolicy.ACCEPT; + }, + shouldProcess(contentLocation, loadInfo) { + return Ci.nsIContentPolicy.ACCEPT; + }, + classDescription: "Policy Engine Content Policy", + contractID: "@mozilla-org/policy-engine-content-policy-service;1", + classID: Components.ID("{ba7b9118-cabc-4845-8b26-4215d2a59ed7}"), + QueryInterface: ChromeUtils.generateQI(["nsIContentPolicy"]), + createInstance(iid) { + return this.QueryInterface(iid); + }, +}; + +function addChromeURLBlocker() { + if (Cc[ChromeURLBlockPolicy.contractID]) { + return; + } + + let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + registrar.registerFactory( + ChromeURLBlockPolicy.classID, + ChromeURLBlockPolicy.classDescription, + ChromeURLBlockPolicy.contractID, + ChromeURLBlockPolicy + ); + + Services.catMan.addCategoryEntry( + "content-policy", + ChromeURLBlockPolicy.contractID, + ChromeURLBlockPolicy.contractID, + false, + true + ); +} + +function pemToBase64(pem) { + return pem + .replace(/(.*)-----BEGIN CERTIFICATE-----/, "") + .replace(/-----END CERTIFICATE-----(.*)/, "") + .replace(/[\r\n]/g, ""); +} + +function processMIMEInfo(mimeInfo, realMIMEInfo) { + if ("handlers" in mimeInfo) { + let firstHandler = true; + for (let handler of mimeInfo.handlers) { + // handler can be null which means they don't + // want a preferred handler. + if (handler) { + let handlerApp; + if ("path" in handler) { + try { + let file = new lazy.FileUtils.File(handler.path); + handlerApp = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + handlerApp.executable = file; + } catch (ex) { + lazy.log.error( + `Unable to create handler executable (${handler.path})` + ); + continue; + } + } else if ("uriTemplate" in handler) { + let templateURL = new URL(handler.uriTemplate); + if (templateURL.protocol != "https:") { + lazy.log.error( + `Web handler must be https (${handler.uriTemplate})` + ); + continue; + } + if ( + !templateURL.pathname.includes("%s") && + !templateURL.search.includes("%s") + ) { + lazy.log.error( + `Web handler must contain %s (${handler.uriTemplate})` + ); + continue; + } + handlerApp = Cc[ + "@mozilla.org/uriloader/web-handler-app;1" + ].createInstance(Ci.nsIWebHandlerApp); + handlerApp.uriTemplate = handler.uriTemplate; + } else { + lazy.log.error("Invalid handler"); + continue; + } + if ("name" in handler) { + handlerApp.name = handler.name; + } + realMIMEInfo.possibleApplicationHandlers.appendElement(handlerApp); + if (firstHandler) { + realMIMEInfo.preferredApplicationHandler = handlerApp; + } + } + firstHandler = false; + } + } + if ("action" in mimeInfo) { + let action = realMIMEInfo[mimeInfo.action]; + if ( + action == realMIMEInfo.useHelperApp && + !realMIMEInfo.possibleApplicationHandlers.length + ) { + lazy.log.error("useHelperApp requires a handler"); + return; + } + realMIMEInfo.preferredAction = action; + } + if ("ask" in mimeInfo) { + realMIMEInfo.alwaysAskBeforeHandling = mimeInfo.ask; + } + lazy.gHandlerService.store(realMIMEInfo); +} |