wikimedia/mediawiki-core

View on GitHub
resources/lib/jquery.client/jquery.client.js

Summary

Maintainability
B
6 hrs
Test Coverage
/*!
 * 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;
        }
    };
}() );