2 * User library provided by 'mediawiki.user' ResourceLoader module.
7 let userInfoPromise, tempUserNamePromise, pageviewRandomId, sessionId;
8 const CLIENTPREF_COOKIE_NAME = 'mwclientpreferences';
9 const CLIENTPREF_SUFFIX = '-clientpref-';
10 const CLIENTPREF_DELIMITER = ',';
13 * Get the current user's groups or rights
16 * @return {jQuery.Promise}
18 function getUserInfo() {
19 if ( !userInfoPromise ) {
20 userInfoPromise = new mw.Api().getUserInfo();
22 return userInfoPromise;
26 * Save the feature value to the client preferences cookie.
29 * @param {string} feature
30 * @param {string} value
32 function saveClientPrefs( feature, value ) {
33 const existingCookie = mw.cookie.get( CLIENTPREF_COOKIE_NAME ) || '';
35 existingCookie.split( CLIENTPREF_DELIMITER ).forEach( ( keyValuePair ) => {
36 const m = keyValuePair.match( /^([\w-]+)-clientpref-(\w+)$/ );
38 data[ m[ 1 ] ] = m[ 2 ];
41 data[ feature ] = value;
43 const newCookie = Object.keys( data ).map( ( key ) => key + CLIENTPREF_SUFFIX + data[ key ] ).join( CLIENTPREF_DELIMITER );
44 mw.cookie.set( CLIENTPREF_COOKIE_NAME, newCookie );
48 * Check if the feature name is composed of valid characters.
50 * A valid feature name may contain letters, numbers, and "-" characters.
53 * @param {string} value
56 function isValidFeatureName( value ) {
57 return value.match( /^[a-zA-Z0-9-]+$/ ) !== null;
61 * Check if the value is composed of valid characters.
64 * @param {string} value
67 function isValidFeatureValue( value ) {
68 return value.match( /^[a-zA-Z0-9]+$/ ) !== null;
71 // mw.user with the properties options and tokens gets defined in mediawiki.base.js.
72 Object.assign( mw.user, /** @lends mw.user */{
75 * Generate a random user session ID.
77 * This information would potentially be stored in a cookie to identify a user during a
78 * session or series of sessions. Its uniqueness should not be depended on unless the
79 * browser supports the crypto API.
81 * Known problems with `Math.random()`:
82 * Using the `Math.random` function we have seen sets
83 * with 1% of non uniques among 200,000 values with Safari providing most of these.
84 * Given the prevalence of Safari in mobile the percentage of duplicates in
85 * mobile usages of this code is probably higher.
88 * We need about 80 bits to make sure that probability of collision
89 * on 155 billion is <= 1%
91 * See {@link https://en.wikipedia.org/wiki/Birthday_attack#Mathematics}
93 * `n(p;H) = n(0.01,2^80)= sqrt (2 * 2^80 * ln(1/(1-0.01)))`
95 * @return {string} 80 bit integer (20 characters) in hex format, padded
97 generateRandomSessionId: function () {
100 // We first attempt to generate a set of random values using the WebCrypto API's
101 // getRandomValues method. If the WebCrypto API is not supported, the Uint16Array
102 // type does not exist, or getRandomValues fails (T263041), an exception will be
103 // thrown, which we'll catch and fall back to using Math.random.
105 // Initialize a typed array containing 5 0-initialized 16-bit integers.
106 // Note that Uint16Array is array-like but does not implement Array.
108 rnds = new Uint16Array( 5 );
109 // Overwrite the array elements with cryptographically strong random values.
110 // https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues
111 // NOTE: this operation can fail internally (T263041), so the try-catch block
112 // must be preserved even after WebCrypto is supported in all modern (Grade A)
114 crypto.getRandomValues( rnds );
116 rnds = new Array( 5 );
117 // 0x10000 is 2^16 so the operation below will return a number
118 // between 2^16 and zero
119 for ( let i = 0; i < 5; i++ ) {
120 rnds[ i ] = Math.floor( Math.random() * 0x10000 );
124 // Convert the 5 16bit-numbers into 20 characters (4 hex per 16 bits).
125 // Concatenation of two random integers with entropy n and m
126 // returns a string with entropy n+m if those strings are independent.
127 // Tested that below code is faster than array + loop + join.
128 return ( rnds[ 0 ] + 0x10000 ).toString( 16 ).slice( 1 ) +
129 ( rnds[ 1 ] + 0x10000 ).toString( 16 ).slice( 1 ) +
130 ( rnds[ 2 ] + 0x10000 ).toString( 16 ).slice( 1 ) +
131 ( rnds[ 3 ] + 0x10000 ).toString( 16 ).slice( 1 ) +
132 ( rnds[ 4 ] + 0x10000 ).toString( 16 ).slice( 1 );
136 * A sticky generateRandomSessionId for the current JS execution context,
137 * cached within this class (also known as a page view token).
140 * @return {string} 80 bit integer in hex format, padded
142 getPageviewToken: function () {
143 if ( !pageviewRandomId ) {
144 pageviewRandomId = mw.user.generateRandomSessionId();
147 return pageviewRandomId;
151 * Get the current user's database id.
153 * Not to be confused with {@link mw.user#id id}.
155 * @return {number} Current user's id, or 0 if user is anonymous
158 return mw.config.get( 'wgUserId' ) || 0;
162 * Check whether the user is a normal non-temporary registered user.
166 isNamed: function () {
167 return !mw.user.isAnon() && !mw.user.isTemp();
171 * Check whether the user is an autocreated temporary user.
175 isTemp: function () {
176 return mw.config.get( 'wgUserIsTemp' ) || false;
180 * Get the current user's name.
182 * @return {string|null} User name string or null if user is anonymous
184 getName: function () {
185 return mw.config.get( 'wgUserName' );
189 * Acquire a temporary user username and stash it in the current session, if temp account creation
190 * is enabled and the current user is logged out. If a name has already been stashed, returns the
193 * If the user later performs an action that results in temp account creation, the stashed username
194 * will be used for their account. It may also be used in previews. However, the account is not
195 * created yet, and the name is not visible to other users.
197 * @return {jQuery.Promise} Promise resolved with the username if we succeeded,
198 * or resolved with `null` if we failed
200 acquireTempUserName: function () {
201 if ( tempUserNamePromise !== undefined ) {
202 // Return the existing promise if we already tried. Do not retry even if we failed.
203 return tempUserNamePromise;
206 if ( mw.config.get( 'wgUserId' ) ) {
207 // User is logged in (or has a temporary account), nothing to do
208 tempUserNamePromise = $.Deferred().resolve( null );
209 } else if ( mw.config.get( 'wgTempUserName' ) ) {
210 // Temporary user username already acquired
211 tempUserNamePromise = $.Deferred().resolve( mw.config.get( 'wgTempUserName' ) );
213 const api = new mw.Api();
214 tempUserNamePromise = api.post( { action: 'acquiretempusername' } ).then( ( resp ) => {
215 mw.config.set( 'wgTempUserName', resp.acquiretempusername );
216 return resp.acquiretempusername;
218 // Ignore failures. The temp name should not be necessary for anything to work.
223 return tempUserNamePromise;
227 * Get date user registered, if available.
229 * @return {boolean|null|Date} False for anonymous users, null if data is
230 * unavailable, or Date for when the user registered.
232 getRegistration: function () {
233 if ( mw.user.isAnon() ) {
236 const registration = mw.config.get( 'wgUserRegistration' );
237 // Registration may be unavailable if the user signed up before MediaWiki
238 // began tracking this.
239 return !registration ? null : new Date( registration );
243 * Get date user first registered, if available.
245 * @return {boolean|null|Date} False for anonymous users, null if data is
246 * unavailable, or Date for when the user registered. For temporary users
247 * that is when their temporary account was created.
249 getFirstRegistration: function () {
250 if ( mw.user.isAnon() ) {
253 const registration = mw.config.get( 'wgUserFirstRegistration' );
254 // Registration may be unavailable if the user signed up before MediaWiki
255 // began tracking this.
256 return registration ? new Date( registration ) : null;
260 * Check whether the current user is anonymous.
264 isAnon: function () {
265 return mw.user.getName() === null;
269 * Retrieve a random ID, generating it if needed.
271 * This ID is shared across windows, tabs, and page views. It is persisted
272 * for the duration of one browser session (until the browser app is closed),
273 * unless the user evokes a "restore previous session" feature that some browsers have.
275 * **Note:** Server-side code must never interpret or modify this value.
277 * @return {string} Random session ID (20 hex characters)
279 sessionId: function () {
280 if ( sessionId === undefined ) {
281 sessionId = mw.cookie.get( 'mwuser-sessionId' );
282 // Validate that the value is 20 hex characters, as it is user-controlled,
283 // and we also used different formats in the past (T283881)
284 if ( sessionId === null || !/^[0-9a-f]{20}$/.test( sessionId ) ) {
285 sessionId = mw.user.generateRandomSessionId();
286 // Setting the `expires` field to `null` means that the cookie should
287 // persist (shared across windows and tabs) until the browser is closed.
288 mw.cookie.set( 'mwuser-sessionId', sessionId, { expires: null } );
295 * Get the current user's name or the session ID.
297 * Not to be confused with {@link mw.user#getId getId}.
299 * @return {string} User name or random session ID
302 return mw.user.getName() || mw.user.sessionId();
306 * Get the current user's groups.
308 * @param {Function} [callback]
309 * @return {jQuery.Promise}
311 getGroups: function ( callback ) {
312 const userGroups = mw.config.get( 'wgUserGroups', [] );
314 // Uses promise for backwards compatibility
315 return $.Deferred().resolve( userGroups ).then( callback );
319 * Get the current user's rights.
321 * @param {Function} [callback]
322 * @return {jQuery.Promise}
324 getRights: function ( callback ) {
325 return getUserInfo().then(
326 ( userInfo ) => userInfo.rights,
332 * Manage client preferences.
334 * For skins that enable the `clientPrefEnabled` option (see Skin class in PHP),
335 * this feature allows you to store preferences in the browser session that will
336 * switch one or more the classes on the HTML document.
338 * This is only supported for unregistered users. For registered users, skins
339 * and extensions must use user preferences (e.g. hidden or API-only options)
340 * and swap class names server-side through the Skin interface.
342 * This feature is limited to page views by unregistered users. For logged-in requests,
343 * store preferences in the database instead, via UserOptionsManager or
344 * {@link mw.Api#saveOption} (may be hidden or API-only to exclude from Special:Preferences),
345 * and then include the desired classes directly in Skin::getHtmlElementAttributes.
347 * Classes toggled by this feature must be named as `<feature>-clientpref-<value>`,
348 * where `value` contains only alphanumerical characters (a-z, A-Z, and 0-9), and `feature`
349 * can also include hyphens.
351 * @namespace mw.user.clientPrefs
355 * Change the class on the HTML document element, and save the value in a cookie.
357 * @memberof mw.user.clientPrefs
358 * @param {string} feature
359 * @param {string} value
360 * @return {boolean} True if feature was stored successfully, false if the value
361 * uses a forbidden character or the feature is not recognised
362 * e.g. a matching class was not defined on the HTML document element.
364 set: function ( feature, value ) {
365 if ( mw.user.isNamed() ) {
366 // Avoid storing an unused cookie and returning true when the setting
367 // wouldn't actually be applied.
368 // Encourage future-proof and server-first implementations.
369 // Encourage feature parity for logged-in users.
370 throw new Error( 'clientPrefs are for unregistered users only' );
372 if ( !isValidFeatureName( feature ) || !isValidFeatureValue( value ) ) {
375 const currentValue = mw.user.clientPrefs.get( feature );
376 // the feature is not recognized
377 if ( !currentValue ) {
380 const oldFeatureClass = feature + CLIENTPREF_SUFFIX + currentValue;
381 const newFeatureClass = feature + CLIENTPREF_SUFFIX + value;
382 // The following classes are removed here:
383 // * feature-name-clientpref-<old-feature-value>
384 // * e.g. vector-font-size--clientpref-small
385 document.documentElement.classList.remove( oldFeatureClass );
386 // The following classes are added here:
387 // * feature-name-clientpref-<feature-value>
388 // * e.g. vector-font-size--clientpref-xlarge
389 document.documentElement.classList.add( newFeatureClass );
390 saveClientPrefs( feature, value );
395 * Retrieve the current value of the feature from the HTML document element.
397 * @memberof mw.user.clientPrefs
398 * @param {string} feature
399 * @return {string|boolean} returns boolean if the feature is not recognized
400 * returns string if a feature was found.
402 get: function ( feature ) {
403 const featurePrefix = feature + CLIENTPREF_SUFFIX;
404 const docClass = document.documentElement.className;
406 const featureRegEx = new RegExp(
407 '(^| )' + mw.util.escapeRegExp( featurePrefix ) + '([a-zA-Z0-9]+)( |$)'
409 const match = docClass.match( featureRegEx );
411 // check no further matches if we replaced this occurance.
412 const isAmbiguous = docClass.replace( featureRegEx, '$1$3' ).match( featureRegEx ) !== null;
413 return !isAmbiguous && match ? match[ 2 ] : false;