337 lines
10 KiB
JavaScript
337 lines
10 KiB
JavaScript
/*!
|
|
* 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;
|
|
}
|
|
};
|
|
}() );
|