resources/lib/jquery.client/jquery.client.js
/*!
* jQuery Client 3.0.0
* https://gerrit.wikimedia.org/g/jquery-client/
*
* Copyright 2010-2020 wikimedia/jquery-client maintainers and other contributors.
* Released under the MIT license
* https://jquery-client.mit-license.org
*/
/**
* User-agent detection
*
* @class jQuery.client
* @singleton
*/
( function () {
/**
* @private
* @property {Object} profileCache Keyed by userAgent string,
* value is the parsed $.client.profile object for that user agent.
*/
var profileCache = {};
$.client = {
/**
* Get an object containing information about the client.
*
* The resulting client object will be in the following format:
*
* {
* 'name': 'firefox',
* 'layout': 'gecko',
* 'layoutVersion': 20101026,
* 'platform': 'linux'
* 'version': '3.5.1',
* 'versionBase': '3',
* 'versionNumber': 3.5,
* }
*
* Example:
*
* if ( $.client.profile().layout == 'gecko' ) {
* // This will only run in Gecko browsers, such as Mozilla Firefox.
* }
*
* var profile = $.client.profile();
* if ( profile.layout == 'gecko' && profile.platform == 'linux' ) {
* // This will only run in Gecko browsers on Linux.
* }
*
* Recognised browser names:
*
* - `android` (legacy Android browser, prior to Chrome Mobile)
* - `chrome` (includes Chrome Mobile, Microsoft Edge, Opera, and others)
* - `crios` (Chrome on iOS, which uses Mobile Safari)
* - `edge` (legacy Microsoft Edge, which uses EdgeHTML)
* - `firefox` (includes Firefox Mobile, Iceweasel, and others)
* - `fxios` (Firefox on iOS, which uses Mobile Safari)
* - `konqueror`
* - `msie`
* - `opera` (legacy Opera, which uses Presto)
* - `rekonq`
* - `safari` (including Mobile Safari)
* - `silk`
*
* Recognised layout engines:
*
* - `edge` (EdgeHTML 12-18, as used by legacy Microsoft Edge)
* - `gecko`
* - `khtml`
* - `presto`
* - `trident`
* - `webkit`
*
* Note that Chrome and Chromium-based browsers like Opera have their layout
* engine identified as `webkit`.
*
* Recognised platforms:
*
* - `ipad`
* - `iphone`
* - `linux`
* - `mac`
* - `solaris` (untested)
* - `win`
*
* @param {Object} [nav] An object with a 'userAgent' and 'platform' property.
* Defaults to the global `navigator` object.
* @return {Object} The client object
*/
profile: function ( nav ) {
if ( !nav ) {
nav = window.navigator;
}
// Use the cached version if possible
if ( profileCache[ nav.userAgent + '|' + nav.platform ] ) {
return profileCache[ nav.userAgent + '|' + nav.platform ];
}
// eslint-disable-next-line vars-on-top
var
versionNumber,
key = nav.userAgent + '|' + nav.platform,
// Configuration
// Name of browsers or layout engines we don't recognize
uk = 'unknown',
// Generic version digit
x = 'x',
// Fixups for user agent strings that contain wild words
wildFixups = [
// Chrome lives in the shadow of Safari still
[ 'Chrome Safari', 'Chrome' ],
// KHTML is the layout engine not the browser - LIES!
[ 'KHTML/', 'Konqueror/' ],
// For Firefox Mobile, strip out "Android;" or "Android [version]" so that we
// classify it as Firefox instead of Android (default browser)
[ /Android(?:;|\s[a-zA-Z0-9.+-]+)(.*Firefox)/, '$1' ]
],
// Strings which precede a version number in a user agent string
versionPrefixes = '(?:chrome|crios|firefox|fxios|opera|version|konqueror|msie|safari|android)',
// This matches the actual version number, with non-capturing groups for the
// separator and suffix
versionSuffix = '(?:\\/|;?\\s|)([a-z0-9\\.\\+]*?)(?:;|dev|rel|\\)|\\s|$)',
// Match the names of known browser families
rName = /(chrome|crios|firefox|fxios|konqueror|msie|opera|safari|rekonq|android)/,
// Match the name of known layout engines
rLayout = /(gecko|konqueror|msie|trident|edge|opera|webkit)/,
// Translations for conforming layout names
layoutMap = { konqueror: 'khtml', msie: 'trident', opera: 'presto' },
// Match the prefix and version of supported layout engines
rLayoutVersion = /(applewebkit|gecko|trident|edge)\/(\d+)/,
// Match the name of known operating systems
rPlatform = /(win|wow64|mac|linux|sunos|solaris|iphone|ipad)/,
// Translations for conforming operating system names
platformMap = { sunos: 'solaris', wow64: 'win' },
// Pre-processing
ua = nav.userAgent,
match,
name = uk,
layout = uk,
layoutversion = uk,
platform = uk,
version = x;
// Takes a userAgent string and fixes it into something we can more
// easily work with
wildFixups.forEach( function ( fixup ) {
ua = ua.replace( fixup[ 0 ], fixup[ 1 ] );
} );
// Everything will be in lowercase from now on
ua = ua.toLowerCase();
// Extraction
if ( ( match = rName.exec( ua ) ) ) {
name = match[ 1 ];
}
if ( ( match = rLayout.exec( ua ) ) ) {
layout = layoutMap[ match[ 1 ] ] || match[ 1 ];
}
if ( ( match = rLayoutVersion.exec( ua ) ) ) {
layoutversion = parseInt( match[ 2 ], 10 );
}
if ( ( match = rPlatform.exec( nav.platform.toLowerCase() ) ) ) {
platform = platformMap[ match[ 1 ] ] || match[ 1 ];
}
if ( ( match = new RegExp( versionPrefixes + versionSuffix ).exec( ua ) ) ) {
version = match[ 1 ];
}
// Edge Cases -- did I mention about how user agent string lie?
// Decode Safari's crazy 400+ version numbers
if ( name === 'safari' && version > 400 ) {
version = '2.0';
}
// Expose Opera 10's lies about being Opera 9.8
if ( name === 'opera' && version >= 9.8 ) {
match = ua.match( /\bversion\/([0-9.]*)/ );
if ( match && match[ 1 ] ) {
version = match[ 1 ];
} else {
version = '10';
}
}
// And IE 11's lies about being not being IE
if ( layout === 'trident' && layoutversion >= 7 && ( match = ua.match( /\brv[ :/]([0-9.]*)/ ) ) ) {
if ( match[ 1 ] ) {
name = 'msie';
version = match[ 1 ];
}
}
// And MS Edge's lies about being Chrome
//
// It's different enough from classic IE Trident engine that they do this
// to avoid getting caught by MSIE-specific browser sniffing.
if ( name === 'chrome' && ( match = ua.match( /\bedge\/([0-9.]*)/ ) ) ) {
name = 'edge';
version = match[ 1 ];
layout = 'edge';
layoutversion = parseInt( match[ 1 ], 10 );
}
// And Amazon Silk's lies about being Android on mobile or Safari on desktop
if ( ( match = ua.match( /\bsilk\/([0-9.\-_]*)/ ) ) ) {
if ( match[ 1 ] ) {
name = 'silk';
version = match[ 1 ];
}
}
versionNumber = parseFloat( version, 10 ) || 0.0;
// Caching
profileCache[ key ] = {
name: name,
layout: layout,
layoutVersion: layoutversion,
platform: platform,
version: version,
versionBase: ( version !== x ? Math.floor( versionNumber ).toString() : x ),
versionNumber: versionNumber
};
return profileCache[ key ];
},
/**
* Checks the current browser against a support map object.
*
* Version numbers passed as numeric values will be compared like numbers (1.2 > 1.11).
* Version numbers passed as string values will be compared using a simple component-wise
* algorithm, similar to PHP's version_compare ('1.2' < '1.11').
*
* A browser map is in the following format:
*
* {
* // Multiple rules with configurable operators
* 'msie': [['>=', 7], ['!=', 9]],
* // Match no versions
* 'iphone': false,
* // Match any version
* 'android': null
* }
*
* It can optionally be split into ltr/rtl sections:
*
* {
* 'ltr': {
* 'android': null,
* 'iphone': false
* },
* 'rtl': {
* 'android': false,
* // rules are not inherited from ltr
* 'iphone': false
* }
* }
*
* @param {Object} map Browser support map
* @param {Object} [profile] A client-profile object
* @param {boolean} [exactMatchOnly=false] Only return true if the browser is matched,
* otherwise returns true if the browser is not found.
*
* @return {boolean} The current browser is in the support map
*/
test: function ( map, profile, exactMatchOnly ) {
var conditions, dir, i, op, val, j, pieceVersion, pieceVal, compare;
profile = $.isPlainObject( profile ) ? profile : $.client.profile();
if ( map.ltr && map.rtl ) {
dir = $( document.body ).is( '.rtl' ) ? 'rtl' : 'ltr';
map = map[ dir ];
}
// Check over each browser condition to determine if we are running in a
// compatible client
if ( typeof map !== 'object' || map[ profile.name ] === undefined ) {
// Not found, return true if exactMatchOnly not set, false otherwise
return !exactMatchOnly;
}
conditions = map[ profile.name ];
if ( conditions === false ) {
// Match no versions
return false;
}
if ( conditions === null ) {
// Match all versions
return true;
}
for ( i = 0; i < conditions.length; i++ ) {
op = conditions[ i ][ 0 ];
val = conditions[ i ][ 1 ];
if ( typeof val === 'string' ) {
// Perform a component-wise comparison of versions, similar to
// PHP's version_compare but simpler. '1.11' is larger than '1.2'.
pieceVersion = profile.version.toString().split( '.' );
pieceVal = val.split( '.' );
// Extend with zeroes to equal length
while ( pieceVersion.length < pieceVal.length ) {
pieceVersion.push( '0' );
}
while ( pieceVal.length < pieceVersion.length ) {
pieceVal.push( '0' );
}
// Compare components
compare = 0;
for ( j = 0; j < pieceVersion.length; j++ ) {
if ( Number( pieceVersion[ j ] ) < Number( pieceVal[ j ] ) ) {
compare = -1;
break;
} else if ( Number( pieceVersion[ j ] ) > Number( pieceVal[ j ] ) ) {
compare = 1;
break;
}
}
// compare will be -1, 0 or 1, depending on comparison result
// eslint-disable-next-line no-eval
if ( !( eval( String( compare + op + '0' ) ) ) ) {
return false;
}
} else if ( typeof val === 'number' ) {
// eslint-disable-next-line no-eval
if ( !( eval( 'profile.versionNumber' + op + val ) ) ) {
return false;
}
}
}
return true;
}
};
}() );