* @singleton
*/
( function ( mw, $ ) {
- var user,
+ var user, i,
deferreds = {},
+ byteToHex = [],
// Extend the skeleton mw.user from mediawiki.js
// This is kind of ugly but we're stuck with this for b/c reasons
options = mw.user.options || new mw.Map(),
tokens = mw.user.tokens || new mw.Map();
+ // Maps for number -> hex string conversion (with padding)
+ // idea from: https://github.com/broofa/node-uuid/blob/master/uuid.js
+ for ( i = 0; i < 256; i++ ) {
+ byteToHex[i] = (i + 0x100).toString(16).substr(1);
+ }
+
/**
* Get the current user's groups or rights
*
tokens: tokens,
/**
- * Generate a random user session ID (32 alpha-numeric characters)
- *
+ * Generate a random user session ID
* This information would potentially be stored in a cookie to identify a user during a
- * session or series of sessions. Its uniqueness should not be depended on.
+ * session or series of sessions. Its uniqueness should
+ * not be depended on unless the browser supports the crypto API.
+ *
+ * Known problems with Math.random():
+ * Using the Math.random function we have seen sets
+ * with 1% of non uniques among 200.000 values with Safari providing most of these.
+ * Given the prevalence of Safari in mobile the percentage of duplicates in
+ * mobile usages of this code is probably higher.
+ *
+ * Rationale:
+ * We need about 64 bits to make sure that probability of collision
+ * on 500 million (5*10^8) is <= 1%
+ * See: https://en.wikipedia.org/wiki/Birthday_problem#Probability_table
*
- * @return {string} Random set of 32 alpha-numeric characters
+ * @return {string} 64 bit integer in hex format, padded
*/
generateRandomSessionId: function () {
- var i, r,
- id = '',
- seed = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
- for ( i = 0; i < 32; i++ ) {
- r = Math.floor( Math.random() * seed.length );
- id += seed.charAt( r );
+ /*jshint bitwise:false */
+ var rnds, i, r, cryptoObj, hexRnds = new Array( 8 );
+ cryptoObj = window.crypto || window.msCrypto; // for IE 11
+
+ if ( cryptoObj ) {
+ // We fill an array with 8 random values, each of which is 8 bits.
+ // note that rnds is an array-like object not a true array
+ rnds = new Uint8Array( 8 );
+ cryptoObj.getRandomValues( rnds );
+ } else {
+ rnds = new Array( 8 );
+ // From: https://github.com/broofa/node-uuid/blob/master/uuid.js
+ for ( i = 0, r; i < 8; i++ ) {
+ if ( ( i & 0x03 ) === 0 ) {
+ r = Math.random() * 0x100000000;
+ }
+ rnds[i] = r >>> ( ( i & 0x03 ) << 3 ) & 0xff;
+ }
}
- return id;
+ // convert to hex using byteToHex that already contains padding
+ for ( i = 0; i < rnds.length; i++ ) {
+ hexRnds[i] = byteToHex[rnds[i]];
+ }
+
+ // concatenation of two random integers with entrophy n and m
+ // returns a string with entrophy n+m if those strings are independent
+ return hexRnds.join( '' );
},
/**
this.server.respond();
} );
+
+ QUnit.test( 'session numbers', 4, function ( assert ) {
+ /*global $:false */
+ var sessionId = mw.user.generateRandomSessionId(),
+ cryptoObj = window.crypto;
+
+ assert.equal( typeof sessionId, 'string', 'generateRandomSessionId should return a string' );
+ assert.equal( $.trim(sessionId), sessionId, 'generateRandomSessionId should not contain whitespace' );
+ // pretend crypto API is not there and do same test, make sure code runs
+ // through Math.random loop
+ window.crypto = undefined;
+ sessionId = mw.user.generateRandomSessionId();
+ assert.equal( typeof sessionId, 'string', 'generateRandomSessionId should return a string' );
+ assert.equal( sessionId.trim(), sessionId, 'generateRandomSessionId should not be empty' );
+ //restoring crypto object
+ window.crypto = cryptoObj;
+
+ } );
}( mediaWiki ) );