Implement extension registration from an extension.json file
[mediawiki.git] / resources / src / jquery / jquery.client.js
blob3796b0b1ab8f5475eb4c39f2ba87b5b30fc2a919
1 /**
2  * User-agent detection
3  *
4  * @class jQuery.client
5  * @singleton
6  */
7 ( function ( $ ) {
9         /**
10          * @private
11          * @property {Object} profileCache Keyed by userAgent string,
12          * value is the parsed $.client.profile object for that user agent.
13          */
14         var profileCache = {};
16         $.client = {
18                 /**
19                  * Get an object containing information about the client.
20                  *
21                  * @param {Object} [nav] An object with a 'userAgent' and 'platform' property.
22                  *  Defaults to the global `navigator` object.
23                  * @return {Object} The resulting client object will be in the following format:
24                  *
25                  *     {
26                  *         'name': 'firefox',
27                  *         'layout': 'gecko',
28                  *         'layoutVersion': 20101026,
29                  *         'platform': 'linux'
30                  *         'version': '3.5.1',
31                  *         'versionBase': '3',
32                  *         'versionNumber': 3.5,
33                  *     }
34                  */
35                 profile: function ( nav ) {
36                         /*jshint boss: true */
38                         if ( nav === undefined ) {
39                                 nav = window.navigator;
40                         }
42                         // Use the cached version if possible
43                         if ( profileCache[ nav.userAgent + '|' + nav.platform ] !== undefined ) {
44                                 return profileCache[ nav.userAgent + '|' + nav.platform ];
45                         }
47                         var
48                                 versionNumber,
49                                 key = nav.userAgent + '|' + nav.platform,
51                                 // Configuration
53                                 // Name of browsers or layout engines we don't recognize
54                                 uk = 'unknown',
55                                 // Generic version digit
56                                 x = 'x',
57                                 // Strings found in user agent strings that need to be conformed
58                                 wildUserAgents = ['Opera', 'Navigator', 'Minefield', 'KHTML', 'Chrome', 'PLAYSTATION 3', 'Iceweasel'],
59                                 // Translations for conforming user agent strings
60                                 userAgentTranslations = [
61                                         // Tons of browsers lie about being something they are not
62                                         [/(Firefox|MSIE|KHTML,?\slike\sGecko|Konqueror)/, ''],
63                                         // Chrome lives in the shadow of Safari still
64                                         ['Chrome Safari', 'Chrome'],
65                                         // KHTML is the layout engine not the browser - LIES!
66                                         ['KHTML', 'Konqueror'],
67                                         // Firefox nightly builds
68                                         ['Minefield', 'Firefox'],
69                                         // This helps keep different versions consistent
70                                         ['Navigator', 'Netscape'],
71                                         // This prevents version extraction issues, otherwise translation would happen later
72                                         ['PLAYSTATION 3', 'PS3']
73                                 ],
74                                 // Strings which precede a version number in a user agent string - combined and used as
75                                 // match 1 in version detection
76                                 versionPrefixes = [
77                                         'camino', 'chrome', 'firefox', 'iceweasel', 'netscape', 'netscape6', 'opera', 'version', 'konqueror',
78                                         'lynx', 'msie', 'safari', 'ps3', 'android'
79                                 ],
80                                 // Used as matches 2, 3 and 4 in version extraction - 3 is used as actual version number
81                                 versionSuffix = '(\\/|\\;?\\s|)([a-z0-9\\.\\+]*?)(\\;|dev|rel|\\)|\\s|$)',
82                                 // Names of known browsers
83                                 names = [
84                                         'camino', 'chrome', 'firefox', 'iceweasel', 'netscape', 'konqueror', 'lynx', 'msie', 'opera',
85                                         'safari', 'ipod', 'iphone', 'blackberry', 'ps3', 'rekonq', 'android'
86                                 ],
87                                 // Tanslations for conforming browser names
88                                 nameTranslations = [],
89                                 // Names of known layout engines
90                                 layouts = ['gecko', 'konqueror', 'msie', 'trident', 'edge', 'opera', 'webkit'],
91                                 // Translations for conforming layout names
92                                 layoutTranslations = [ ['konqueror', 'khtml'], ['msie', 'trident'], ['opera', 'presto'] ],
93                                 // Names of supported layout engines for version number
94                                 layoutVersions = ['applewebkit', 'gecko', 'trident', 'edge'],
95                                 // Names of known operating systems
96                                 platforms = ['win', 'wow64', 'mac', 'linux', 'sunos', 'solaris', 'iphone'],
97                                 // Translations for conforming operating system names
98                                 platformTranslations = [ ['sunos', 'solaris'], ['wow64', 'win'] ],
100                                 /**
101                                  * Performs multiple replacements on a string
102                                  * @ignore
103                                  */
104                                 translate = function ( source, translations ) {
105                                         var i;
106                                         for ( i = 0; i < translations.length; i++ ) {
107                                                 source = source.replace( translations[i][0], translations[i][1] );
108                                         }
109                                         return source;
110                                 },
112                                 // Pre-processing
114                                 ua = nav.userAgent,
115                                 match,
116                                 name = uk,
117                                 layout = uk,
118                                 layoutversion = uk,
119                                 platform = uk,
120                                 version = x;
122                         if ( match = new RegExp( '(' + wildUserAgents.join( '|' ) + ')' ).exec( ua ) ) {
123                                 // Takes a userAgent string and translates given text into something we can more easily work with
124                                 ua = translate( ua, userAgentTranslations );
125                         }
126                         // Everything will be in lowercase from now on
127                         ua = ua.toLowerCase();
129                         // Extraction
131                         if ( match = new RegExp( '(' + names.join( '|' ) + ')' ).exec( ua ) ) {
132                                 name = translate( match[1], nameTranslations );
133                         }
134                         if ( match = new RegExp( '(' + layouts.join( '|' ) + ')' ).exec( ua ) ) {
135                                 layout = translate( match[1], layoutTranslations );
136                         }
137                         if ( match = new RegExp( '(' + layoutVersions.join( '|' ) + ')\\\/(\\d+)').exec( ua ) ) {
138                                 layoutversion = parseInt( match[2], 10 );
139                         }
140                         if ( match = new RegExp( '(' + platforms.join( '|' ) + ')' ).exec( nav.platform.toLowerCase() ) ) {
141                                 platform = translate( match[1], platformTranslations );
142                         }
143                         if ( match = new RegExp( '(' + versionPrefixes.join( '|' ) + ')' + versionSuffix ).exec( ua ) ) {
144                                 version = match[3];
145                         }
147                         // Edge Cases -- did I mention about how user agent string lie?
149                         // Decode Safari's crazy 400+ version numbers
150                         if ( name === 'safari' && version > 400 ) {
151                                 version = '2.0';
152                         }
153                         // Expose Opera 10's lies about being Opera 9.8
154                         if ( name === 'opera' && version >= 9.8 ) {
155                                 match = ua.match( /\bversion\/([0-9\.]*)/ );
156                                 if ( match && match[1] ) {
157                                         version = match[1];
158                                 } else {
159                                         version = '10';
160                                 }
161                         }
162                         // And Opera 15's lies about being Chrome
163                         if ( name === 'chrome' && ( match = ua.match( /\bopr\/([0-9\.]*)/ ) ) ) {
164                                 if ( match[1] ) {
165                                         name = 'opera';
166                                         version = match[1];
167                                 }
168                         }
169                         // And IE 11's lies about being not being IE
170                         if ( layout === 'trident' && layoutversion >= 7 && ( match = ua.match( /\brv[ :\/]([0-9\.]*)/ ) ) ) {
171                                 if ( match[1] ) {
172                                         name = 'msie';
173                                         version = match[1];
174                                 }
175                         }
176                         // And IE 12's different lies about not being IE
177                         if ( name === 'chrome' && ( match = ua.match( /\bedge\/([0-9\.]*)/ ) ) ) {
178                                 name = 'msie';
179                                 version = match[1];
180                                 layout = 'edge';
181                                 layoutversion = parseInt( match[1], 10 );
182                         }
183                         // And Amazon Silk's lies about being Android on mobile or Safari on desktop
184                         if ( match = ua.match( /\bsilk\/([0-9.\-_]*)/ ) ) {
185                                 if ( match[1] ) {
186                                         name = 'silk';
187                                         version = match[1];
188                                 }
189                         }
191                         versionNumber = parseFloat( version, 10 ) || 0.0;
193                         // Caching
195                         return profileCache[ key  ] = {
196                                 name: name,
197                                 layout: layout,
198                                 layoutVersion: layoutversion,
199                                 platform: platform,
200                                 version: version,
201                                 versionBase: ( version !== x ? Math.floor( versionNumber ).toString() : x ),
202                                 versionNumber: versionNumber
203                         };
204                 },
206                 /**
207                  * Checks the current browser against a support map object.
208                  *
209                  * Version numbers passed as numeric values will be compared like numbers (1.2 > 1.11).
210                  * Version numbers passed as string values will be compared using a simple component-wise
211                  * algorithm, similar to PHP's version_compare ('1.2' < '1.11').
212                  *
213                  * A browser map is in the following format:
214                  *
215                  *     {
216                  *         // Multiple rules with configurable operators
217                  *         'msie': [['>=', 7], ['!=', 9]],
218                  *         // Match no versions
219                  *         'iphone': false,
220                  *         // Match any version
221                  *         'android': null
222                  *     }
223                  *
224                  * It can optionally be split into ltr/rtl sections:
225                  *
226                  *     {
227                  *         'ltr': {
228                  *             'android': null,
229                  *             'iphone': false
230                  *         },
231                  *         'rtl': {
232                  *             'android': false,
233                  *             // rules are not inherited from ltr
234                  *             'iphone': false
235                  *         }
236                  *     }
237                  *
238                  * @param {Object} map Browser support map
239                  * @param {Object} [profile] A client-profile object
240                  * @param {boolean} [exactMatchOnly=false] Only return true if the browser is matched, otherwise
241                  * returns true if the browser is not found.
242                  *
243                  * @return {boolean} The current browser is in the support map
244                  */
245                 test: function ( map, profile, exactMatchOnly ) {
246                         /*jshint evil: true */
248                         var conditions, dir, i, op, val, j, pieceVersion, pieceVal, compare;
249                         profile = $.isPlainObject( profile ) ? profile : $.client.profile();
250                         if ( map.ltr && map.rtl ) {
251                                 dir = $( 'body' ).is( '.rtl' ) ? 'rtl' : 'ltr';
252                                 map = map[dir];
253                         }
254                         // Check over each browser condition to determine if we are running in a compatible client
255                         if ( typeof map !== 'object' || map[profile.name] === undefined ) {
256                                 // Not found, return true if exactMatchOnly not set, false otherwise
257                                 return !exactMatchOnly;
258                         }
259                         conditions = map[profile.name];
260                         if ( conditions === false ) {
261                                 // Match no versions
262                                 return false;
263                         }
264                         if ( conditions === null ) {
265                                 // Match all versions
266                                 return true;
267                         }
268                         for ( i = 0; i < conditions.length; i++ ) {
269                                 op = conditions[i][0];
270                                 val = conditions[i][1];
271                                 if ( typeof val === 'string' ) {
272                                         // Perform a component-wise comparison of versions, similar to PHP's version_compare
273                                         // but simpler. '1.11' is larger than '1.2'.
274                                         pieceVersion = profile.version.toString().split( '.' );
275                                         pieceVal = val.split( '.' );
276                                         // Extend with zeroes to equal length
277                                         while ( pieceVersion.length < pieceVal.length ) {
278                                                 pieceVersion.push( '0' );
279                                         }
280                                         while ( pieceVal.length < pieceVersion.length ) {
281                                                 pieceVal.push( '0' );
282                                         }
283                                         // Compare components
284                                         compare = 0;
285                                         for ( j = 0; j < pieceVersion.length; j++ ) {
286                                                 if ( Number( pieceVersion[j] ) < Number( pieceVal[j] ) ) {
287                                                         compare = -1;
288                                                         break;
289                                                 } else if ( Number( pieceVersion[j] ) > Number( pieceVal[j] ) ) {
290                                                         compare = 1;
291                                                         break;
292                                                 }
293                                         }
294                                         // compare will be -1, 0 or 1, depending on comparison result
295                                         if ( !( eval( '' + compare + op + '0' ) ) ) {
296                                                 return false;
297                                         }
298                                 } else if ( typeof val === 'number' ) {
299                                         if ( !( eval( 'profile.versionNumber' + op + val ) ) ) {
300                                                 return false;
301                                         }
302                                 }
303                         }
305                         return true;
306                 }
307         };
308 }( jQuery ) );