Localisation updates from https://translatewiki.net.
[mediawiki.git] / resources / src / mediawiki.user.js
blob2f9e80c70cc89babc3bde9f38616ff078c69b14e
1 /**
2  * User library provided by 'mediawiki.user' ResourceLoader module.
3  *
4  * @namespace mw.user
5  */
6 ( function () {
7         let userInfoPromise, tempUserNamePromise, pageviewRandomId, sessionId;
8         const CLIENTPREF_COOKIE_NAME = 'mwclientpreferences';
9         const CLIENTPREF_SUFFIX = '-clientpref-';
10         const CLIENTPREF_DELIMITER = ',';
12         /**
13          * Get the current user's groups or rights
14          *
15          * @private
16          * @return {jQuery.Promise}
17          */
18         function getUserInfo() {
19                 if ( !userInfoPromise ) {
20                         userInfoPromise = new mw.Api().getUserInfo();
21                 }
22                 return userInfoPromise;
23         }
25         /**
26          * Save the feature value to the client preferences cookie.
27          *
28          * @private
29          * @param {string} feature
30          * @param {string} value
31          */
32         function saveClientPrefs( feature, value ) {
33                 const existingCookie = mw.cookie.get( CLIENTPREF_COOKIE_NAME ) || '';
34                 const data = {};
35                 existingCookie.split( CLIENTPREF_DELIMITER ).forEach( ( keyValuePair ) => {
36                         const m = keyValuePair.match( /^([\w-]+)-clientpref-(\w+)$/ );
37                         if ( m ) {
38                                 data[ m[ 1 ] ] = m[ 2 ];
39                         }
40                 } );
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 );
45         }
47         /**
48          * Check if the feature name is composed of valid characters.
49          *
50          * A valid feature name may contain letters, numbers, and "-" characters.
51          *
52          * @private
53          * @param {string} value
54          * @return {boolean}
55          */
56         function isValidFeatureName( value ) {
57                 return value.match( /^[a-zA-Z0-9-]+$/ ) !== null;
58         }
60         /**
61          * Check if the value is composed of valid characters.
62          *
63          * @private
64          * @param {string} value
65          * @return {boolean}
66          */
67         function isValidFeatureValue( value ) {
68                 return value.match( /^[a-zA-Z0-9]+$/ ) !== null;
69         }
71         // mw.user with the properties options and tokens gets defined in mediawiki.base.js.
72         Object.assign( mw.user, /** @lends mw.user */{
74                 /**
75                  * Generate a random user session ID.
76                  *
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.
80                  *
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.
86                  *
87                  * Rationale:
88                  * We need about 80 bits to make sure that probability of collision
89                  * on 155 billion  is <= 1%
90                  *
91                  * See {@link https://en.wikipedia.org/wiki/Birthday_attack#Mathematics}
92                  *
93                  * `n(p;H) = n(0.01,2^80)= sqrt (2 * 2^80 * ln(1/(1-0.01)))`
94                  *
95                  * @return {string} 80 bit integer (20 characters) in hex format, padded
96                  */
97                 generateRandomSessionId: function () {
98                         let rnds;
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.
104                         try {
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)
113                                 // browsers.
114                                 crypto.getRandomValues( rnds );
115                         } catch ( e ) {
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 );
121                                 }
122                         }
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 );
133                 },
135                 /**
136                  * A sticky generateRandomSessionId for the current JS execution context,
137                  * cached within this class (also known as a page view token).
138                  *
139                  * @since 1.32
140                  * @return {string} 80 bit integer in hex format, padded
141                  */
142                 getPageviewToken: function () {
143                         if ( !pageviewRandomId ) {
144                                 pageviewRandomId = mw.user.generateRandomSessionId();
145                         }
147                         return pageviewRandomId;
148                 },
150                 /**
151                  * Get the current user's database id.
152                  *
153                  * Not to be confused with {@link mw.user#id id}.
154                  *
155                  * @return {number} Current user's id, or 0 if user is anonymous
156                  */
157                 getId: function () {
158                         return mw.config.get( 'wgUserId' ) || 0;
159                 },
161                 /**
162                  * Check whether the user is a normal non-temporary registered user.
163                  *
164                  * @return {boolean}
165                  */
166                 isNamed: function () {
167                         return !mw.user.isAnon() && !mw.user.isTemp();
168                 },
170                 /**
171                  * Check whether the user is an autocreated temporary user.
172                  *
173                  * @return {boolean}
174                  */
175                 isTemp: function () {
176                         return mw.config.get( 'wgUserIsTemp' ) || false;
177                 },
179                 /**
180                  * Get the current user's name.
181                  *
182                  * @return {string|null} User name string or null if user is anonymous
183                  */
184                 getName: function () {
185                         return mw.config.get( 'wgUserName' );
186                 },
188                 /**
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
191                  * same name.
192                  *
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.
196                  *
197                  * @return {jQuery.Promise} Promise resolved with the username if we succeeded,
198                  *   or resolved with `null` if we failed
199                  */
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;
204                         }
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' ) );
212                         } else {
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;
217                                 } ).catch(
218                                         // Ignore failures. The temp name should not be necessary for anything to work.
219                                         () => null
220                                 );
221                         }
223                         return tempUserNamePromise;
224                 },
226                 /**
227                  * Get date user registered, if available.
228                  *
229                  * @return {boolean|null|Date} False for anonymous users, null if data is
230                  *  unavailable, or Date for when the user registered.
231                  */
232                 getRegistration: function () {
233                         if ( mw.user.isAnon() ) {
234                                 return false;
235                         }
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 );
240                 },
242                 /**
243                  * Get date user first registered, if available.
244                  *
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.
248                  */
249                 getFirstRegistration: function () {
250                         if ( mw.user.isAnon() ) {
251                                 return false;
252                         }
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;
257                 },
259                 /**
260                  * Check whether the current user is anonymous.
261                  *
262                  * @return {boolean}
263                  */
264                 isAnon: function () {
265                         return mw.user.getName() === null;
266                 },
268                 /**
269                  * Retrieve a random ID, generating it if needed.
270                  *
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.
274                  *
275                  * **Note:** Server-side code must never interpret or modify this value.
276                  *
277                  * @return {string} Random session ID (20 hex characters)
278                  */
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 } );
289                                 }
290                         }
291                         return sessionId;
292                 },
294                 /**
295                  * Get the current user's name or the session ID.
296                  *
297                  * Not to be confused with {@link mw.user#getId getId}.
298                  *
299                  * @return {string} User name or random session ID
300                  */
301                 id: function () {
302                         return mw.user.getName() || mw.user.sessionId();
303                 },
305                 /**
306                  * Get the current user's groups.
307                  *
308                  * @param {Function} [callback]
309                  * @return {jQuery.Promise}
310                  */
311                 getGroups: function ( callback ) {
312                         const userGroups = mw.config.get( 'wgUserGroups', [] );
314                         // Uses promise for backwards compatibility
315                         return $.Deferred().resolve( userGroups ).then( callback );
316                 },
318                 /**
319                  * Get the current user's rights.
320                  *
321                  * @param {Function} [callback]
322                  * @return {jQuery.Promise}
323                  */
324                 getRights: function ( callback ) {
325                         return getUserInfo().then(
326                                 ( userInfo ) => userInfo.rights,
327                                 () => []
328                         ).then( callback );
329                 },
331                 /**
332                  * Manage client preferences.
333                  *
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.
337                  *
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.
341                  *
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.
346                  *
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.
350                  *
351                  * @namespace mw.user.clientPrefs
352                  */
353                 clientPrefs: {
354                         /**
355                          * Change the class on the HTML document element, and save the value in a cookie.
356                          *
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.
363                          */
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' );
371                                 }
372                                 if ( !isValidFeatureName( feature ) || !isValidFeatureValue( value ) ) {
373                                         return false;
374                                 }
375                                 const currentValue = mw.user.clientPrefs.get( feature );
376                                 // the feature is not recognized
377                                 if ( !currentValue ) {
378                                         return false;
379                                 }
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 );
391                                 return true;
392                         },
394                         /**
395                          * Retrieve the current value of the feature from the HTML document element.
396                          *
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.
401                          */
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]+)( |$)'
408                                 );
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;
414                         }
415                 }
416         } );
418 }() );