Merge "DatabaseMssql: Don't duplicate body of makeList()"
[mediawiki.git] / resources / src / mediawiki / mediawiki.user.js
blob04d9ec65ce2e90c1244705e16eda71d0d9b93883
1 /**
2  * @class mw.user
3  * @singleton
4  */
5 ( function ( mw, $ ) {
6         var user, i,
7                 deferreds = {},
8                 byteToHex = [],
9                 // Extend the skeleton mw.user from mediawiki.js
10                 // This is kind of ugly but we're stuck with this for b/c reasons
11                 options = mw.user.options || new mw.Map(),
12                 tokens = mw.user.tokens || new mw.Map();
14         /**
15          * Get the current user's groups or rights
16          *
17          * @private
18          * @param {string} info One of 'groups' or 'rights'
19          * @return {jQuery.Promise}
20          */
21         function getUserInfo( info ) {
22                 var api;
23                 if ( !deferreds[info] ) {
25                         deferreds.rights = $.Deferred();
26                         deferreds.groups = $.Deferred();
28                         api = new mw.Api();
29                         api.get( {
30                                 action: 'query',
31                                 meta: 'userinfo',
32                                 uiprop: 'rights|groups'
33                         } ).always( function ( data ) {
34                                 var rights, groups;
35                                 if ( data.query && data.query.userinfo ) {
36                                         rights = data.query.userinfo.rights;
37                                         groups = data.query.userinfo.groups;
38                                 }
39                                 deferreds.rights.resolve( rights || [] );
40                                 deferreds.groups.resolve( groups || [] );
41                         } );
43                 }
45                 return deferreds[info].promise();
46         }
48         // Map from numbers 0-255 to a hex string (with padding)
49         for ( i = 0; i < 256; i++ ) {
50                 // Padding: Add a full byte (0x100, 256) and strip the extra character
51                 byteToHex[i] = ( i + 256 ).toString( 16 ).slice( 1 );
52         }
54         mw.user = user = {
55                 options: options,
56                 tokens: tokens,
58                 /**
59                  * Generate a random user session ID.
60                  *
61                  * This information would potentially be stored in a cookie to identify a user during a
62                  * session or series of sessions. Its uniqueness should not be depended on unless the
63                  * browser supports the crypto API.
64                  *
65                  * Known problems with Math.random():
66                  * Using the Math.random function we have seen sets
67                  * with 1% of non uniques among 200,000 values with Safari providing most of these.
68                  * Given the prevalence of Safari in mobile the percentage of duplicates in
69                  * mobile usages of this code is probably higher.
70                  *
71                  * Rationale:
72                  * We need about 64 bits to make sure that probability of collision
73                  * on 500 million (5*10^8) is <= 1%
74                  * See https://en.wikipedia.org/wiki/Birthday_problem#Probability_table
75                  *
76                  * @return {string} 64 bit integer in hex format, padded
77                  */
78                 generateRandomSessionId: function () {
79                         /*jshint bitwise:false */
80                         var rnds, i, r,
81                                 hexRnds = new Array( 8 ),
82                                 // Support: IE 11
83                                 crypto = window.crypto || window.msCrypto;
85                         // Based on https://github.com/broofa/node-uuid/blob/bfd9f96127/uuid.js
86                         if ( crypto ) {
87                                 // Fill an array with 8 random values, each of which is 8 bits.
88                                 // Note that Uint8Array is array-like but does not implement Array.
89                                 rnds = new Uint8Array( 8 );
90                                 crypto.getRandomValues( rnds );
91                         } else {
92                                 rnds = new Array( 8 );
93                                 for ( i = 0; i < 8; i++ ) {
94                                         if ( ( i & 3 ) === 0 ) {
95                                                 r = Math.random() * 0x100000000;
96                                         }
97                                         rnds[i] = r >>> ( ( i & 3 ) << 3 ) & 255;
98                                 }
99                         }
100                         // Convert from number to hex
101                         for ( i = 0; i < 8; i++ ) {
102                                 hexRnds[i] = byteToHex[rnds[i]];
103                         }
105                         // Concatenation of two random integers with entrophy n and m
106                         // returns a string with entrophy n+m if those strings are independent
107                         return hexRnds.join( '' );
108                 },
110                 /**
111                  * Get the current user's database id
112                  *
113                  * Not to be confused with #id.
114                  *
115                  * @return {number} Current user's id, or 0 if user is anonymous
116                  */
117                 getId: function () {
118                         return mw.config.get( 'wgUserId', 0 );
119                 },
121                 /**
122                  * Get the current user's name
123                  *
124                  * @return {string|null} User name string or null if user is anonymous
125                  */
126                 getName: function () {
127                         return mw.config.get( 'wgUserName' );
128                 },
130                 /**
131                  * Get date user registered, if available
132                  *
133                  * @return {Date|boolean|null} Date user registered, or false for anonymous users, or
134                  *  null when data is not available
135                  */
136                 getRegistration: function () {
137                         var registration = mw.config.get( 'wgUserRegistration' );
138                         if ( user.isAnon() ) {
139                                 return false;
140                         }
141                         if ( registration === null ) {
142                                 // Information may not be available if they signed up before
143                                 // MW began storing this.
144                                 return null;
145                         }
146                         return new Date( registration );
147                 },
149                 /**
150                  * Whether the current user is anonymous
151                  *
152                  * @return {boolean}
153                  */
154                 isAnon: function () {
155                         return user.getName() === null;
156                 },
158                 /**
159                  * Get an automatically generated random ID (stored in a session cookie)
160                  *
161                  * This ID is ephemeral for everyone, staying in their browser only until they close
162                  * their browser.
163                  *
164                  * @return {string} Random session ID
165                  */
166                 sessionId: function () {
167                         var sessionId = $.cookie( 'mediaWiki.user.sessionId' );
168                         if ( sessionId === undefined || sessionId === null ) {
169                                 sessionId = user.generateRandomSessionId();
170                                 $.cookie( 'mediaWiki.user.sessionId', sessionId, { expires: null, path: '/' } );
171                         }
172                         return sessionId;
173                 },
175                 /**
176                  * Get the current user's name or the session ID
177                  *
178                  * Not to be confused with #getId.
179                  *
180                  * @return {string} User name or random session ID
181                  */
182                 id: function () {
183                         return user.getName() || user.sessionId();
184                 },
186                 /**
187                  * Get the user's bucket (place them in one if not done already)
188                  *
189                  *     mw.user.bucket( 'test', {
190                  *         buckets: { ignored: 50, control: 25, test: 25 },
191                  *         version: 1,
192                  *         expires: 7
193                  *     } );
194                  *
195                  * @deprecated since 1.23
196                  * @param {string} key Name of bucket
197                  * @param {Object} options Bucket configuration options
198                  * @param {Object} options.buckets List of bucket-name/relative-probability pairs (required,
199                  *  must have at least one pair)
200                  * @param {number} [options.version=0] Version of bucket test, changing this forces
201                  *  rebucketing
202                  * @param {number} [options.expires=30] Length of time (in days) until the user gets
203                  *  rebucketed
204                  * @return {string} Bucket name - the randomly chosen key of the `options.buckets` object
205                  */
206                 bucket: function ( key, options ) {
207                         var cookie, parts, version, bucket,
208                                 range, k, rand, total;
210                         options = $.extend( {
211                                 buckets: {},
212                                 version: 0,
213                                 expires: 30
214                         }, options || {} );
216                         cookie = $.cookie( 'mediaWiki.user.bucket:' + key );
218                         // Bucket information is stored as 2 integers, together as version:bucket like: "1:2"
219                         if ( typeof cookie === 'string' && cookie.length > 2 && cookie.indexOf( ':' ) !== -1 ) {
220                                 parts = cookie.split( ':' );
221                                 if ( parts.length > 1 && Number( parts[0] ) === options.version ) {
222                                         version = Number( parts[0] );
223                                         bucket = String( parts[1] );
224                                 }
225                         }
227                         if ( bucket === undefined ) {
228                                 if ( !$.isPlainObject( options.buckets ) ) {
229                                         throw new Error( 'Invalid bucket. Object expected for options.buckets.' );
230                                 }
232                                 version = Number( options.version );
234                                 // Find range
235                                 range = 0;
236                                 for ( k in options.buckets ) {
237                                         range += options.buckets[k];
238                                 }
240                                 // Select random value within range
241                                 rand = Math.random() * range;
243                                 // Determine which bucket the value landed in
244                                 total = 0;
245                                 for ( k in options.buckets ) {
246                                         bucket = k;
247                                         total += options.buckets[k];
248                                         if ( total >= rand ) {
249                                                 break;
250                                         }
251                                 }
253                                 $.cookie(
254                                         'mediaWiki.user.bucket:' + key,
255                                         version + ':' + bucket,
256                                         { path: '/', expires: Number( options.expires ) }
257                                 );
258                         }
260                         return bucket;
261                 },
263                 /**
264                  * Get the current user's groups
265                  *
266                  * @param {Function} [callback]
267                  * @return {jQuery.Promise}
268                  */
269                 getGroups: function ( callback ) {
270                         return getUserInfo( 'groups' ).done( callback );
271                 },
273                 /**
274                  * Get the current user's rights
275                  *
276                  * @param {Function} [callback]
277                  * @return {jQuery.Promise}
278                  */
279                 getRights: function ( callback ) {
280                         return getUserInfo( 'rights' ).done( callback );
281                 }
282         };
284 }( mediaWiki, jQuery ) );