/*TRANSPILED*/goog.loadModule(function(exports) {'use strict';/** * @license * Copyright The Closure Library Authors. * SPDX-License-Identifier: Apache-2.0 */ /** * @fileoverview Closure user agent detection (Browser). * @see User agent strings * For more information on rendering engine, platform, or device see the other * sub-namespaces in goog.labs.userAgent, goog.labs.userAgent.platform, * goog.labs.userAgent.device respectively.) */ goog.module('goog.labs.userAgent.browser'); goog.module.declareLegacyNamespace(); const util = goog.require('goog.labs.userAgent.util'); const {AsyncValue, Version} = goog.require('goog.labs.userAgent.highEntropy.highEntropyValue'); const {assert, assertExists} = goog.require('goog.asserts'); const {compareVersions} = goog.require('goog.string.internal'); const {fullVersionList} = goog.require('goog.labs.userAgent.highEntropy.highEntropyData'); const {useClientHints} = goog.require('goog.labs.userAgent'); // TODO(nnaze): Refactor to remove excessive exclusion logic in matching // functions. /** * A browser brand represents an opaque string that is used for making * brand-specific version checks in userAgentData. * @enum {string} */ const Brand = { /** * The browser brand for Android Browser. * Do not depend on the value of this string. Because Android Browser has not * implemented userAgentData yet, the value of this string is not guaranteed * to stay the same in future revisions. */ ANDROID_BROWSER: 'Android Browser', /** * The browser brand for Chromium, including Chromium-based Edge and Opera. */ CHROMIUM: 'Chromium', /** * The browser brand for Edge. * This brand can be used to get the version of both EdgeHTML and * Chromium-based Edge. */ EDGE: 'Microsoft Edge', /** * The browser brand for Firefox. * Do not depend on the value of this string. Because Firefox has not * implemented userAgentData yet, the value of this string is not guaranteed * to stay the same in future revisions. */ FIREFOX: 'Firefox', /** * The browser brand for Internet Explorer. * Do not depend on the value of this string. Because IE will never support * userAgentData, the value of this string should be treated as opaque (it's * used internally for legacy-userAgent fallback). */ IE: 'Internet Explorer', /** * The browser brand for Opera. * This brand can be used to get the version of both Presto- and * Chromium-based Opera. */ OPERA: 'Opera', /** * The browser brand for Safari. * Do not depend on the value of this string. Because Safari has not * implemented userAgentData yet, the value of this string is not guaranteed * to stay the same in future revisions. */ SAFARI: 'Safari', /** * The browser brand for Silk. * See * https://docs.aws.amazon.com/silk/latest/developerguide/what-is-silk.html * Do not depend on the value of this string. Because Silk does not * identify itself in userAgentData yet, the value of this string is not * guaranteed to stay the same in future revisions. */ SILK: 'Silk', }; exports.Brand = Brand; /** * @param {boolean=} ignoreClientHintsFlag Iff truthy, the `useClientHints` * function will not be called when evaluating if User-Agent Client Hints * Brand data can be used. For existing labs.userAgent API surfaces with * widespread use, this should be a falsy value so that usage of the Client * Hints APIs can be gated behind flags / experiment rollouts. * @return {boolean} Whether to use navigator.userAgentData to determine * browser's brand. */ function useUserAgentDataBrand(ignoreClientHintsFlag = false) { if (util.ASSUME_CLIENT_HINTS_SUPPORT) return true; // High-entropy API surfaces should not be gated behind the useClientHints // check (as in production it is gated behind a define). if (!ignoreClientHintsFlag && !useClientHints()) return false; const userAgentData = util.getUserAgentData(); return !!userAgentData && userAgentData.brands.length > 0; } /** * @return {boolean} Whether this browser is likely to have the fullVersionList * high-entropy Client Hint. */ function hasFullVersionList() { // https://chromiumdash.appspot.com/commits?commit=1eb643c3057e64ff4d22468432ad16c4cab12879&platform=Linux // indicates that for all platforms Chromium 98 shipped this feature. // See also // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-UA-Full-Version-List#browser_compatibility return isAtLeast(Brand.CHROMIUM, 98); } /** * @return {boolean} Whether the user's browser is Opera. Note: Chromium based * Opera (Opera 15+) is detected as Chrome to avoid unnecessary special * casing. */ function matchOpera() { if (useUserAgentDataBrand()) { // Pre-Chromium Edge doesn't support navigator.userAgentData. return false; } return util.matchUserAgent('Opera'); } /** @return {boolean} Whether the user's browser is IE. */ function matchIE() { if (useUserAgentDataBrand()) { // IE doesn't support navigator.userAgentData. return false; } return util.matchUserAgent('Trident') || util.matchUserAgent('MSIE'); } /** * @return {boolean} Whether the user's browser is Edge. This refers to * EdgeHTML based Edge. */ function matchEdgeHtml() { if (useUserAgentDataBrand()) { // Pre-Chromium Edge doesn't support navigator.userAgentData. return false; } return util.matchUserAgent('Edge'); } /** @return {boolean} Whether the user's browser is Chromium based Edge. */ function matchEdgeChromium() { if (useUserAgentDataBrand()) { return util.matchUserAgentDataBrand(Brand.EDGE); } return util.matchUserAgent('Edg/'); } /** @return {boolean} Whether the user's browser is Chromium based Opera. */ function matchOperaChromium() { if (useUserAgentDataBrand()) { return util.matchUserAgentDataBrand(Brand.OPERA); } return util.matchUserAgent('OPR'); } /** @return {boolean} Whether the user's browser is Firefox. */ function matchFirefox() { // Firefox doesn't support navigator.userAgentData yet, so use // navigator.userAgent. return util.matchUserAgent('Firefox') || util.matchUserAgent('FxiOS'); } /** @return {boolean} Whether the user's browser is Safari. */ function matchSafari() { // Apple-based browsers don't support navigator.userAgentData yet, so use // navigator.userAgent. return util.matchUserAgent('Safari') && !(matchChrome() || matchCoast() || matchOpera() || matchEdgeHtml() || matchEdgeChromium() || matchOperaChromium() || matchFirefox() || isSilk() || util.matchUserAgent('Android')); } /** * @return {boolean} Whether the user's browser is Coast (Opera's Webkit-based * iOS browser). */ function matchCoast() { if (useUserAgentDataBrand()) { // Coast doesn't support navigator.userAgentData. return false; } return util.matchUserAgent('Coast'); } /** @return {boolean} Whether the user's browser is iOS Webview. */ function matchIosWebview() { // Apple-based browsers don't support navigator.userAgentData yet, so use // navigator.userAgent. // iOS Webview does not show up as Chrome or Safari. return (util.matchUserAgent('iPad') || util.matchUserAgent('iPhone')) && !matchSafari() && !matchChrome() && !matchCoast() && !matchFirefox() && util.matchUserAgent('AppleWebKit'); } /** * @return {boolean} Whether the user's browser is any Chromium browser. This * returns true for Chrome, Opera 15+, and Edge Chromium. */ function matchChrome() { if (useUserAgentDataBrand()) { return util.matchUserAgentDataBrand(Brand.CHROMIUM); } return ((util.matchUserAgent('Chrome') || util.matchUserAgent('CriOS')) && !matchEdgeHtml()) || isSilk(); } /** @return {boolean} Whether the user's browser is the Android browser. */ function matchAndroidBrowser() { // Android can appear in the user agent string for Chrome on Android. // This is not the Android standalone browser if it does. return util.matchUserAgent('Android') && !(isChrome() || isFirefox() || isOpera() || isSilk()); } /** @return {boolean} Whether the user's browser is Opera. */ const isOpera = matchOpera; exports.isOpera = isOpera; /** @return {boolean} Whether the user's browser is IE. */ const isIE = matchIE; exports.isIE = isIE; /** @return {boolean} Whether the user's browser is EdgeHTML based Edge. */ const isEdge = matchEdgeHtml; exports.isEdge = isEdge; /** @return {boolean} Whether the user's browser is Chromium based Edge. */ const isEdgeChromium = matchEdgeChromium; exports.isEdgeChromium = isEdgeChromium; /** @return {boolean} Whether the user's browser is Chromium based Opera. */ const isOperaChromium = matchOperaChromium; exports.isOperaChromium = isOperaChromium; /** @return {boolean} Whether the user's browser is Firefox. */ const isFirefox = matchFirefox; exports.isFirefox = isFirefox; /** @return {boolean} Whether the user's browser is Safari. */ const isSafari = matchSafari; exports.isSafari = isSafari; /** * @return {boolean} Whether the user's browser is Coast (Opera's Webkit-based * iOS browser). */ const isCoast = matchCoast; exports.isCoast = isCoast; /** @return {boolean} Whether the user's browser is iOS Webview. */ const isIosWebview = matchIosWebview; exports.isIosWebview = isIosWebview; /** * @return {boolean} Whether the user's browser is any Chromium based browser ( * Chrome, Blink-based Opera (15+) and Edge Chromium). */ const isChrome = matchChrome; exports.isChrome = isChrome; /** @return {boolean} Whether the user's browser is the Android browser. */ const isAndroidBrowser = matchAndroidBrowser; exports.isAndroidBrowser = isAndroidBrowser; /** * For more information, see: * http://docs.aws.amazon.com/silk/latest/developerguide/user-agent.html * @return {boolean} Whether the user's browser is Silk. */ function isSilk() { // As of Silk 93, Silk does not identify itself in userAgentData.brands. // When Silk changes this behavior, update this method to call // matchUserAgentDataBrand (akin to isChrome, etc.) return util.matchUserAgent('Silk'); } exports.isSilk = isSilk; /** * A helper function that returns a function mapping a list of candidate * version tuple keys to the first version string present under a key. * Ex: * * // Arg extracted from "Foo/1.2.3 Bar/0.2021" * const mapVersion = createVersionMap([["Foo", "1.2.3"], ["Bar", "0.2021"]]); * mapVersion(["Bar", "Foo"]); // returns "0.2021" * mapVersion(["Baz", "Foo"]); // returns "1.2.3" * mapVersion(["Baz", "???"]); // returns "" * * @param {!Array>} versionTuples Version tuples pre-extracted * from a user agent string. * @return {function(!Array): string} The version string, or empty * string if it doesn't exist under the given key. */ function createVersionMap(versionTuples) { // Construct a map for easy lookup. const versionMap = {}; versionTuples.forEach((tuple) => { // Note that the tuple is of length three, but we only care about the // first two. const key = tuple[0]; const value = tuple[1]; versionMap[key] = value; }); // Gives the value with the first key it finds, otherwise empty string. return (keys) => versionMap[keys.find((key) => key in versionMap)] || ''; } /** * Returns the browser version. * * Note that for browsers with multiple brands, this function assumes a primary * brand and returns the version for that brand. * * Additionally, this function is not userAgentData-aware and will return * incorrect values when the User Agent string is frozen. The current status of * User Agent string freezing is available here: * https://www.chromestatus.com/feature/5704553745874944 * * To mitigate both of these potential issues, use * getVersionStringForLogging() or fullVersionOf() instead. * * @return {string} The browser version or empty string if version cannot be * determined. Note that for Internet Explorer, this returns the version of * the browser, not the version of the rendering engine. (IE 8 in * compatibility mode will return 8.0 rather than 7.0. To determine the * rendering engine version, look at document.documentMode instead. See * http://msdn.microsoft.com/en-us/library/cc196988(v=vs.85).aspx for more * details.) */ function getVersion() { const userAgentString = util.getUserAgent(); // Special case IE since IE's version is inside the parenthesis and // without the '/'. if (isIE()) { return getIEVersion(userAgentString); } const versionTuples = util.extractVersionTuples(userAgentString); const lookUpValueWithKeys = createVersionMap(versionTuples); // Check Opera before Chrome since Opera 15+ has "Chrome" in the string. // See // http://my.opera.com/ODIN/blog/2013/07/15/opera-user-agent-strings-opera-15-and-beyond if (isOpera()) { // Opera 10 has Version/10.0 but Opera/9.8, so look for "Version" first. // Opera uses 'OPR' for more recent UAs. return lookUpValueWithKeys(['Version', 'Opera']); } // Check Edge before Chrome since it has Chrome in the string. if (isEdge()) { return lookUpValueWithKeys(['Edge']); } // Check Chromium Edge before Chrome since it has Chrome in the string. if (isEdgeChromium()) { return lookUpValueWithKeys(['Edg']); } // Check Silk before Chrome since it may have Chrome in its string and be // treated as Chrome. if (isSilk()) { return lookUpValueWithKeys(['Silk']); } if (isChrome()) { return lookUpValueWithKeys(['Chrome', 'CriOS', 'HeadlessChrome']); } // Usually products browser versions are in the third tuple after "Mozilla" // and the engine. const tuple = versionTuples[2]; return tuple && tuple[1] || ''; } exports.getVersion = getVersion; /** * Returns whether the current browser's version is at least as high as the * given one. * * Note that for browsers with multiple brands, this function assumes a primary * brand and checks the version for that brand. * * Additionally, this function is not userAgentData-aware and will return * incorrect values when the User Agent string is frozen. The current status of * User Agent string freezing is available here: * https://www.chromestatus.com/feature/5704553745874944 * * To mitigate both of these potential issues, use isAtLeast()/isAtMost() or * fullVersionOf() instead. * * @param {string|number} version The version to check. * @return {boolean} Whether the browser version is higher or the same as the * given version. * @deprecated Use isAtLeast()/isAtMost() instead. */ function isVersionOrHigher(version) { return compareVersions(getVersion(), version) >= 0; } exports.isVersionOrHigher = isVersionOrHigher; /** * A helper function to determine IE version. More information: * http://msdn.microsoft.com/en-us/library/ie/bg182625(v=vs.85).aspx#uaString * http://msdn.microsoft.com/en-us/library/hh869301(v=vs.85).aspx * http://blogs.msdn.com/b/ie/archive/2010/03/23/introducing-ie9-s-user-agent-string.aspx * http://blogs.msdn.com/b/ie/archive/2009/01/09/the-internet-explorer-8-user-agent-string-updated-edition.aspx * @param {string} userAgent the User-Agent. * @return {string} */ function getIEVersion(userAgent) { // IE11 may identify itself as MSIE 9.0 or MSIE 10.0 due to an IE 11 upgrade // bug. Example UA: // Mozilla/5.0 (MSIE 9.0; Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) // like Gecko. // See http://www.whatismybrowser.com/developers/unknown-user-agent-fragments. const rv = /rv: *([\d\.]*)/.exec(userAgent); if (rv && rv[1]) { return rv[1]; } let version = ''; const msie = /MSIE +([\d\.]+)/.exec(userAgent); if (msie && msie[1]) { // IE in compatibility mode usually identifies itself as MSIE 7.0; in this // case, use the Trident version to determine the version of IE. For more // details, see the links above. const tridentVersion = /Trident\/(\d.\d)/.exec(userAgent); if (msie[1] == '7.0') { if (tridentVersion && tridentVersion[1]) { switch (tridentVersion[1]) { case '4.0': version = '8.0'; break; case '5.0': version = '9.0'; break; case '6.0': version = '10.0'; break; case '7.0': version = '11.0'; break; } } else { version = '7.0'; } } else { version = msie[1]; } } return version; } /** * A helper function to return the navigator.userAgent-supplied full version * number of the current browser or an empty string, based on whether the * current browser is the one specified. * @param {string} browser The brand whose version should be returned. * @return {string} */ function getFullVersionFromUserAgentString(browser) { const userAgentString = util.getUserAgent(); // Special case IE since IE's version is inside the parenthesis and // without the '/'. if (browser === Brand.IE) { return isIE() ? getIEVersion(userAgentString) : ''; } const versionTuples = util.extractVersionTuples(userAgentString); const lookUpValueWithKeys = createVersionMap(versionTuples); switch (browser) { case Brand.OPERA: // Opera 10 has Version/10.0 but Opera/9.8, so look for "Version" // first. Opera uses 'OPR' for more recent UAs. if (isOpera()) { return lookUpValueWithKeys(['Version', 'Opera']); } else if (isOperaChromium()) { return lookUpValueWithKeys(['OPR']); } break; case Brand.EDGE: if (isEdge()) { return lookUpValueWithKeys(['Edge']); } else if (isEdgeChromium()) { return lookUpValueWithKeys(['Edg']); } break; case Brand.CHROMIUM: if (isChrome()) { return lookUpValueWithKeys(['Chrome', 'CriOS', 'HeadlessChrome']); } break; } // For the following browsers, the browser version is in the third tuple after // "Mozilla" and the engine. if ((browser === Brand.FIREFOX && isFirefox()) || (browser === Brand.SAFARI && isSafari()) || (browser === Brand.ANDROID_BROWSER && isAndroidBrowser()) || (browser === Brand.SILK && isSilk())) { const tuple = versionTuples[2]; return tuple && tuple[1] || ''; } return ''; } /** * Returns the major version of the given browser brand, or NaN if the current * browser is not the given brand. * Note that the major version number may be different depending on which * browser is specified. The returned value can be used to make browser version * comparisons using comparison operators. * @private * @param {!Brand} browser The brand whose version should be returned. * @return {number} The major version number associated with the current * browser under the given brand, or NaN if the current browser doesn't match * the given brand. */ function versionOf_(browser) { let versionParts; // Silk currently does not identify itself in its userAgentData.brands array, // so if checking its version, always fall back to the user agent string. if (useUserAgentDataBrand() && browser !== Brand.SILK) { const data = util.getUserAgentData(); const matchingBrand = data.brands.find(({brand}) => brand === browser); if (!matchingBrand || !matchingBrand.version) { return NaN; } versionParts = matchingBrand.version.split('.'); } else { const fullVersion = getFullVersionFromUserAgentString(browser); if (fullVersion === '') { return NaN; } versionParts = fullVersion.split('.'); } if (versionParts.length === 0) { return NaN; } const majorVersion = versionParts[0]; return Number(majorVersion); // Returns NaN if it is not parseable. } /** * Returns true if the current browser matches the given brand and is at least * the given major version. The major version must be a whole number (i.e. * decimals should not be used to represent a minor version). * @param {!Brand} brand The brand whose version should be returned. * @param {number} majorVersion The major version number to compare against. * This must be a whole number. * @return {boolean} Whether the current browser both matches the given brand * and is at least the given version. */ function isAtLeast(brand, majorVersion) { assert( Math.floor(majorVersion) === majorVersion, 'Major version must be an integer'); return versionOf_(brand) >= majorVersion; } exports.isAtLeast = isAtLeast; /** * Returns true if the current browser matches the given brand and is at most * the given version. The major version must be a whole number (i.e. decimals * should not be used to represent a minor version). * @param {!Brand} brand The brand whose version should be returned. * @param {number} majorVersion The major version number to compare against. * This must be a whole number. * @return {boolean} Whether the current browser both matches the given brand * and is at most the given version. */ function isAtMost(brand, majorVersion) { assert( Math.floor(majorVersion) === majorVersion, 'Major version must be an integer'); return versionOf_(brand) <= majorVersion; } exports.isAtMost = isAtMost; /** * Loads the high-entropy browser brand/version data and wraps the correct * version string in a Version object. * @implements {AsyncValue} */ class HighEntropyBrandVersion { /** * @param {string} brand The brand whose version is retrieved in this * container. * @param {boolean} useUach Whether to attempt to use the User-Agent Client * Hints (UACH) API surface. * @param {string} fallbackVersion The fallback version derived from the * userAgent string. */ constructor(brand, useUach, fallbackVersion) { /** @private @const {string} */ this.brand_ = brand; /** @private @const {!Version} */ this.version_ = new Version(fallbackVersion); /** @private @const {boolean} */ this.useUach_ = useUach; } /** * @return {!Version|undefined} * @override */ getIfLoaded() { if (this.useUach_) { const loadedVersionList = fullVersionList.getIfLoaded(); if (loadedVersionList !== undefined) { const matchingBrand = loadedVersionList.find(({brand}) => this.brand_ === brand); // We assumed in fullVersionOf that if the fullVersionList is defined // the brands must match. Double-check this here. assertExists(matchingBrand); return new Version(matchingBrand.version); } // Fallthrough to fallback on Pre-UACH implementation } // We want to make sure the loading semantics of the Pre-UACH implementation // match those of the UACH implementation. Loading must happen before any // data can be retrieved from getIfLoaded. // For HighEntropyBrandVersion, loading can either be done by calling #load // or by calling the module-local loadFullVersions function. if (preUachHasLoaded) { return this.version_; } return; } /** * @return {!Promise} * @override */ async load() { if (this.useUach_) { const loadedVersionList = await fullVersionList.load(); if (loadedVersionList !== undefined) { const matchingBrand = loadedVersionList.find(({brand}) => this.brand_ === brand); assertExists(matchingBrand); return new Version(matchingBrand.version); } // Fallthrough to fallback on Pre-UACH implementation } else { // Await something so that calling load with or without UACH API // availability results in waiting at least one macrotask before allowing // access to the cached version information. await 0; } // Regardless of whether we are using UACH APIs, we can now allow access to // the fallback case preUachHasLoaded = true; return this.version_; } } /** * Whether full version data should be considered available when using UACH * fallback implementations. This is flipped to true when either * loadFullVersions or HighEntropyBrandVersion.prototype.load are called, * matching the global singleton semantics of the UACH codepaths. */ let preUachHasLoaded = false; /** * Requests all full browser versions to be cached. When the returned promise * resolves, subsequent calls to `fullVersionOf(...).getIfLoaded()` will return * a value. * * This method should be avoided in favor of directly awaiting * `fullVersionOf(...).load()` where it is used. * * @return {!Promise} */ async function loadFullVersions() { if (useUserAgentDataBrand(true)) { await fullVersionList.load(); } preUachHasLoaded = true; } exports.loadFullVersions = loadFullVersions; /** * Resets module-local caches used by functionality in this module. * This is only for use by goog.labs.userAgent.testUtil.resetUserAgent (and * labs.userAgent tests). * @package */ exports.resetForTesting = () => { preUachHasLoaded = false; fullVersionList.resetForTesting(); }; /** * Returns an object that provides access to the full version string of the * current browser -- or undefined, based on whether the current browser matches * the requested browser brand. Note that the full version string is a * high-entropy value, and must be asynchronously loaded before it can be * accessed synchronously. * @param {!Brand} browser The brand whose version should be returned. * @return {!AsyncValue|undefined} An object that can be used * to get or load the full version string as a high-entropy value, or * undefined if the current browser doesn't match the given brand. */ function fullVersionOf(browser) { let fallbackVersionString = ''; // If we are reasonably certain now that the browser we are on has the // fullVersionList high-entropy hint, then we can skip computing the fallback // value as we won't end up using it. if (!hasFullVersionList()) { fallbackVersionString = getFullVersionFromUserAgentString(browser); } // Silk has the UACH API surface, but currently does not identify itself in // the userAgentData.brands array. Fallback to using userAgent string version // for Silk. const useUach = browser !== Brand.SILK && useUserAgentDataBrand(true); if (useUach) { const data = util.getUserAgentData(); // Operate under the assumption that the low-entropy and high-entropy lists // of brand/version pairs contain an identical set of brands. Therefore, if // the low-entropy list doesn't contain the given brand, return undefined. if (!data.brands.find(({brand}) => brand === browser)) { return undefined; } } else if (fallbackVersionString === '') { return undefined; } return new HighEntropyBrandVersion(browser, useUach, fallbackVersionString); } exports.fullVersionOf = fullVersionOf; /** * Returns a version string for the current browser or undefined, based on * whether the current browser is the one specified. * This value should ONLY be used for logging/debugging purposes. Do not use it * to branch code paths. For comparing versions, use isAtLeast()/isAtMost() or * fullVersionOf() instead. * @param {!Brand} browser The brand whose version should be returned. * @return {string} The version as a string. */ function getVersionStringForLogging(browser) { if (useUserAgentDataBrand(true)) { const fullVersionObj = fullVersionOf(browser); if (fullVersionObj) { const fullVersion = fullVersionObj.getIfLoaded(); if (fullVersion) { return fullVersion.toVersionStringForLogging(); } // No full version, return the major version instead. const data = util.getUserAgentData(); const matchingBrand = data.brands.find(({brand}) => brand === browser); // Checking for the existence of matchingBrand is not necessary because // the existence of fullVersionObj implies that there is already a // matching brand. assertExists(matchingBrand); return matchingBrand.version; } // If fullVersionObj is undefined, this doesn't mean that the full version // is unavailable, but rather that the current browser doesn't match the // input `browser` argument. return ''; } else { return getFullVersionFromUserAgentString(browser); } } exports.getVersionStringForLogging = getVersionStringForLogging; ;return exports;});