/** * 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 `-clientpref-`, * 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- // * e.g. vector-font-size--clientpref-small document.documentElement.classList.remove( oldFeatureClass ); // The following classes are added here: // * feature-name-clientpref- // * 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; } } } ); }() );