/* 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/. */ /** * Utilities for looking up error configurations and resolving dynamic content. * * Some error configurations require runtime data (e.g., hostname, certificate * validity dates, connectivity status). This module provides resolver functions * that inject runtime context into static configurations. */ import { getErrorConfig, isErrorSupported, } from "chrome://global/content/errors/error-registry.mjs"; /** * Resolve browser error signals to a canonical registry ID. * * Handles resolution scenarios in priority order: * 1. System state overrides (no connectivity, VPN active) * 2. Direct registry lookup by errorCodeString (C++ error strings) * 3. Fallback to a shared config when errorCodeString is unregistered * 4. Direct registry lookup by gErrorCode (URL-derived error codes) * * @param {object} signals * @param {string} signals.errorCodeString - Raw error string from the security/network layer * @param {string} signals.gErrorCode - URL-derived error code (e= parameter) * @param {boolean} signals.noConnectivity - System has no network connectivity * @param {boolean} signals.vpnActive - VPN is currently active * @returns {string|null} Canonical registry ID, or null if the error has no * Felt Privacy config. */ export function resolveErrorID({ errorCodeString, gErrorCode, noConnectivity, vpnActive, }) { // System state overrides. // Currently these are all network-related. if (noConnectivity) { return "NS_ERROR_OFFLINE"; } if (gErrorCode === "proxyConnectFailure" && vpnActive) { return "vpnFailure"; } // Default: use errorCodeString if it is set and registered. This value comes // from getFailedCertSecurityInfo() or getNetErrorInfo() and identifies the // specific error. if (errorCodeString && isErrorSupported(errorCodeString)) { return errorCodeString; } // In some cases, errorCodeString will be set but not registered. This can // be done on purpose to allow many different yet similar errors to share a // config in the error registry. For example, if we see an unregistered error // where gErrorCode is "nssFailure2", we use the nssFailure2 registry entry. if (errorCodeString) { if (gErrorCode !== "nssFailure2" || !isErrorSupported("nssFailure2")) { return null; } } // In some cases, errorCodeString will not be set. Use the code in // gErrorCode, which is parsed from the URL e= parameter and set by // nsDocShell, as a fallback. if (gErrorCode && isErrorSupported(gErrorCode)) { return gErrorCode; } return null; } /** * Check if an error has no action the user can take to fix it. * * @param {string} id - The error id to check * @returns {boolean} True if the error has no user fix */ export function errorHasNoUserFix(id) { const config = getErrorConfig(id); return config ? config.hasNoUserFix === true : false; } /** * Check if an error is supported by the Felt Privacy v1 experience. * * @param {string} id - The error id to check * @returns {boolean} True if the error has a configuration */ export function isFeltPrivacySupported(id) { const config = getErrorConfig(id); return !!config; } /** * Resolve l10n arguments by injecting runtime context. * * @param {object | null} l10nConfig - The l10n config with { dataL10nId, dataL10nArgs } * @param {object} l10nArgValues - Context from the environment during runtime (hostname, errorInfo, etc.) * @returns {object | null} Resolved l10n config with dataL10nArgs filled in */ export function resolveL10nArgs(l10nConfig, l10nArgValues) { if (!l10nConfig?.dataL10nArgs) { return l10nConfig; } const values = { hostname: l10nArgValues.hostname, path: l10nArgValues.filePath, date: Date.now(), now: l10nArgValues.now ?? Date.now(), errorMessage: l10nArgValues.errorInfo?.errorMessage ?? "", errorCodeString: l10nArgValues.errorInfo?.errorCodeString ?? "", validHosts: l10nArgValues.domainMismatchNames ?? "", mitm: l10nArgValues.mitmName ?? "", responsestatus: l10nArgValues.errorInfo?.responseStatus ?? 0, responsestatustext: l10nArgValues.errorInfo?.responseStatusText ?? "", }; if (typeof l10nConfig.dataL10nId === "function") { l10nConfig.dataL10nId = l10nConfig.dataL10nId(l10nArgValues); } for (const [key, value] of Object.entries(l10nConfig.dataL10nArgs)) { if (value === null || value === "") { l10nConfig.dataL10nArgs[key] = values[key]; } else if (typeof value === "function") { l10nConfig.dataL10nArgs[key] = value(l10nArgValues); } } return l10nConfig; } /** * Resolve an array of l10n arguments by injecting runtime context. * * @param {Array | null} l10nConfig - The l10n config(s) with { dataL10nId, dataL10nArgs } * @param {object} l10nArgValues - Context from the environment during runtime (hostname, errorInfo, etc.) * @returns {Array | null} Resolved l10n config with dataL10nArgs filled in */ export function resolveManyL10nArgs(l10nConfigs, l10nArgValues) { if (!l10nConfigs) { return null; } for (let i = 0; i < l10nConfigs.length; i++) { l10nConfigs[i] = resolveL10nArgs(l10nConfigs[i], l10nArgValues); } return l10nConfigs; } /** * Resolve description parts by calling resolver functions for dynamic content. * * @param {Array|Function} descriptionParts - Static parts array or resolver function * @param {object} l10nArgValues - Context from the environment during runtime { noConnectivity, hostname, errorInfo } * @returns {Array} Resolved description parts */ export function resolveDescriptionParts(descriptionParts, l10nArgValues) { if (!descriptionParts) { return []; } if (typeof descriptionParts === "function") { return descriptionParts(l10nArgValues); } // Static parts - resolve any l10n args return descriptionParts.map(part => resolveL10nArgs(part, l10nArgValues)); } /** * Resolve the advanced section configuration. * * @param {object | null} advancedConfig - The advanced section config * @param {object} l10nArgValues - Context from the environment during runtime * @returns {object | null} Resolved advanced config */ export function resolveAdvancedConfig(advancedConfig, l10nArgValues) { if (!advancedConfig) { return null; } const resolved = { ...advancedConfig }; ["whyDangerous", "whatCanYouDo", "learnMore"].forEach(key => { if (resolved[key]) { resolved[key] = resolveL10nArgs(advancedConfig[key], l10nArgValues); } }); return resolved; } function resolveCustomNetError(customNetError, l10nArgValues) { if (!customNetError) { return customNetError; } const resolved = { ...customNetError }; if (typeof resolved.whatCanYouDoItems === "function") { resolved.whatCanYouDoItems = resolved.whatCanYouDoItems(l10nArgValues); } if (customNetError.whatCanYouDoL10nArgs) { const argsClone = { dataL10nArgs: { ...customNetError.whatCanYouDoL10nArgs }, }; resolveL10nArgs(argsClone, l10nArgValues); resolved.whatCanYouDoL10nArgs = argsClone.dataL10nArgs; } return resolved; } /** * Get a fully resolved error configuration with runtime context applied. * * @param {string} id - The error id to look up * @param {object} l10nArgValues - Context from the environment during runtime { hostname, errorInfo, noConnectivity, showOSXPermissionWarning, offline } * @returns {object} Fully resolved error configuration */ export function getResolvedErrorConfig(id, l10nArgValues) { const baseConfig = getErrorConfig(id); if (!baseConfig) { return {}; } const introContentHandler = Array.isArray(baseConfig.introContent) ? resolveManyL10nArgs : resolveL10nArgs; return { ...baseConfig, introContent: introContentHandler(baseConfig.introContent, l10nArgValues), shortDescription: resolveL10nArgs( baseConfig.shortDescription, l10nArgValues ), descriptionParts: resolveDescriptionParts( baseConfig.descriptionParts, l10nArgValues ), advanced: resolveAdvancedConfig(baseConfig.advanced, l10nArgValues), customNetError: resolveCustomNetError( baseConfig.customNetError, l10nArgValues ), }; }