/**
* User library provided by 'mediawiki.user' ResourceLoader module.
*
* @namespace mw.user
*/
( function () {
let userInfoPromise, tempUserNamePromise, pageviewRandomId, sessionId;
const CLIENTPREF_COOKIE_NAME = 'mwclientpreferences';
const CLIENTPREF_SUFFIX = '-clientpref-';
const CLIENTPREF_DELIMITER = ',';
/**
* Get the current user's groups or rights
*
* @private
* @return {jQuery.Promise}
*/
function getUserInfo() {
if ( !userInfoPromise ) {
userInfoPromise = new mw.Api().getUserInfo();
}
return userInfoPromise;
}
/**
* Save the feature value to the client preferences cookie.
*
* @private
* @param {string} feature
* @param {string} value
*/
function saveClientPrefs( feature, value ) {
const existingCookie = mw.cookie.get( CLIENTPREF_COOKIE_NAME ) || '';
const data = {};
existingCookie.split( CLIENTPREF_DELIMITER ).forEach( ( keyValuePair ) => {
const m = keyValuePair.match( /^([\w-]+)-clientpref-(\w+)$/ );
if ( m ) {
data[ m[ 1 ] ] = m[ 2 ];
}
} );
data[ feature ] = value;
const newCookie = Object.keys( data ).map( ( key ) => key + CLIENTPREF_SUFFIX + data[ key ] ).join( CLIENTPREF_DELIMITER );
mw.cookie.set( CLIENTPREF_COOKIE_NAME, newCookie );
}
/**
* Check if the feature name is composed of valid characters.
*
* A valid feature name may contain letters, numbers, and "-" characters.
*
* @private
* @param {string} value
* @return {boolean}
*/
function isValidFeatureName( value ) {
return value.match( /^[a-zA-Z0-9-]+$/ ) !== null;
}
/**
* Check if the value is composed of valid characters.
*
* @private
* @param {string} value
* @return {boolean}
*/
function isValidFeatureValue( value ) {
return value.match( /^[a-zA-Z0-9]+$/ ) !== null;
}
// mw.user with the properties options and tokens gets defined in mediawiki.base.js.
Object.assign( mw.user, /** @lends mw.user */{
/**
* 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 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 80 bits to make sure that probability of collision
* on 155 billion is <= 1%
*
* See {@link https://en.wikipedia.org/wiki/Birthday_attack#Mathematics}
*
* `n(p;H) = n(0.01,2^80)= sqrt (2 * 2^80 * ln(1/(1-0.01)))`
*
* @return {string} 80 bit integer (20 characters) in hex format, padded
*/
generateRandomSessionId: function () {
let rnds;
// We first attempt to generate a set of random values using the WebCrypto API's
// getRandomValues method. If the WebCrypto API is not supported, the Uint16Array
// type does not exist, or getRandomValues fails (T263041), an exception will be
// thrown, which we'll catch and fall back to using Math.random.
try {
// Initialize a typed array containing 5 0-initialized 16-bit integers.
// Note that Uint16Array is array-like but does not implement Array.
rnds = new Uint16Array( 5 );
// Overwrite the array elements with cryptographically strong random values.
// https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues
// NOTE: this operation can fail internally (T263041), so the try-catch block
// must be preserved even after WebCrypto is supported in all modern (Grade A)
// browsers.
crypto.getRandomValues( rnds );
} catch ( e ) {
rnds = new Array( 5 );
// 0x10000 is 2^16 so the operation below will return a number
// between 2^16 and zero
for ( let i = 0; i < 5; i++ ) {
rnds[ i ] = Math.floor( Math.random() * 0x10000 );
}
}
// Convert the 5 16bit-numbers into 20 characters (4 hex per 16 bits).
// Concatenation of two random integers with entropy n and m
// returns a string with entropy n+m if those strings are independent.
// Tested that below code is faster than array + loop + join.
return ( rnds[ 0 ] + 0x10000 ).toString( 16 ).slice( 1 ) +
( rnds[ 1 ] + 0x10000 ).toString( 16 ).slice( 1 ) +
( rnds[ 2 ] + 0x10000 ).toString( 16 ).slice( 1 ) +
( rnds[ 3 ] + 0x10000 ).toString( 16 ).slice( 1 ) +
( rnds[ 4 ] + 0x10000 ).toString( 16 ).slice( 1 );
},
/**
* A sticky generateRandomSessionId for the current JS execution context,
* cached within this class (also known as a page view token).
*
* @since 1.32
* @return {string} 80 bit integer in hex format, padded
*/
getPageviewToken: function () {
if ( !pageviewRandomId ) {
pageviewRandomId = mw.user.generateRandomSessionId();
}
return pageviewRandomId;
},
/**
* Get the current user's database id.
*
* Not to be confused with {@link mw.user#id id}.
*
* @return {number} Current user's id, or 0 if user is anonymous
*/
getId: function () {
return mw.config.get( 'wgUserId' ) || 0;
},
/**
* Check whether the user is a normal non-temporary registered user.
*
* @return {boolean}
*/
isNamed: function () {
return !mw.user.isAnon() && !mw.user.isTemp();
},
/**
* Check whether the user is an autocreated temporary user.
*
* @return {boolean}
*/
isTemp: function () {
return mw.config.get( 'wgUserIsTemp' ) || false;
},
/**
* Get the current user's name.
*
* @return {string|null} User name string or null if user is anonymous
*/
getName: function () {
return mw.config.get( 'wgUserName' );
},
/**
* Acquire a temporary user username and stash it in the current session, if temp account creation
* is enabled and the current user is logged out. If a name has already been stashed, returns the
* same name.
*
* If the user later performs an action that results in temp account creation, the stashed username
* will be used for their account. It may also be used in previews. However, the account is not
* created yet, and the name is not visible to other users.
*
* @return {jQuery.Promise} Promise resolved with the username if we succeeded,
* or resolved with `null` if we failed
*/
acquireTempUserName: function () {
if ( tempUserNamePromise !== undefined ) {
// Return the existing promise if we already tried. Do not retry even if we failed.
return tempUserNamePromise;
}
if ( mw.config.get( 'wgUserId' ) ) {
// User is logged in (or has a temporary account), nothing to do
tempUserNamePromise = $.Deferred().resolve( null );
} else if ( mw.config.get( 'wgTempUserName' ) ) {
// Temporary user username already acquired
tempUserNamePromise = $.Deferred().resolve( mw.config.get( 'wgTempUserName' ) );
} else {
const api = new mw.Api();
tempUserNamePromise = api.post( { action: 'acquiretempusername' } ).then( ( resp ) => {
mw.config.set( 'wgTempUserName', resp.acquiretempusername );
return resp.acquiretempusername;
} ).catch(
// Ignore failures. The temp name should not be necessary for anything to work.
() => null
);
}
return tempUserNamePromise;
},
/**
* Get date user registered, if available.
*
* @return {boolean|null|Date} False for anonymous users, null if data is
* unavailable, or Date for when the user registered.
*/
getRegistration: function () {
if ( mw.user.isAnon() ) {
return false;
}
const registration = mw.config.get( 'wgUserRegistration' );
// Registration may be unavailable if the user signed up before MediaWiki
// began tracking this.
return !registration ? null : new Date( registration );
},
/**
* Get date user first registered, if available.
*
* @return {boolean|null|Date} False for anonymous users, null if data is
* unavailable, or Date for when the user registered. For temporary users
* that is when their temporary account was created.
*/
getFirstRegistration: function () {
if ( mw.user.isAnon() ) {
return false;
}
const registration = mw.config.get( 'wgUserFirstRegistration' );
// Registration may be unavailable if the user signed up before MediaWiki
// began tracking this.
return registration ? new Date( registration ) : null;
},
/**
* Check whether the current user is anonymous.
*
* @return {boolean}
*/
isAnon: function () {
return mw.user.getName() === null;
},
/**
* Retrieve a random ID, generating it if needed.
*
* This ID is shared across windows, tabs, and page views. It is persisted
* for the duration of one browser session (until the browser app is closed),
* unless the user evokes a "restore previous session" feature that some browsers have.
*
* **Note:** Server-side code must never interpret or modify this value.
*
* @return {string} Random session ID (20 hex characters)
*/
sessionId: function () {
if ( sessionId === undefined ) {
sessionId = mw.cookie.get( 'mwuser-sessionId' );
// Validate that the value is 20 hex characters, as it is user-controlled,
// and we also used different formats in the past (T283881)
if ( sessionId === null || !/^[0-9a-f]{20}$/.test( sessionId ) ) {
sessionId = mw.user.generateRandomSessionId();
// Setting the `expires` field to `null` means that the cookie should
// persist (shared across windows and tabs) until the browser is closed.
mw.cookie.set( 'mwuser-sessionId', sessionId, { expires: null } );
}
}
return sessionId;
},
/**
* Get the current user's name or the session ID.
*
* Not to be confused with {@link mw.user#getId getId}.
*
* @return {string} User name or random session ID
*/
id: function () {
return mw.user.getName() || mw.user.sessionId();
},
/**
* Get the current user's groups.
*
* @param {Function} [callback]
* @return {jQuery.Promise}
*/
getGroups: function ( callback ) {
const userGroups = mw.config.get( 'wgUserGroups', [] );
// Uses promise for backwards compatibility
return $.Deferred().resolve( userGroups ).then( callback );
},
/**
* Get the current user's rights.
*
* @param {Function} [callback]
* @return {jQuery.Promise}
*/
getRights: function ( callback ) {
return getUserInfo().then(
( userInfo ) => userInfo.rights,
() => []
).then( callback );
},
/**
* Manage client preferences.
*
* For skins that enable the `clientPrefEnabled` option (see Skin class in PHP),
* this feature allows you to store preferences in the browser session that will
* switch one or more the classes on the HTML document.
*
* This is only supported for unregistered users. For registered users, skins
* and extensions must use user preferences (e.g. hidden or API-only options)
* and swap class names server-side through the Skin interface.
*
* This feature is limited to page views by unregistered users. For logged-in requests,
* store preferences in the database instead, via UserOptionsManager or
* {@link mw.Api#saveOption} (may be hidden or API-only to exclude from Special:Preferences),
* and then include the desired classes directly in Skin::getHtmlElementAttributes.
*
* Classes toggled by this feature must be named as `<feature>-clientpref-<value>`,
* where `value` contains only alphanumerical characters (a-z, A-Z, and 0-9), and `feature`
* can also include hyphens.
*
* @namespace mw.user.clientPrefs
*/
clientPrefs: {
/**
* Change the class on the HTML document element, and save the value in a cookie.
*
* @memberof mw.user.clientPrefs
* @param {string} feature
* @param {string} value
* @return {boolean} True if feature was stored successfully, false if the value
* uses a forbidden character or the feature is not recognised
* e.g. a matching class was not defined on the HTML document element.
*/
set: function ( feature, value ) {
if ( mw.user.isNamed() ) {
// Avoid storing an unused cookie and returning true when the setting
// wouldn't actually be applied.
// Encourage future-proof and server-first implementations.
// Encourage feature parity for logged-in users.
throw new Error( 'clientPrefs are for unregistered users only' );
}
if ( !isValidFeatureName( feature ) || !isValidFeatureValue( value ) ) {
return false;
}
const currentValue = mw.user.clientPrefs.get( feature );
// the feature is not recognized
if ( !currentValue ) {
return false;
}
const oldFeatureClass = feature + CLIENTPREF_SUFFIX + currentValue;
const newFeatureClass = feature + CLIENTPREF_SUFFIX + value;
// The following classes are removed here:
// * feature-name-clientpref-<old-feature-value>
// * e.g. vector-font-size--clientpref-small
document.documentElement.classList.remove( oldFeatureClass );
// The following classes are added here:
// * feature-name-clientpref-<feature-value>
// * e.g. vector-font-size--clientpref-xlarge
document.documentElement.classList.add( newFeatureClass );
saveClientPrefs( feature, value );
return true;
},
/**
* Retrieve the current value of the feature from the HTML document element.
*
* @memberof mw.user.clientPrefs
* @param {string} feature
* @return {string|boolean} returns boolean if the feature is not recognized
* returns string if a feature was found.
*/
get: function ( feature ) {
const featurePrefix = feature + CLIENTPREF_SUFFIX;
const docClass = document.documentElement.className;
const featureRegEx = new RegExp(
'(^| )' + mw.util.escapeRegExp( featurePrefix ) + '([a-zA-Z0-9]+)( |$)'
);
const match = docClass.match( featureRegEx );
// check no further matches if we replaced this occurance.
const isAmbiguous = docClass.replace( featureRegEx, '$1$3' ).match( featureRegEx ) !== null;
return !isAmbiguous && match ? match[ 2 ] : false;
}
}
} );
}() );