Localisation updates from https://translatewiki.net.
[mediawiki.git] / resources / lib / jquery.client / jquery.client.js
blob01b4afaefaa324dbdad7a691b7c02e5efe68aa64
1 /*!
2  * jQuery Client 3.0.0
3  * https://gerrit.wikimedia.org/g/jquery-client/
4  *
5  * Copyright 2010-2020 wikimedia/jquery-client maintainers and other contributors.
6  * Released under the MIT license
7  * https://jquery-client.mit-license.org
8  */
10 /**
11  * User-agent detection
12  *
13  * @class jQuery.client
14  * @singleton
15  */
16 ( function () {
18         /**
19          * @private
20          * @property {Object} profileCache Keyed by userAgent string,
21          * value is the parsed $.client.profile object for that user agent.
22          */
23         var profileCache = {};
25         $.client = {
27                 /**
28                  * Get an object containing information about the client.
29                  *
30                  * The resulting client object will be in the following format:
31                  *
32                  *     {
33                  *         'name': 'firefox',
34                  *         'layout': 'gecko',
35                  *         'layoutVersion': 20101026,
36                  *         'platform': 'linux'
37                  *         'version': '3.5.1',
38                  *         'versionBase': '3',
39                  *         'versionNumber': 3.5,
40                  *     }
41                  *
42                  * Example:
43                  *
44                  *     if ( $.client.profile().layout == 'gecko' ) {
45                  *         // This will only run in Gecko browsers, such as Mozilla Firefox.
46                  *     }
47                  *
48                  *     var profile = $.client.profile();
49                  *     if ( profile.layout == 'gecko' && profile.platform == 'linux' ) {
50                  *         // This will only run in Gecko browsers on Linux.
51                  *     }
52                  *
53                  * Recognised browser names:
54                  *
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)
61                  * - `konqueror`
62                  * - `msie`
63                  * - `opera` (legacy Opera, which uses Presto)
64                  * - `rekonq`
65                  * - `safari` (including Mobile Safari)
66                  * - `silk`
67                  *
68                  * Recognised layout engines:
69                  *
70                  * - `edge` (EdgeHTML 12-18, as used by legacy Microsoft Edge)
71                  * - `gecko`
72                  * - `khtml`
73                  * - `presto`
74                  * - `trident`
75                  * - `webkit`
76                  *
77                  * Note that Chrome and Chromium-based browsers like Opera have their layout
78                  * engine identified as `webkit`.
79                  *
80                  * Recognised platforms:
81                  *
82                  * - `ipad`
83                  * - `iphone`
84                  * - `linux`
85                  * - `mac`
86                  * - `solaris` (untested)
87                  * - `win`
88                  *
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
92                  */
93                 profile: function ( nav ) {
94                         if ( !nav ) {
95                                 nav = window.navigator;
96                         }
98                         // Use the cached version if possible
99                         if ( profileCache[ nav.userAgent + '|' + nav.platform ] ) {
100                                 return profileCache[ nav.userAgent + '|' + nav.platform ];
101                         }
103                         // eslint-disable-next-line vars-on-top
104                         var
105                                 versionNumber,
106                                 key = nav.userAgent + '|' + nav.platform,
108                                 // Configuration
110                                 // Name of browsers or layout engines we don't recognize
111                                 uk = 'unknown',
112                                 // Generic version digit
113                                 x = 'x',
114                                 // Fixups for user agent strings that contain wild words
115                                 wildFixups = [
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' ]
123                                 ],
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' },
142                                 // Pre-processing
144                                 ua = nav.userAgent,
145                                 match,
146                                 name = uk,
147                                 layout = uk,
148                                 layoutversion = uk,
149                                 platform = uk,
150                                 version = x;
152                         // Takes a userAgent string and fixes it into something we can more
153                         // easily work with
154                         wildFixups.forEach( function ( fixup ) {
155                                 ua = ua.replace( fixup[ 0 ], fixup[ 1 ] );
156                         } );
157                         // Everything will be in lowercase from now on
158                         ua = ua.toLowerCase();
160                         // Extraction
162                         if ( ( match = rName.exec( ua ) ) ) {
163                                 name = match[ 1 ];
164                         }
165                         if ( ( match = rLayout.exec( ua ) ) ) {
166                                 layout = layoutMap[ match[ 1 ] ] || match[ 1 ];
167                         }
168                         if ( ( match = rLayoutVersion.exec( ua ) ) ) {
169                                 layoutversion = parseInt( match[ 2 ], 10 );
170                         }
171                         if ( ( match = rPlatform.exec( nav.platform.toLowerCase() ) ) ) {
172                                 platform = platformMap[ match[ 1 ] ] || match[ 1 ];
173                         }
174                         if ( ( match = new RegExp( versionPrefixes + versionSuffix ).exec( ua ) ) ) {
175                                 version = match[ 1 ];
176                         }
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 ) {
182                                 version = '2.0';
183                         }
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 ];
189                                 } else {
190                                         version = '10';
191                                 }
192                         }
193                         // And IE 11's lies about being not being IE
194                         if ( layout === 'trident' && layoutversion >= 7 && ( match = ua.match( /\brv[ :/]([0-9.]*)/ ) ) ) {
195                                 if ( match[ 1 ] ) {
196                                         name = 'msie';
197                                         version = match[ 1 ];
198                                 }
199                         }
200                         // And MS Edge's lies about being Chrome
201                         //
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.]*)/ ) ) ) {
205                                 name = 'edge';
206                                 version = match[ 1 ];
207                                 layout = 'edge';
208                                 layoutversion = parseInt( match[ 1 ], 10 );
209                         }
210                         // And Amazon Silk's lies about being Android on mobile or Safari on desktop
211                         if ( ( match = ua.match( /\bsilk\/([0-9.\-_]*)/ ) ) ) {
212                                 if ( match[ 1 ] ) {
213                                         name = 'silk';
214                                         version = match[ 1 ];
215                                 }
216                         }
218                         versionNumber = parseFloat( version, 10 ) || 0.0;
220                         // Caching
221                         profileCache[ key ] = {
222                                 name: name,
223                                 layout: layout,
224                                 layoutVersion: layoutversion,
225                                 platform: platform,
226                                 version: version,
227                                 versionBase: ( version !== x ? Math.floor( versionNumber ).toString() : x ),
228                                 versionNumber: versionNumber
229                         };
231                         return profileCache[ key ];
232                 },
234                 /**
235                  * Checks the current browser against a support map object.
236                  *
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').
240                  *
241                  * A browser map is in the following format:
242                  *
243                  *     {
244                  *         // Multiple rules with configurable operators
245                  *         'msie': [['>=', 7], ['!=', 9]],
246                  *         // Match no versions
247                  *         'iphone': false,
248                  *         // Match any version
249                  *         'android': null
250                  *     }
251                  *
252                  * It can optionally be split into ltr/rtl sections:
253                  *
254                  *     {
255                  *         'ltr': {
256                  *             'android': null,
257                  *             'iphone': false
258                  *         },
259                  *         'rtl': {
260                  *             'android': false,
261                  *             // rules are not inherited from ltr
262                  *             'iphone': false
263                  *         }
264                  *     }
265                  *
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.
270                  *
271                  * @return {boolean} The current browser is in the support map
272                  */
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';
278                                 map = map[ dir ];
279                         }
280                         // Check over each browser condition to determine if we are running in a
281                         // compatible client
282                         if ( typeof map !== 'object' || map[ profile.name ] === undefined ) {
283                                 // Not found, return true if exactMatchOnly not set, false otherwise
284                                 return !exactMatchOnly;
285                         }
286                         conditions = map[ profile.name ];
287                         if ( conditions === false ) {
288                                 // Match no versions
289                                 return false;
290                         }
291                         if ( conditions === null ) {
292                                 // Match all versions
293                                 return true;
294                         }
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' );
306                                         }
307                                         while ( pieceVal.length < pieceVersion.length ) {
308                                                 pieceVal.push( '0' );
309                                         }
310                                         // Compare components
311                                         compare = 0;
312                                         for ( j = 0; j < pieceVersion.length; j++ ) {
313                                                 if ( Number( pieceVersion[ j ] ) < Number( pieceVal[ j ] ) ) {
314                                                         compare = -1;
315                                                         break;
316                                                 } else if ( Number( pieceVersion[ j ] ) > Number( pieceVal[ j ] ) ) {
317                                                         compare = 1;
318                                                         break;
319                                                 }
320                                         }
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' ) ) ) ) {
324                                                 return false;
325                                         }
326                                 } else if ( typeof val === 'number' ) {
327                                         // eslint-disable-next-line no-eval
328                                         if ( !( eval( 'profile.versionNumber' + op + val ) ) ) {
329                                                 return false;
330                                         }
331                                 }
332                         }
334                         return true;
335                 }
336         };
337 }() );