3 * https://gerrit.wikimedia.org/g/jquery-client/
5 * Copyright 2010-2020 wikimedia/jquery-client maintainers and other contributors.
6 * Released under the MIT license
7 * https://jquery-client.mit-license.org
11 * User-agent detection
13 * @class jQuery.client
20 * @property {Object} profileCache Keyed by userAgent string,
21 * value is the parsed $.client.profile object for that user agent.
23 var profileCache = {};
28 * Get an object containing information about the client.
30 * The resulting client object will be in the following format:
35 * 'layoutVersion': 20101026,
39 * 'versionNumber': 3.5,
44 * if ( $.client.profile().layout == 'gecko' ) {
45 * // This will only run in Gecko browsers, such as Mozilla Firefox.
48 * var profile = $.client.profile();
49 * if ( profile.layout == 'gecko' && profile.platform == 'linux' ) {
50 * // This will only run in Gecko browsers on Linux.
53 * Recognised browser names:
55 * - `android` (legacy Android browser, prior to Chrome Mobile)
56 * - `chrome` (includes Chrome Mobile, Microsoft Edge, Opera, and others)
57 * - `crios` (Chrome on iOS, which uses Mobile Safari)
58 * - `edge` (legacy Microsoft Edge, which uses EdgeHTML)
59 * - `firefox` (includes Firefox Mobile, Iceweasel, and others)
60 * - `fxios` (Firefox on iOS, which uses Mobile Safari)
63 * - `opera` (legacy Opera, which uses Presto)
65 * - `safari` (including Mobile Safari)
68 * Recognised layout engines:
70 * - `edge` (EdgeHTML 12-18, as used by legacy Microsoft Edge)
77 * Note that Chrome and Chromium-based browsers like Opera have their layout
78 * engine identified as `webkit`.
80 * Recognised platforms:
86 * - `solaris` (untested)
89 * @param {Object} [nav] An object with a 'userAgent' and 'platform' property.
90 * Defaults to the global `navigator` object.
91 * @return {Object} The client object
93 profile: function ( nav ) {
95 nav = window.navigator;
98 // Use the cached version if possible
99 if ( profileCache[ nav.userAgent + '|' + nav.platform ] ) {
100 return profileCache[ nav.userAgent + '|' + nav.platform ];
103 // eslint-disable-next-line vars-on-top
106 key = nav.userAgent + '|' + nav.platform,
110 // Name of browsers or layout engines we don't recognize
112 // Generic version digit
114 // Fixups for user agent strings that contain wild words
116 // Chrome lives in the shadow of Safari still
117 [ 'Chrome Safari', 'Chrome' ],
118 // KHTML is the layout engine not the browser - LIES!
119 [ 'KHTML/', 'Konqueror/' ],
120 // For Firefox Mobile, strip out "Android;" or "Android [version]" so that we
121 // classify it as Firefox instead of Android (default browser)
122 [ /Android(?:;|\s[a-zA-Z0-9.+-]+)(.*Firefox)/, '$1' ]
124 // Strings which precede a version number in a user agent string
125 versionPrefixes = '(?:chrome|crios|firefox|fxios|opera|version|konqueror|msie|safari|android)',
126 // This matches the actual version number, with non-capturing groups for the
127 // separator and suffix
128 versionSuffix = '(?:\\/|;?\\s|)([a-z0-9\\.\\+]*?)(?:;|dev|rel|\\)|\\s|$)',
129 // Match the names of known browser families
130 rName = /(chrome|crios|firefox|fxios|konqueror|msie|opera|safari|rekonq|android)/,
131 // Match the name of known layout engines
132 rLayout = /(gecko|konqueror|msie|trident|edge|opera|webkit)/,
133 // Translations for conforming layout names
134 layoutMap = { konqueror: 'khtml', msie: 'trident', opera: 'presto' },
135 // Match the prefix and version of supported layout engines
136 rLayoutVersion = /(applewebkit|gecko|trident|edge)\/(\d+)/,
137 // Match the name of known operating systems
138 rPlatform = /(win|wow64|mac|linux|sunos|solaris|iphone|ipad)/,
139 // Translations for conforming operating system names
140 platformMap = { sunos: 'solaris', wow64: 'win' },
152 // Takes a userAgent string and fixes it into something we can more
154 wildFixups.forEach( function ( fixup ) {
155 ua = ua.replace( fixup[ 0 ], fixup[ 1 ] );
157 // Everything will be in lowercase from now on
158 ua = ua.toLowerCase();
162 if ( ( match = rName.exec( ua ) ) ) {
165 if ( ( match = rLayout.exec( ua ) ) ) {
166 layout = layoutMap[ match[ 1 ] ] || match[ 1 ];
168 if ( ( match = rLayoutVersion.exec( ua ) ) ) {
169 layoutversion = parseInt( match[ 2 ], 10 );
171 if ( ( match = rPlatform.exec( nav.platform.toLowerCase() ) ) ) {
172 platform = platformMap[ match[ 1 ] ] || match[ 1 ];
174 if ( ( match = new RegExp( versionPrefixes + versionSuffix ).exec( ua ) ) ) {
175 version = match[ 1 ];
178 // Edge Cases -- did I mention about how user agent string lie?
180 // Decode Safari's crazy 400+ version numbers
181 if ( name === 'safari' && version > 400 ) {
184 // Expose Opera 10's lies about being Opera 9.8
185 if ( name === 'opera' && version >= 9.8 ) {
186 match = ua.match( /\bversion\/([0-9.]*)/ );
187 if ( match && match[ 1 ] ) {
188 version = match[ 1 ];
193 // And IE 11's lies about being not being IE
194 if ( layout === 'trident' && layoutversion >= 7 && ( match = ua.match( /\brv[ :/]([0-9.]*)/ ) ) ) {
197 version = match[ 1 ];
200 // And MS Edge's lies about being Chrome
202 // It's different enough from classic IE Trident engine that they do this
203 // to avoid getting caught by MSIE-specific browser sniffing.
204 if ( name === 'chrome' && ( match = ua.match( /\bedge\/([0-9.]*)/ ) ) ) {
206 version = match[ 1 ];
208 layoutversion = parseInt( match[ 1 ], 10 );
210 // And Amazon Silk's lies about being Android on mobile or Safari on desktop
211 if ( ( match = ua.match( /\bsilk\/([0-9.\-_]*)/ ) ) ) {
214 version = match[ 1 ];
218 versionNumber = parseFloat( version, 10 ) || 0.0;
221 profileCache[ key ] = {
224 layoutVersion: layoutversion,
227 versionBase: ( version !== x ? Math.floor( versionNumber ).toString() : x ),
228 versionNumber: versionNumber
231 return profileCache[ key ];
235 * Checks the current browser against a support map object.
237 * Version numbers passed as numeric values will be compared like numbers (1.2 > 1.11).
238 * Version numbers passed as string values will be compared using a simple component-wise
239 * algorithm, similar to PHP's version_compare ('1.2' < '1.11').
241 * A browser map is in the following format:
244 * // Multiple rules with configurable operators
245 * 'msie': [['>=', 7], ['!=', 9]],
246 * // Match no versions
248 * // Match any version
252 * It can optionally be split into ltr/rtl sections:
261 * // rules are not inherited from ltr
266 * @param {Object} map Browser support map
267 * @param {Object} [profile] A client-profile object
268 * @param {boolean} [exactMatchOnly=false] Only return true if the browser is matched,
269 * otherwise returns true if the browser is not found.
271 * @return {boolean} The current browser is in the support map
273 test: function ( map, profile, exactMatchOnly ) {
274 var conditions, dir, i, op, val, j, pieceVersion, pieceVal, compare;
275 profile = $.isPlainObject( profile ) ? profile : $.client.profile();
276 if ( map.ltr && map.rtl ) {
277 dir = $( document.body ).is( '.rtl' ) ? 'rtl' : 'ltr';
280 // Check over each browser condition to determine if we are running in a
282 if ( typeof map !== 'object' || map[ profile.name ] === undefined ) {
283 // Not found, return true if exactMatchOnly not set, false otherwise
284 return !exactMatchOnly;
286 conditions = map[ profile.name ];
287 if ( conditions === false ) {
291 if ( conditions === null ) {
292 // Match all versions
295 for ( i = 0; i < conditions.length; i++ ) {
296 op = conditions[ i ][ 0 ];
297 val = conditions[ i ][ 1 ];
298 if ( typeof val === 'string' ) {
299 // Perform a component-wise comparison of versions, similar to
300 // PHP's version_compare but simpler. '1.11' is larger than '1.2'.
301 pieceVersion = profile.version.toString().split( '.' );
302 pieceVal = val.split( '.' );
303 // Extend with zeroes to equal length
304 while ( pieceVersion.length < pieceVal.length ) {
305 pieceVersion.push( '0' );
307 while ( pieceVal.length < pieceVersion.length ) {
308 pieceVal.push( '0' );
310 // Compare components
312 for ( j = 0; j < pieceVersion.length; j++ ) {
313 if ( Number( pieceVersion[ j ] ) < Number( pieceVal[ j ] ) ) {
316 } else if ( Number( pieceVersion[ j ] ) > Number( pieceVal[ j ] ) ) {
321 // compare will be -1, 0 or 1, depending on comparison result
322 // eslint-disable-next-line no-eval
323 if ( !( eval( String( compare + op + '0' ) ) ) ) {
326 } else if ( typeof val === 'number' ) {
327 // eslint-disable-next-line no-eval
328 if ( !( eval( 'profile.versionNumber' + op + val ) ) ) {