418 lines
14 KiB
JavaScript
418 lines
14 KiB
JavaScript
/**
|
|
* 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;
|
|
}
|
|
}
|
|
} );
|
|
|
|
}() );
|