75749ec7e860a1f32b5999cc9be1360ece6583e9
[lhc/web/wiklou.git] / includes / User.php
1 <?php
2 /**
3 * Implements the User class for the %MediaWiki software.
4 * @file
5 */
6
7 /**
8 * \int Number of characters in user_token field.
9 * @ingroup Constants
10 */
11 define( 'USER_TOKEN_LENGTH', 32 );
12
13 /**
14 * \int Serialized record version.
15 * @ingroup Constants
16 */
17 define( 'MW_USER_VERSION', 8 );
18
19 /**
20 * \string Some punctuation to prevent editing from broken text-mangling proxies.
21 * @ingroup Constants
22 */
23 define( 'EDIT_TOKEN_SUFFIX', '+\\' );
24
25 /**
26 * Thrown by User::setPassword() on error.
27 * @ingroup Exception
28 */
29 class PasswordError extends MWException {
30 // NOP
31 }
32
33 /**
34 * The User object encapsulates all of the user-specific settings (user_id,
35 * name, rights, password, email address, options, last login time). Client
36 * classes use the getXXX() functions to access these fields. These functions
37 * do all the work of determining whether the user is logged in,
38 * whether the requested option can be satisfied from cookies or
39 * whether a database query is needed. Most of the settings needed
40 * for rendering normal pages are set in the cookie to minimize use
41 * of the database.
42 */
43 class User {
44
45 /**
46 * \type{\arrayof{\string}} A list of default user toggles, i.e., boolean user
47 * preferences that are displayed by Special:Preferences as checkboxes.
48 * This list can be extended via the UserToggles hook or by
49 * $wgContLang::getExtraUserToggles().
50 * @showinitializer
51 */
52 public static $mToggles = array(
53 'highlightbroken',
54 'justify',
55 'hideminor',
56 'extendwatchlist',
57 'usenewrc',
58 'numberheadings',
59 'showtoolbar',
60 'editondblclick',
61 'editsection',
62 'editsectiononrightclick',
63 'showtoc',
64 'rememberpassword',
65 'editwidth',
66 'watchcreations',
67 'watchdefault',
68 'watchmoves',
69 'watchdeletion',
70 'minordefault',
71 'previewontop',
72 'previewonfirst',
73 'nocache',
74 'enotifwatchlistpages',
75 'enotifusertalkpages',
76 'enotifminoredits',
77 'enotifrevealaddr',
78 'shownumberswatching',
79 'fancysig',
80 'externaleditor',
81 'externaldiff',
82 'showjumplinks',
83 'uselivepreview',
84 'forceeditsummary',
85 'watchlisthideminor',
86 'watchlisthidebots',
87 'watchlisthideown',
88 'watchlisthideanons',
89 'watchlisthideliu',
90 'ccmeonemails',
91 'diffonly',
92 'showhiddencats',
93 'noconvertlink',
94 'norollbackdiff',
95 );
96
97 /**
98 * \type{\arrayof{\string}} List of member variables which are saved to the
99 * shared cache (memcached). Any operation which changes the
100 * corresponding database fields must call a cache-clearing function.
101 * @showinitializer
102 */
103 static $mCacheVars = array(
104 // user table
105 'mId',
106 'mName',
107 'mRealName',
108 'mPassword',
109 'mNewpassword',
110 'mNewpassTime',
111 'mEmail',
112 'mTouched',
113 'mToken',
114 'mEmailAuthenticated',
115 'mEmailToken',
116 'mEmailTokenExpires',
117 'mRegistration',
118 'mEditCount',
119 // user_group table
120 'mGroups',
121 // user_properties table
122 'mOptionOverrides',
123 );
124
125 /**
126 * \type{\arrayof{\string}} Core rights.
127 * Each of these should have a corresponding message of the form
128 * "right-$right".
129 * @showinitializer
130 */
131 static $mCoreRights = array(
132 'apihighlimits',
133 'autoconfirmed',
134 'autopatrol',
135 'bigdelete',
136 'block',
137 'blockemail',
138 'bot',
139 'browsearchive',
140 'createaccount',
141 'createpage',
142 'createtalk',
143 'delete',
144 'deletedhistory',
145 'deletedcontent',
146 'deletedrevision',
147 'deleterevision',
148 'edit',
149 'editinterface',
150 'editusercssjs',
151 'hideuser',
152 'import',
153 'importupload',
154 'ipblock-exempt',
155 'markbotedits',
156 'minoredit',
157 'move',
158 'movefile',
159 'move-rootuserpages',
160 'move-subpages',
161 'nominornewtalk',
162 'noratelimit',
163 'override-export-depth',
164 'patrol',
165 'protect',
166 'proxyunbannable',
167 'purge',
168 'read',
169 'reupload',
170 'reupload-shared',
171 'rollback',
172 'sendemail',
173 'siteadmin',
174 'suppressionlog',
175 'suppressredirect',
176 'suppressrevision',
177 'trackback',
178 'undelete',
179 'unwatchedpages',
180 'upload',
181 'upload_by_url',
182 'userrights',
183 'userrights-interwiki',
184 'writeapi',
185 );
186 /**
187 * \string Cached results of getAllRights()
188 */
189 static $mAllRights = false;
190
191 /** @name Cache variables */
192 //@{
193 var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime,
194 $mEmail, $mTouched, $mToken, $mEmailAuthenticated,
195 $mEmailToken, $mEmailTokenExpires, $mRegistration, $mGroups, $mOptionOverrides;
196 //@}
197
198 /**
199 * \bool Whether the cache variables have been loaded.
200 */
201 var $mDataLoaded, $mAuthLoaded, $mOptionsLoaded;
202
203 /**
204 * \string Initialization data source if mDataLoaded==false. May be one of:
205 * - 'defaults' anonymous user initialised from class defaults
206 * - 'name' initialise from mName
207 * - 'id' initialise from mId
208 * - 'session' log in from cookies or session if possible
209 *
210 * Use the User::newFrom*() family of functions to set this.
211 */
212 var $mFrom;
213
214 /** @name Lazy-initialized variables, invalidated with clearInstanceCache */
215 //@{
216 var $mNewtalk, $mDatePreference, $mBlockedby, $mHash, $mSkin, $mRights,
217 $mBlockreason, $mBlock, $mEffectiveGroups, $mBlockedGlobally,
218 $mLocked, $mHideName, $mOptions;
219 //@}
220
221 static $idCacheByName = array();
222
223 /**
224 * Lightweight constructor for an anonymous user.
225 * Use the User::newFrom* factory functions for other kinds of users.
226 *
227 * @see newFromName()
228 * @see newFromId()
229 * @see newFromConfirmationCode()
230 * @see newFromSession()
231 * @see newFromRow()
232 */
233 function User() {
234 $this->clearInstanceCache( 'defaults' );
235 }
236
237 /**
238 * Load the user table data for this object from the source given by mFrom.
239 */
240 function load() {
241 if ( $this->mDataLoaded ) {
242 return;
243 }
244 wfProfileIn( __METHOD__ );
245
246 # Set it now to avoid infinite recursion in accessors
247 $this->mDataLoaded = true;
248
249 switch ( $this->mFrom ) {
250 case 'defaults':
251 $this->loadDefaults();
252 break;
253 case 'name':
254 $this->mId = self::idFromName( $this->mName );
255 if ( !$this->mId ) {
256 # Nonexistent user placeholder object
257 $this->loadDefaults( $this->mName );
258 } else {
259 $this->loadFromId();
260 }
261 break;
262 case 'id':
263 $this->loadFromId();
264 break;
265 case 'session':
266 $this->loadFromSession();
267 wfRunHooks( 'UserLoadAfterLoadFromSession', array( $this ) );
268 break;
269 default:
270 throw new MWException( "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" );
271 }
272 wfProfileOut( __METHOD__ );
273 }
274
275 /**
276 * Load user table data, given mId has already been set.
277 * @return \bool false if the ID does not exist, true otherwise
278 * @private
279 */
280 function loadFromId() {
281 global $wgMemc;
282 if ( $this->mId == 0 ) {
283 $this->loadDefaults();
284 return false;
285 }
286
287 # Try cache
288 $key = wfMemcKey( 'user', 'id', $this->mId );
289 $data = $wgMemc->get( $key );
290 if ( !is_array( $data ) || $data['mVersion'] < MW_USER_VERSION ) {
291 # Object is expired, load from DB
292 $data = false;
293 }
294
295 if ( !$data ) {
296 wfDebug( "Cache miss for user {$this->mId}\n" );
297 # Load from DB
298 if ( !$this->loadFromDatabase() ) {
299 # Can't load from ID, user is anonymous
300 return false;
301 }
302 $this->saveToCache();
303 } else {
304 wfDebug( "Got user {$this->mId} from cache\n" );
305 # Restore from cache
306 foreach ( self::$mCacheVars as $name ) {
307 $this->$name = $data[$name];
308 }
309 }
310 return true;
311 }
312
313 /**
314 * Save user data to the shared cache
315 */
316 function saveToCache() {
317 $this->load();
318 $this->loadGroups();
319 $this->loadOptions();
320 if ( $this->isAnon() ) {
321 // Anonymous users are uncached
322 return;
323 }
324 $data = array();
325 foreach ( self::$mCacheVars as $name ) {
326 $data[$name] = $this->$name;
327 }
328 $data['mVersion'] = MW_USER_VERSION;
329 $key = wfMemcKey( 'user', 'id', $this->mId );
330 global $wgMemc;
331 $wgMemc->set( $key, $data );
332 }
333
334
335 /** @name newFrom*() static factory methods */
336 //@{
337
338 /**
339 * Static factory method for creation from username.
340 *
341 * This is slightly less efficient than newFromId(), so use newFromId() if
342 * you have both an ID and a name handy.
343 *
344 * @param $name \string Username, validated by Title::newFromText()
345 * @param $validate \mixed Validate username. Takes the same parameters as
346 * User::getCanonicalName(), except that true is accepted as an alias
347 * for 'valid', for BC.
348 *
349 * @return \type{User} The User object, or null if the username is invalid. If the
350 * username is not present in the database, the result will be a user object
351 * with a name, zero user ID and default settings.
352 */
353 static function newFromName( $name, $validate = 'valid' ) {
354 if ( $validate === true ) {
355 $validate = 'valid';
356 }
357 $name = self::getCanonicalName( $name, $validate );
358 if ( $name === false ) {
359 return null;
360 } else {
361 # Create unloaded user object
362 $u = new User;
363 $u->mName = $name;
364 $u->mFrom = 'name';
365 return $u;
366 }
367 }
368
369 /**
370 * Static factory method for creation from a given user ID.
371 *
372 * @param $id \int Valid user ID
373 * @return \type{User} The corresponding User object
374 */
375 static function newFromId( $id ) {
376 $u = new User;
377 $u->mId = $id;
378 $u->mFrom = 'id';
379 return $u;
380 }
381
382 /**
383 * Factory method to fetch whichever user has a given email confirmation code.
384 * This code is generated when an account is created or its e-mail address
385 * has changed.
386 *
387 * If the code is invalid or has expired, returns NULL.
388 *
389 * @param $code \string Confirmation code
390 * @return \type{User}
391 */
392 static function newFromConfirmationCode( $code ) {
393 $dbr = wfGetDB( DB_SLAVE );
394 $id = $dbr->selectField( 'user', 'user_id', array(
395 'user_email_token' => md5( $code ),
396 'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ),
397 ) );
398 if( $id !== false ) {
399 return User::newFromId( $id );
400 } else {
401 return null;
402 }
403 }
404
405 /**
406 * Create a new user object using data from session or cookies. If the
407 * login credentials are invalid, the result is an anonymous user.
408 *
409 * @return \type{User}
410 */
411 static function newFromSession() {
412 $user = new User;
413 $user->mFrom = 'session';
414 return $user;
415 }
416
417 /**
418 * Create a new user object from a user row.
419 * The row should have all fields from the user table in it.
420 * @param $row array A row from the user table
421 * @return \type{User}
422 */
423 static function newFromRow( $row ) {
424 $user = new User;
425 $user->loadFromRow( $row );
426 return $user;
427 }
428
429 //@}
430
431
432 /**
433 * Get the username corresponding to a given user ID
434 * @param $id \int User ID
435 * @return \string The corresponding username
436 */
437 static function whoIs( $id ) {
438 $dbr = wfGetDB( DB_SLAVE );
439 return $dbr->selectField( 'user', 'user_name', array( 'user_id' => $id ), 'User::whoIs' );
440 }
441
442 /**
443 * Get the real name of a user given their user ID
444 *
445 * @param $id \int User ID
446 * @return \string The corresponding user's real name
447 */
448 static function whoIsReal( $id ) {
449 $dbr = wfGetDB( DB_SLAVE );
450 return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ), __METHOD__ );
451 }
452
453 /**
454 * Get database id given a user name
455 * @param $name \string Username
456 * @return \types{\int,\null} The corresponding user's ID, or null if user is nonexistent
457 */
458 static function idFromName( $name ) {
459 $nt = Title::makeTitleSafe( NS_USER, $name );
460 if( is_null( $nt ) ) {
461 # Illegal name
462 return null;
463 }
464
465 if ( isset(self::$idCacheByName[$name]) ) {
466 return self::$idCacheByName[$name];
467 }
468
469 $dbr = wfGetDB( DB_SLAVE );
470 $s = $dbr->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), __METHOD__ );
471
472 if ( $s === false ) {
473 $result = null;
474 } else {
475 $result = $s->user_id;
476 }
477
478 self::$idCacheByName[$name] = $result;
479
480 if ( count(self::$idCacheByName) > 1000 ) {
481 self::$idCacheByName = array();
482 }
483
484 return $result;
485 }
486
487 /**
488 * Does the string match an anonymous IPv4 address?
489 *
490 * This function exists for username validation, in order to reject
491 * usernames which are similar in form to IP addresses. Strings such
492 * as 300.300.300.300 will return true because it looks like an IP
493 * address, despite not being strictly valid.
494 *
495 * We match \d{1,3}\.\d{1,3}\.\d{1,3}\.xxx as an anonymous IP
496 * address because the usemod software would "cloak" anonymous IP
497 * addresses like this, if we allowed accounts like this to be created
498 * new users could get the old edits of these anonymous users.
499 *
500 * @param $name \string String to match
501 * @return \bool True or false
502 */
503 static function isIP( $name ) {
504 return preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/',$name) || IP::isIPv6($name);
505 }
506
507 /**
508 * Is the input a valid username?
509 *
510 * Checks if the input is a valid username, we don't want an empty string,
511 * an IP address, anything that containins slashes (would mess up subpages),
512 * is longer than the maximum allowed username size or doesn't begin with
513 * a capital letter.
514 *
515 * @param $name \string String to match
516 * @return \bool True or false
517 */
518 static function isValidUserName( $name ) {
519 global $wgContLang, $wgMaxNameChars;
520
521 if ( $name == ''
522 || User::isIP( $name )
523 || strpos( $name, '/' ) !== false
524 || strlen( $name ) > $wgMaxNameChars
525 || $name != $wgContLang->ucfirst( $name ) ) {
526 wfDebugLog( 'username', __METHOD__ .
527 ": '$name' invalid due to empty, IP, slash, length, or lowercase" );
528 return false;
529 }
530
531 // Ensure that the name can't be misresolved as a different title,
532 // such as with extra namespace keys at the start.
533 $parsed = Title::newFromText( $name );
534 if( is_null( $parsed )
535 || $parsed->getNamespace()
536 || strcmp( $name, $parsed->getPrefixedText() ) ) {
537 wfDebugLog( 'username', __METHOD__ .
538 ": '$name' invalid due to ambiguous prefixes" );
539 return false;
540 }
541
542 // Check an additional blacklist of troublemaker characters.
543 // Should these be merged into the title char list?
544 $unicodeBlacklist = '/[' .
545 '\x{0080}-\x{009f}' . # iso-8859-1 control chars
546 '\x{00a0}' . # non-breaking space
547 '\x{2000}-\x{200f}' . # various whitespace
548 '\x{2028}-\x{202f}' . # breaks and control chars
549 '\x{3000}' . # ideographic space
550 '\x{e000}-\x{f8ff}' . # private use
551 ']/u';
552 if( preg_match( $unicodeBlacklist, $name ) ) {
553 wfDebugLog( 'username', __METHOD__ .
554 ": '$name' invalid due to blacklisted characters" );
555 return false;
556 }
557
558 return true;
559 }
560
561 /**
562 * Usernames which fail to pass this function will be blocked
563 * from user login and new account registrations, but may be used
564 * internally by batch processes.
565 *
566 * If an account already exists in this form, login will be blocked
567 * by a failure to pass this function.
568 *
569 * @param $name \string String to match
570 * @return \bool True or false
571 */
572 static function isUsableName( $name ) {
573 global $wgReservedUsernames;
574 // Must be a valid username, obviously ;)
575 if ( !self::isValidUserName( $name ) ) {
576 return false;
577 }
578
579 static $reservedUsernames = false;
580 if ( !$reservedUsernames ) {
581 $reservedUsernames = $wgReservedUsernames;
582 wfRunHooks( 'UserGetReservedNames', array( &$reservedUsernames ) );
583 }
584
585 // Certain names may be reserved for batch processes.
586 foreach ( $reservedUsernames as $reserved ) {
587 if ( substr( $reserved, 0, 4 ) == 'msg:' ) {
588 $reserved = wfMsgForContent( substr( $reserved, 4 ) );
589 }
590 if ( $reserved == $name ) {
591 return false;
592 }
593 }
594 return true;
595 }
596
597 /**
598 * Usernames which fail to pass this function will be blocked
599 * from new account registrations, but may be used internally
600 * either by batch processes or by user accounts which have
601 * already been created.
602 *
603 * Additional character blacklisting may be added here
604 * rather than in isValidUserName() to avoid disrupting
605 * existing accounts.
606 *
607 * @param $name \string String to match
608 * @return \bool True or false
609 */
610 static function isCreatableName( $name ) {
611 global $wgInvalidUsernameCharacters;
612 return
613 self::isUsableName( $name ) &&
614
615 // Registration-time character blacklisting...
616 !preg_match( '/[' . preg_quote( $wgInvalidUsernameCharacters, '/' ) . ']/', $name );
617 }
618
619 /**
620 * Is the input a valid password for this user?
621 *
622 * @param $password String Desired password
623 * @return mixed: true on success, string of error message on failure
624 */
625 function isValidPassword( $password ) {
626 global $wgMinimalPasswordLength, $wgContLang;
627
628 if( !wfRunHooks( 'isValidPassword', array( $password, &$result, $this ) ) )
629 return $result;
630
631 // Password needs to be long enough
632 if( strlen( $password ) < $wgMinimalPasswordLength ) {
633 return 'passwordtooshort';
634 } elseif( $wgContLang->lc( $password ) == $wgContLang->lc( $this->mName ) ) {
635 return 'password-name-match';
636 } else {
637 return true;
638 }
639 }
640
641 /**
642 * Does a string look like an e-mail address?
643 *
644 * There used to be a regular expression here, it got removed because it
645 * rejected valid addresses. Actually just check if there is '@' somewhere
646 * in the given address.
647 *
648 * @todo Check for RFC 2822 compilance (bug 959)
649 *
650 * @param $addr \string E-mail address
651 * @return \bool True or false
652 */
653 public static function isValidEmailAddr( $addr ) {
654 $result = null;
655 if( !wfRunHooks( 'isValidEmailAddr', array( $addr, &$result ) ) ) {
656 return $result;
657 }
658
659 return strpos( $addr, '@' ) !== false;
660 }
661
662 /**
663 * Given unvalidated user input, return a canonical username, or false if
664 * the username is invalid.
665 * @param $name \string User input
666 * @param $validate \types{\string,\bool} Type of validation to use:
667 * - false No validation
668 * - 'valid' Valid for batch processes
669 * - 'usable' Valid for batch processes and login
670 * - 'creatable' Valid for batch processes, login and account creation
671 */
672 static function getCanonicalName( $name, $validate = 'valid' ) {
673 # Force usernames to capital
674 global $wgContLang;
675 $name = $wgContLang->ucfirst( $name );
676
677 # Reject names containing '#'; these will be cleaned up
678 # with title normalisation, but then it's too late to
679 # check elsewhere
680 if( strpos( $name, '#' ) !== false )
681 return false;
682
683 # Clean up name according to title rules
684 $t = ($validate === 'valid') ?
685 Title::newFromText( $name ) : Title::makeTitle( NS_USER, $name );
686 # Check for invalid titles
687 if( is_null( $t ) ) {
688 return false;
689 }
690
691 # Reject various classes of invalid names
692 $name = $t->getText();
693 global $wgAuth;
694 $name = $wgAuth->getCanonicalName( $t->getText() );
695
696 switch ( $validate ) {
697 case false:
698 break;
699 case 'valid':
700 if ( !User::isValidUserName( $name ) ) {
701 $name = false;
702 }
703 break;
704 case 'usable':
705 if ( !User::isUsableName( $name ) ) {
706 $name = false;
707 }
708 break;
709 case 'creatable':
710 if ( !User::isCreatableName( $name ) ) {
711 $name = false;
712 }
713 break;
714 default:
715 throw new MWException( 'Invalid parameter value for $validate in '.__METHOD__ );
716 }
717 return $name;
718 }
719
720 /**
721 * Count the number of edits of a user
722 * @todo It should not be static and some day should be merged as proper member function / deprecated -- domas
723 *
724 * @param $uid \int User ID to check
725 * @return \int The user's edit count
726 */
727 static function edits( $uid ) {
728 wfProfileIn( __METHOD__ );
729 $dbr = wfGetDB( DB_SLAVE );
730 // check if the user_editcount field has been initialized
731 $field = $dbr->selectField(
732 'user', 'user_editcount',
733 array( 'user_id' => $uid ),
734 __METHOD__
735 );
736
737 if( $field === null ) { // it has not been initialized. do so.
738 $dbw = wfGetDB( DB_MASTER );
739 $count = $dbr->selectField(
740 'revision', 'count(*)',
741 array( 'rev_user' => $uid ),
742 __METHOD__
743 );
744 $dbw->update(
745 'user',
746 array( 'user_editcount' => $count ),
747 array( 'user_id' => $uid ),
748 __METHOD__
749 );
750 } else {
751 $count = $field;
752 }
753 wfProfileOut( __METHOD__ );
754 return $count;
755 }
756
757 /**
758 * Return a random password. Sourced from mt_rand, so it's not particularly secure.
759 * @todo hash random numbers to improve security, like generateToken()
760 *
761 * @return \string New random password
762 */
763 static function randomPassword() {
764 global $wgMinimalPasswordLength;
765 $pwchars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz';
766 $l = strlen( $pwchars ) - 1;
767
768 $pwlength = max( 7, $wgMinimalPasswordLength );
769 $digit = mt_rand(0, $pwlength - 1);
770 $np = '';
771 for ( $i = 0; $i < $pwlength; $i++ ) {
772 $np .= $i == $digit ? chr( mt_rand(48, 57) ) : $pwchars{ mt_rand(0, $l)};
773 }
774 return $np;
775 }
776
777 /**
778 * Set cached properties to default.
779 *
780 * @note This no longer clears uncached lazy-initialised properties;
781 * the constructor does that instead.
782 * @private
783 */
784 function loadDefaults( $name = false ) {
785 wfProfileIn( __METHOD__ );
786
787 global $wgCookiePrefix;
788
789 $this->mId = 0;
790 $this->mName = $name;
791 $this->mRealName = '';
792 $this->mPassword = $this->mNewpassword = '';
793 $this->mNewpassTime = null;
794 $this->mEmail = '';
795 $this->mOptionOverrides = null;
796 $this->mOptionsLoaded = false;
797
798 if ( isset( $_COOKIE[$wgCookiePrefix.'LoggedOut'] ) ) {
799 $this->mTouched = wfTimestamp( TS_MW, $_COOKIE[$wgCookiePrefix.'LoggedOut'] );
800 } else {
801 $this->mTouched = '0'; # Allow any pages to be cached
802 }
803
804 $this->setToken(); # Random
805 $this->mEmailAuthenticated = null;
806 $this->mEmailToken = '';
807 $this->mEmailTokenExpires = null;
808 $this->mRegistration = wfTimestamp( TS_MW );
809 $this->mGroups = array();
810
811 wfRunHooks( 'UserLoadDefaults', array( $this, $name ) );
812
813 wfProfileOut( __METHOD__ );
814 }
815
816 /**
817 * @deprecated Use wfSetupSession().
818 */
819 function SetupSession() {
820 wfDeprecated( __METHOD__ );
821 wfSetupSession();
822 }
823
824 /**
825 * Load user data from the session or login cookie. If there are no valid
826 * credentials, initialises the user as an anonymous user.
827 * @return \bool True if the user is logged in, false otherwise.
828 */
829 private function loadFromSession() {
830 global $wgMemc, $wgCookiePrefix;
831
832 $result = null;
833 wfRunHooks( 'UserLoadFromSession', array( $this, &$result ) );
834 if ( $result !== null ) {
835 return $result;
836 }
837
838 if ( isset( $_COOKIE["{$wgCookiePrefix}UserID"] ) ) {
839 $sId = intval( $_COOKIE["{$wgCookiePrefix}UserID"] );
840 if( isset( $_SESSION['wsUserID'] ) && $sId != $_SESSION['wsUserID'] ) {
841 $this->loadDefaults(); // Possible collision!
842 wfDebugLog( 'loginSessions', "Session user ID ({$_SESSION['wsUserID']}) and
843 cookie user ID ($sId) don't match!" );
844 return false;
845 }
846 $_SESSION['wsUserID'] = $sId;
847 } else if ( isset( $_SESSION['wsUserID'] ) ) {
848 if ( $_SESSION['wsUserID'] != 0 ) {
849 $sId = $_SESSION['wsUserID'];
850 } else {
851 $this->loadDefaults();
852 return false;
853 }
854 } else {
855 $this->loadDefaults();
856 return false;
857 }
858
859 if ( isset( $_SESSION['wsUserName'] ) ) {
860 $sName = $_SESSION['wsUserName'];
861 } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserName"] ) ) {
862 $sName = $_COOKIE["{$wgCookiePrefix}UserName"];
863 $_SESSION['wsUserName'] = $sName;
864 } else {
865 $this->loadDefaults();
866 return false;
867 }
868
869 $passwordCorrect = FALSE;
870 $this->mId = $sId;
871 if ( !$this->loadFromId() ) {
872 # Not a valid ID, loadFromId has switched the object to anon for us
873 return false;
874 }
875
876 if ( isset( $_SESSION['wsToken'] ) ) {
877 $passwordCorrect = $_SESSION['wsToken'] == $this->mToken;
878 $from = 'session';
879 } else if ( isset( $_COOKIE["{$wgCookiePrefix}Token"] ) ) {
880 $passwordCorrect = $this->mToken == $_COOKIE["{$wgCookiePrefix}Token"];
881 $from = 'cookie';
882 } else {
883 # No session or persistent login cookie
884 $this->loadDefaults();
885 return false;
886 }
887
888 if ( ( $sName == $this->mName ) && $passwordCorrect ) {
889 $_SESSION['wsToken'] = $this->mToken;
890 wfDebug( "Logged in from $from\n" );
891 return true;
892 } else {
893 # Invalid credentials
894 wfDebug( "Can't log in from $from, invalid credentials\n" );
895 $this->loadDefaults();
896 return false;
897 }
898 }
899
900 /**
901 * Load user and user_group data from the database.
902 * $this::mId must be set, this is how the user is identified.
903 *
904 * @return \bool True if the user exists, false if the user is anonymous
905 * @private
906 */
907 function loadFromDatabase() {
908 # Paranoia
909 $this->mId = intval( $this->mId );
910
911 /** Anonymous user */
912 if( !$this->mId ) {
913 $this->loadDefaults();
914 return false;
915 }
916
917 $dbr = wfGetDB( DB_MASTER );
918 $s = $dbr->selectRow( 'user', '*', array( 'user_id' => $this->mId ), __METHOD__ );
919
920 wfRunHooks( 'UserLoadFromDatabase', array( $this, &$s ) );
921
922 if ( $s !== false ) {
923 # Initialise user table data
924 $this->loadFromRow( $s );
925 $this->mGroups = null; // deferred
926 $this->getEditCount(); // revalidation for nulls
927 return true;
928 } else {
929 # Invalid user_id
930 $this->mId = 0;
931 $this->loadDefaults();
932 return false;
933 }
934 }
935
936 /**
937 * Initialize this object from a row from the user table.
938 *
939 * @param $row \type{\arrayof{\mixed}} Row from the user table to load.
940 */
941 function loadFromRow( $row ) {
942 $this->mDataLoaded = true;
943
944 if ( isset( $row->user_id ) ) {
945 $this->mId = intval( $row->user_id );
946 }
947 $this->mName = $row->user_name;
948 $this->mRealName = $row->user_real_name;
949 $this->mPassword = $row->user_password;
950 $this->mNewpassword = $row->user_newpassword;
951 $this->mNewpassTime = wfTimestampOrNull( TS_MW, $row->user_newpass_time );
952 $this->mEmail = $row->user_email;
953 $this->decodeOptions( $row->user_options );
954 $this->mTouched = wfTimestamp(TS_MW,$row->user_touched);
955 $this->mToken = $row->user_token;
956 $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated );
957 $this->mEmailToken = $row->user_email_token;
958 $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires );
959 $this->mRegistration = wfTimestampOrNull( TS_MW, $row->user_registration );
960 $this->mEditCount = $row->user_editcount;
961 }
962
963 /**
964 * Load the groups from the database if they aren't already loaded.
965 * @private
966 */
967 function loadGroups() {
968 if ( is_null( $this->mGroups ) ) {
969 $dbr = wfGetDB( DB_MASTER );
970 $res = $dbr->select( 'user_groups',
971 array( 'ug_group' ),
972 array( 'ug_user' => $this->mId ),
973 __METHOD__ );
974 $this->mGroups = array();
975 while( $row = $dbr->fetchObject( $res ) ) {
976 $this->mGroups[] = $row->ug_group;
977 }
978 }
979 }
980
981 /**
982 * Clear various cached data stored in this object.
983 * @param $reloadFrom \string Reload user and user_groups table data from a
984 * given source. May be "name", "id", "defaults", "session", or false for
985 * no reload.
986 */
987 function clearInstanceCache( $reloadFrom = false ) {
988 $this->mNewtalk = -1;
989 $this->mDatePreference = null;
990 $this->mBlockedby = -1; # Unset
991 $this->mHash = false;
992 $this->mSkin = null;
993 $this->mRights = null;
994 $this->mEffectiveGroups = null;
995 $this->mOptions = null;
996
997 if ( $reloadFrom ) {
998 $this->mDataLoaded = false;
999 $this->mFrom = $reloadFrom;
1000 }
1001 }
1002
1003 /**
1004 * Combine the language default options with any site-specific options
1005 * and add the default language variants.
1006 *
1007 * @return \type{\arrayof{\string}} Array of options
1008 */
1009 static function getDefaultOptions() {
1010 global $wgNamespacesToBeSearchedDefault;
1011 /**
1012 * Site defaults will override the global/language defaults
1013 */
1014 global $wgDefaultUserOptions, $wgContLang, $wgDefaultSkin;
1015 $defOpt = $wgDefaultUserOptions + $wgContLang->getDefaultUserOptionOverrides();
1016
1017 /**
1018 * default language setting
1019 */
1020 $variant = $wgContLang->getPreferredVariant( false );
1021 $defOpt['variant'] = $variant;
1022 $defOpt['language'] = $variant;
1023 foreach( SearchEngine::searchableNamespaces() as $nsnum => $nsname ) {
1024 $defOpt['searchNs'.$nsnum] = !empty($wgNamespacesToBeSearchedDefault[$nsnum]);
1025 }
1026 $defOpt['skin'] = $wgDefaultSkin;
1027
1028 return $defOpt;
1029 }
1030
1031 /**
1032 * Get a given default option value.
1033 *
1034 * @param $opt \string Name of option to retrieve
1035 * @return \string Default option value
1036 */
1037 public static function getDefaultOption( $opt ) {
1038 $defOpts = self::getDefaultOptions();
1039 if( isset( $defOpts[$opt] ) ) {
1040 return $defOpts[$opt];
1041 } else {
1042 return null;
1043 }
1044 }
1045
1046 /**
1047 * Get a list of user toggle names
1048 * @return \type{\arrayof{\string}} Array of user toggle names
1049 */
1050 static function getToggles() {
1051 global $wgContLang, $wgUseRCPatrol;
1052 $extraToggles = array();
1053 wfRunHooks( 'UserToggles', array( &$extraToggles ) );
1054 if( $wgUseRCPatrol ) {
1055 $extraToggles[] = 'hidepatrolled';
1056 $extraToggles[] = 'newpageshidepatrolled';
1057 $extraToggles[] = 'watchlisthidepatrolled';
1058 }
1059 return array_merge( self::$mToggles, $extraToggles, $wgContLang->getExtraUserToggles() );
1060 }
1061
1062
1063 /**
1064 * Get blocking information
1065 * @private
1066 * @param $bFromSlave \bool Whether to check the slave database first. To
1067 * improve performance, non-critical checks are done
1068 * against slaves. Check when actually saving should be
1069 * done against master.
1070 */
1071 function getBlockedStatus( $bFromSlave = true ) {
1072 global $wgEnableSorbs, $wgProxyWhitelist, $wgUser;
1073
1074 if ( -1 != $this->mBlockedby ) {
1075 wfDebug( "User::getBlockedStatus: already loaded.\n" );
1076 return;
1077 }
1078
1079 wfProfileIn( __METHOD__ );
1080 wfDebug( __METHOD__.": checking...\n" );
1081
1082 // Initialize data...
1083 // Otherwise something ends up stomping on $this->mBlockedby when
1084 // things get lazy-loaded later, causing false positive block hits
1085 // due to -1 !== 0. Probably session-related... Nothing should be
1086 // overwriting mBlockedby, surely?
1087 $this->load();
1088
1089 $this->mBlockedby = 0;
1090 $this->mHideName = 0;
1091 $this->mAllowUsertalk = 0;
1092
1093 # Check if we are looking at an IP or a logged-in user
1094 if ( $this->isIP( $this->getName() ) ) {
1095 $ip = $this->getName();
1096 }
1097 else {
1098 # Check if we are looking at the current user
1099 # If we don't, and the user is logged in, we don't know about
1100 # his IP / autoblock status, so ignore autoblock of current user's IP
1101 if ( $this->getID() != $wgUser->getID() ) {
1102 $ip = '';
1103 }
1104 else {
1105 # Get IP of current user
1106 $ip = wfGetIP();
1107 }
1108 }
1109
1110 if ( $this->isAllowed( 'ipblock-exempt' ) ) {
1111 # Exempt from all types of IP-block
1112 $ip = '';
1113 }
1114
1115 # User/IP blocking
1116 $this->mBlock = new Block();
1117 $this->mBlock->fromMaster( !$bFromSlave );
1118 if ( $this->mBlock->load( $ip , $this->mId ) ) {
1119 wfDebug( __METHOD__.": Found block.\n" );
1120 $this->mBlockedby = $this->mBlock->mBy;
1121 $this->mBlockreason = $this->mBlock->mReason;
1122 $this->mHideName = $this->mBlock->mHideName;
1123 $this->mAllowUsertalk = $this->mBlock->mAllowUsertalk;
1124 if ( $this->isLoggedIn() && $wgUser->getID() == $this->getID() ) {
1125 $this->spreadBlock();
1126 }
1127 } else {
1128 // Bug 13611: don't remove mBlock here, to allow account creation blocks to
1129 // apply to users. Note that the existence of $this->mBlock is not used to
1130 // check for edit blocks, $this->mBlockedby is instead.
1131 }
1132
1133 # Proxy blocking
1134 if ( !$this->isAllowed('proxyunbannable') && !in_array( $ip, $wgProxyWhitelist ) ) {
1135 # Local list
1136 if ( wfIsLocallyBlockedProxy( $ip ) ) {
1137 $this->mBlockedby = wfMsg( 'proxyblocker' );
1138 $this->mBlockreason = wfMsg( 'proxyblockreason' );
1139 }
1140
1141 # DNSBL
1142 if ( !$this->mBlockedby && $wgEnableSorbs && !$this->getID() ) {
1143 if ( $this->inSorbsBlacklist( $ip ) ) {
1144 $this->mBlockedby = wfMsg( 'sorbs' );
1145 $this->mBlockreason = wfMsg( 'sorbsreason' );
1146 }
1147 }
1148 }
1149
1150 # Extensions
1151 wfRunHooks( 'GetBlockedStatus', array( &$this ) );
1152
1153 wfProfileOut( __METHOD__ );
1154 }
1155
1156 /**
1157 * Whether the given IP is in the SORBS blacklist.
1158 *
1159 * @param $ip \string IP to check
1160 * @return \bool True if blacklisted.
1161 */
1162 function inSorbsBlacklist( $ip ) {
1163 global $wgEnableSorbs, $wgSorbsUrl;
1164
1165 return $wgEnableSorbs &&
1166 $this->inDnsBlacklist( $ip, $wgSorbsUrl );
1167 }
1168
1169 /**
1170 * Whether the given IP is in a given DNS blacklist.
1171 *
1172 * @param $ip \string IP to check
1173 * @param $base \string URL of the DNS blacklist
1174 * @return \bool True if blacklisted.
1175 */
1176 function inDnsBlacklist( $ip, $base ) {
1177 wfProfileIn( __METHOD__ );
1178
1179 $found = false;
1180 $host = '';
1181 // FIXME: IPv6 ??? (http://bugs.php.net/bug.php?id=33170)
1182 if( IP::isIPv4($ip) ) {
1183 # Make hostname
1184 $host = "$ip.$base";
1185
1186 # Send query
1187 $ipList = gethostbynamel( $host );
1188
1189 if( $ipList ) {
1190 wfDebug( "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" );
1191 $found = true;
1192 } else {
1193 wfDebug( "Requested $host, not found in $base.\n" );
1194 }
1195 }
1196
1197 wfProfileOut( __METHOD__ );
1198 return $found;
1199 }
1200
1201 /**
1202 * Is this user subject to rate limiting?
1203 *
1204 * @return \bool True if rate limited
1205 */
1206 public function isPingLimitable() {
1207 global $wgRateLimitsExcludedGroups;
1208 if( array_intersect( $this->getEffectiveGroups(), $wgRateLimitsExcludedGroups ) ) {
1209 // Deprecated, but kept for backwards-compatibility config
1210 return false;
1211 }
1212 return !$this->isAllowed('noratelimit');
1213 }
1214
1215 /**
1216 * Primitive rate limits: enforce maximum actions per time period
1217 * to put a brake on flooding.
1218 *
1219 * @note When using a shared cache like memcached, IP-address
1220 * last-hit counters will be shared across wikis.
1221 *
1222 * @param $action \string Action to enforce; 'edit' if unspecified
1223 * @return \bool True if a rate limiter was tripped
1224 */
1225 function pingLimiter( $action='edit' ) {
1226
1227 # Call the 'PingLimiter' hook
1228 $result = false;
1229 if( !wfRunHooks( 'PingLimiter', array( &$this, $action, $result ) ) ) {
1230 return $result;
1231 }
1232
1233 global $wgRateLimits;
1234 if( !isset( $wgRateLimits[$action] ) ) {
1235 return false;
1236 }
1237
1238 # Some groups shouldn't trigger the ping limiter, ever
1239 if( !$this->isPingLimitable() )
1240 return false;
1241
1242 global $wgMemc, $wgRateLimitLog;
1243 wfProfileIn( __METHOD__ );
1244
1245 $limits = $wgRateLimits[$action];
1246 $keys = array();
1247 $id = $this->getId();
1248 $ip = wfGetIP();
1249 $userLimit = false;
1250
1251 if( isset( $limits['anon'] ) && $id == 0 ) {
1252 $keys[wfMemcKey( 'limiter', $action, 'anon' )] = $limits['anon'];
1253 }
1254
1255 if( isset( $limits['user'] ) && $id != 0 ) {
1256 $userLimit = $limits['user'];
1257 }
1258 if( $this->isNewbie() ) {
1259 if( isset( $limits['newbie'] ) && $id != 0 ) {
1260 $keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $limits['newbie'];
1261 }
1262 if( isset( $limits['ip'] ) ) {
1263 $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip'];
1264 }
1265 $matches = array();
1266 if( isset( $limits['subnet'] ) && preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
1267 $subnet = $matches[1];
1268 $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
1269 }
1270 }
1271 // Check for group-specific permissions
1272 // If more than one group applies, use the group with the highest limit
1273 foreach ( $this->getGroups() as $group ) {
1274 if ( isset( $limits[$group] ) ) {
1275 if ( $userLimit === false || $limits[$group] > $userLimit ) {
1276 $userLimit = $limits[$group];
1277 }
1278 }
1279 }
1280 // Set the user limit key
1281 if ( $userLimit !== false ) {
1282 wfDebug( __METHOD__.": effective user limit: $userLimit\n" );
1283 $keys[ wfMemcKey( 'limiter', $action, 'user', $id ) ] = $userLimit;
1284 }
1285
1286 $triggered = false;
1287 foreach( $keys as $key => $limit ) {
1288 list( $max, $period ) = $limit;
1289 $summary = "(limit $max in {$period}s)";
1290 $count = $wgMemc->get( $key );
1291 if( $count ) {
1292 if( $count > $max ) {
1293 wfDebug( __METHOD__.": tripped! $key at $count $summary\n" );
1294 if( $wgRateLimitLog ) {
1295 @error_log( wfTimestamp( TS_MW ) . ' ' . wfWikiID() . ': ' . $this->getName() . " tripped $key at $count $summary\n", 3, $wgRateLimitLog );
1296 }
1297 $triggered = true;
1298 } else {
1299 wfDebug( __METHOD__.": ok. $key at $count $summary\n" );
1300 }
1301 } else {
1302 wfDebug( __METHOD__.": adding record for $key $summary\n" );
1303 $wgMemc->add( $key, 1, intval( $period ) );
1304 }
1305 $wgMemc->incr( $key );
1306 }
1307
1308 wfProfileOut( __METHOD__ );
1309 return $triggered;
1310 }
1311
1312 /**
1313 * Check if user is blocked
1314 *
1315 * @param $bFromSlave \bool Whether to check the slave database instead of the master
1316 * @return \bool True if blocked, false otherwise
1317 */
1318 function isBlocked( $bFromSlave = true ) { // hacked from false due to horrible probs on site
1319 wfDebug( "User::isBlocked: enter\n" );
1320 $this->getBlockedStatus( $bFromSlave );
1321 return $this->mBlockedby !== 0;
1322 }
1323
1324 /**
1325 * Check if user is blocked from editing a particular article
1326 *
1327 * @param $title \string Title to check
1328 * @param $bFromSlave \bool Whether to check the slave database instead of the master
1329 * @return \bool True if blocked, false otherwise
1330 */
1331 function isBlockedFrom( $title, $bFromSlave = false ) {
1332 global $wgBlockAllowsUTEdit;
1333 wfProfileIn( __METHOD__ );
1334 wfDebug( __METHOD__.": enter\n" );
1335
1336 wfDebug( __METHOD__.": asking isBlocked()\n" );
1337 $blocked = $this->isBlocked( $bFromSlave );
1338 $allowUsertalk = ($wgBlockAllowsUTEdit ? $this->mAllowUsertalk : false);
1339 # If a user's name is suppressed, they cannot make edits anywhere
1340 if ( !$this->mHideName && $allowUsertalk && $title->getText() === $this->getName() &&
1341 $title->getNamespace() == NS_USER_TALK ) {
1342 $blocked = false;
1343 wfDebug( __METHOD__.": self-talk page, ignoring any blocks\n" );
1344 }
1345 wfProfileOut( __METHOD__ );
1346 return $blocked;
1347 }
1348
1349 /**
1350 * If user is blocked, return the name of the user who placed the block
1351 * @return \string name of blocker
1352 */
1353 function blockedBy() {
1354 $this->getBlockedStatus();
1355 return $this->mBlockedby;
1356 }
1357
1358 /**
1359 * If user is blocked, return the specified reason for the block
1360 * @return \string Blocking reason
1361 */
1362 function blockedFor() {
1363 $this->getBlockedStatus();
1364 return $this->mBlockreason;
1365 }
1366
1367 /**
1368 * If user is blocked, return the ID for the block
1369 * @return \int Block ID
1370 */
1371 function getBlockId() {
1372 $this->getBlockedStatus();
1373 return ($this->mBlock ? $this->mBlock->mId : false);
1374 }
1375
1376 /**
1377 * Check if user is blocked on all wikis.
1378 * Do not use for actual edit permission checks!
1379 * This is intented for quick UI checks.
1380 *
1381 * @param $ip \type{\string} IP address, uses current client if none given
1382 * @return \type{\bool} True if blocked, false otherwise
1383 */
1384 function isBlockedGlobally( $ip = '' ) {
1385 if( $this->mBlockedGlobally !== null ) {
1386 return $this->mBlockedGlobally;
1387 }
1388 // User is already an IP?
1389 if( IP::isIPAddress( $this->getName() ) ) {
1390 $ip = $this->getName();
1391 } else if( !$ip ) {
1392 $ip = wfGetIP();
1393 }
1394 $blocked = false;
1395 wfRunHooks( 'UserIsBlockedGlobally', array( &$this, $ip, &$blocked ) );
1396 $this->mBlockedGlobally = (bool)$blocked;
1397 return $this->mBlockedGlobally;
1398 }
1399
1400 /**
1401 * Check if user account is locked
1402 *
1403 * @return \type{\bool} True if locked, false otherwise
1404 */
1405 function isLocked() {
1406 if( $this->mLocked !== null ) {
1407 return $this->mLocked;
1408 }
1409 global $wgAuth;
1410 $authUser = $wgAuth->getUserInstance( $this );
1411 $this->mLocked = (bool)$authUser->isLocked();
1412 return $this->mLocked;
1413 }
1414
1415 /**
1416 * Check if user account is hidden
1417 *
1418 * @return \type{\bool} True if hidden, false otherwise
1419 */
1420 function isHidden() {
1421 if( $this->mHideName !== null ) {
1422 return $this->mHideName;
1423 }
1424 $this->getBlockedStatus();
1425 if( !$this->mHideName ) {
1426 global $wgAuth;
1427 $authUser = $wgAuth->getUserInstance( $this );
1428 $this->mHideName = (bool)$authUser->isHidden();
1429 }
1430 return $this->mHideName;
1431 }
1432
1433 /**
1434 * Get the user's ID.
1435 * @return \int The user's ID; 0 if the user is anonymous or nonexistent
1436 */
1437 function getId() {
1438 if( $this->mId === null and $this->mName !== null
1439 and User::isIP( $this->mName ) ) {
1440 // Special case, we know the user is anonymous
1441 return 0;
1442 } elseif( $this->mId === null ) {
1443 // Don't load if this was initialized from an ID
1444 $this->load();
1445 }
1446 return $this->mId;
1447 }
1448
1449 /**
1450 * Set the user and reload all fields according to a given ID
1451 * @param $v \int User ID to reload
1452 */
1453 function setId( $v ) {
1454 $this->mId = $v;
1455 $this->clearInstanceCache( 'id' );
1456 }
1457
1458 /**
1459 * Get the user name, or the IP of an anonymous user
1460 * @return \string User's name or IP address
1461 */
1462 function getName() {
1463 if ( !$this->mDataLoaded && $this->mFrom == 'name' ) {
1464 # Special case optimisation
1465 return $this->mName;
1466 } else {
1467 $this->load();
1468 if ( $this->mName === false ) {
1469 # Clean up IPs
1470 $this->mName = IP::sanitizeIP( wfGetIP() );
1471 }
1472 return $this->mName;
1473 }
1474 }
1475
1476 /**
1477 * Set the user name.
1478 *
1479 * This does not reload fields from the database according to the given
1480 * name. Rather, it is used to create a temporary "nonexistent user" for
1481 * later addition to the database. It can also be used to set the IP
1482 * address for an anonymous user to something other than the current
1483 * remote IP.
1484 *
1485 * @note User::newFromName() has rougly the same function, when the named user
1486 * does not exist.
1487 * @param $str \string New user name to set
1488 */
1489 function setName( $str ) {
1490 $this->load();
1491 $this->mName = $str;
1492 }
1493
1494 /**
1495 * Get the user's name escaped by underscores.
1496 * @return \string Username escaped by underscores.
1497 */
1498 function getTitleKey() {
1499 return str_replace( ' ', '_', $this->getName() );
1500 }
1501
1502 /**
1503 * Check if the user has new messages.
1504 * @return \bool True if the user has new messages
1505 */
1506 function getNewtalk() {
1507 $this->load();
1508
1509 # Load the newtalk status if it is unloaded (mNewtalk=-1)
1510 if( $this->mNewtalk === -1 ) {
1511 $this->mNewtalk = false; # reset talk page status
1512
1513 # Check memcached separately for anons, who have no
1514 # entire User object stored in there.
1515 if( !$this->mId ) {
1516 global $wgMemc;
1517 $key = wfMemcKey( 'newtalk', 'ip', $this->getName() );
1518 $newtalk = $wgMemc->get( $key );
1519 if( strval( $newtalk ) !== '' ) {
1520 $this->mNewtalk = (bool)$newtalk;
1521 } else {
1522 // Since we are caching this, make sure it is up to date by getting it
1523 // from the master
1524 $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName(), true );
1525 $wgMemc->set( $key, (int)$this->mNewtalk, 1800 );
1526 }
1527 } else {
1528 $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
1529 }
1530 }
1531
1532 return (bool)$this->mNewtalk;
1533 }
1534
1535 /**
1536 * Return the talk page(s) this user has new messages on.
1537 * @return \type{\arrayof{\string}} Array of page URLs
1538 */
1539 function getNewMessageLinks() {
1540 $talks = array();
1541 if (!wfRunHooks('UserRetrieveNewTalks', array(&$this, &$talks)))
1542 return $talks;
1543
1544 if (!$this->getNewtalk())
1545 return array();
1546 $up = $this->getUserPage();
1547 $utp = $up->getTalkPage();
1548 return array(array("wiki" => wfWikiID(), "link" => $utp->getLocalURL()));
1549 }
1550
1551
1552 /**
1553 * Internal uncached check for new messages
1554 *
1555 * @see getNewtalk()
1556 * @param $field \string 'user_ip' for anonymous users, 'user_id' otherwise
1557 * @param $id \types{\string,\int} User's IP address for anonymous users, User ID otherwise
1558 * @param $fromMaster \bool true to fetch from the master, false for a slave
1559 * @return \bool True if the user has new messages
1560 * @private
1561 */
1562 function checkNewtalk( $field, $id, $fromMaster = false ) {
1563 if ( $fromMaster ) {
1564 $db = wfGetDB( DB_MASTER );
1565 } else {
1566 $db = wfGetDB( DB_SLAVE );
1567 }
1568 $ok = $db->selectField( 'user_newtalk', $field,
1569 array( $field => $id ), __METHOD__ );
1570 return $ok !== false;
1571 }
1572
1573 /**
1574 * Add or update the new messages flag
1575 * @param $field \string 'user_ip' for anonymous users, 'user_id' otherwise
1576 * @param $id \types{\string,\int} User's IP address for anonymous users, User ID otherwise
1577 * @return \bool True if successful, false otherwise
1578 * @private
1579 */
1580 function updateNewtalk( $field, $id ) {
1581 $dbw = wfGetDB( DB_MASTER );
1582 $dbw->insert( 'user_newtalk',
1583 array( $field => $id ),
1584 __METHOD__,
1585 'IGNORE' );
1586 if ( $dbw->affectedRows() ) {
1587 wfDebug( __METHOD__.": set on ($field, $id)\n" );
1588 return true;
1589 } else {
1590 wfDebug( __METHOD__." already set ($field, $id)\n" );
1591 return false;
1592 }
1593 }
1594
1595 /**
1596 * Clear the new messages flag for the given user
1597 * @param $field \string 'user_ip' for anonymous users, 'user_id' otherwise
1598 * @param $id \types{\string,\int} User's IP address for anonymous users, User ID otherwise
1599 * @return \bool True if successful, false otherwise
1600 * @private
1601 */
1602 function deleteNewtalk( $field, $id ) {
1603 $dbw = wfGetDB( DB_MASTER );
1604 $dbw->delete( 'user_newtalk',
1605 array( $field => $id ),
1606 __METHOD__ );
1607 if ( $dbw->affectedRows() ) {
1608 wfDebug( __METHOD__.": killed on ($field, $id)\n" );
1609 return true;
1610 } else {
1611 wfDebug( __METHOD__.": already gone ($field, $id)\n" );
1612 return false;
1613 }
1614 }
1615
1616 /**
1617 * Update the 'You have new messages!' status.
1618 * @param $val \bool Whether the user has new messages
1619 */
1620 function setNewtalk( $val ) {
1621 if( wfReadOnly() ) {
1622 return;
1623 }
1624
1625 $this->load();
1626 $this->mNewtalk = $val;
1627
1628 if( $this->isAnon() ) {
1629 $field = 'user_ip';
1630 $id = $this->getName();
1631 } else {
1632 $field = 'user_id';
1633 $id = $this->getId();
1634 }
1635 global $wgMemc;
1636
1637 if( $val ) {
1638 $changed = $this->updateNewtalk( $field, $id );
1639 } else {
1640 $changed = $this->deleteNewtalk( $field, $id );
1641 }
1642
1643 if( $this->isAnon() ) {
1644 // Anons have a separate memcached space, since
1645 // user records aren't kept for them.
1646 $key = wfMemcKey( 'newtalk', 'ip', $id );
1647 $wgMemc->set( $key, $val ? 1 : 0, 1800 );
1648 }
1649 if ( $changed ) {
1650 $this->invalidateCache();
1651 }
1652 }
1653
1654 /**
1655 * Generate a current or new-future timestamp to be stored in the
1656 * user_touched field when we update things.
1657 * @return \string Timestamp in TS_MW format
1658 */
1659 private static function newTouchedTimestamp() {
1660 global $wgClockSkewFudge;
1661 return wfTimestamp( TS_MW, time() + $wgClockSkewFudge );
1662 }
1663
1664 /**
1665 * Clear user data from memcached.
1666 * Use after applying fun updates to the database; caller's
1667 * responsibility to update user_touched if appropriate.
1668 *
1669 * Called implicitly from invalidateCache() and saveSettings().
1670 */
1671 private function clearSharedCache() {
1672 $this->load();
1673 if( $this->mId ) {
1674 global $wgMemc;
1675 $wgMemc->delete( wfMemcKey( 'user', 'id', $this->mId ) );
1676 }
1677 }
1678
1679 /**
1680 * Immediately touch the user data cache for this account.
1681 * Updates user_touched field, and removes account data from memcached
1682 * for reload on the next hit.
1683 */
1684 function invalidateCache() {
1685 if( wfReadOnly() ) {
1686 return;
1687 }
1688 $this->load();
1689 if( $this->mId ) {
1690 $this->mTouched = self::newTouchedTimestamp();
1691
1692 $dbw = wfGetDB( DB_MASTER );
1693 $dbw->update( 'user',
1694 array( 'user_touched' => $dbw->timestamp( $this->mTouched ) ),
1695 array( 'user_id' => $this->mId ),
1696 __METHOD__ );
1697
1698 $this->clearSharedCache();
1699 }
1700 }
1701
1702 /**
1703 * Validate the cache for this account.
1704 * @param $timestamp \string A timestamp in TS_MW format
1705 */
1706 function validateCache( $timestamp ) {
1707 $this->load();
1708 return ($timestamp >= $this->mTouched);
1709 }
1710
1711 /**
1712 * Get the user touched timestamp
1713 */
1714 function getTouched() {
1715 $this->load();
1716 return $this->mTouched;
1717 }
1718
1719 /**
1720 * Set the password and reset the random token.
1721 * Calls through to authentication plugin if necessary;
1722 * will have no effect if the auth plugin refuses to
1723 * pass the change through or if the legal password
1724 * checks fail.
1725 *
1726 * As a special case, setting the password to null
1727 * wipes it, so the account cannot be logged in until
1728 * a new password is set, for instance via e-mail.
1729 *
1730 * @param $str \string New password to set
1731 * @throws PasswordError on failure
1732 */
1733 function setPassword( $str ) {
1734 global $wgAuth;
1735
1736 if( $str !== null ) {
1737 if( !$wgAuth->allowPasswordChange() ) {
1738 throw new PasswordError( wfMsg( 'password-change-forbidden' ) );
1739 }
1740
1741 $valid = $this->isValidPassword( $str );
1742 if( $valid !== true ) {
1743 global $wgMinimalPasswordLength;
1744 throw new PasswordError( wfMsgExt( $valid, array( 'parsemag' ),
1745 $wgMinimalPasswordLength ) );
1746 }
1747 }
1748
1749 if( !$wgAuth->setPassword( $this, $str ) ) {
1750 throw new PasswordError( wfMsg( 'externaldberror' ) );
1751 }
1752
1753 $this->setInternalPassword( $str );
1754
1755 return true;
1756 }
1757
1758 /**
1759 * Set the password and reset the random token unconditionally.
1760 *
1761 * @param $str \string New password to set
1762 */
1763 function setInternalPassword( $str ) {
1764 $this->load();
1765 $this->setToken();
1766
1767 if( $str === null ) {
1768 // Save an invalid hash...
1769 $this->mPassword = '';
1770 } else {
1771 $this->mPassword = self::crypt( $str );
1772 }
1773 $this->mNewpassword = '';
1774 $this->mNewpassTime = null;
1775 }
1776
1777 /**
1778 * Get the user's current token.
1779 * @return \string Token
1780 */
1781 function getToken() {
1782 $this->load();
1783 return $this->mToken;
1784 }
1785
1786 /**
1787 * Set the random token (used for persistent authentication)
1788 * Called from loadDefaults() among other places.
1789 *
1790 * @param $token \string If specified, set the token to this value
1791 * @private
1792 */
1793 function setToken( $token = false ) {
1794 global $wgSecretKey, $wgProxyKey;
1795 $this->load();
1796 if ( !$token ) {
1797 if ( $wgSecretKey ) {
1798 $key = $wgSecretKey;
1799 } elseif ( $wgProxyKey ) {
1800 $key = $wgProxyKey;
1801 } else {
1802 $key = microtime();
1803 }
1804 $this->mToken = md5( $key . mt_rand( 0, 0x7fffffff ) . wfWikiID() . $this->mId );
1805 } else {
1806 $this->mToken = $token;
1807 }
1808 }
1809
1810 /**
1811 * Set the cookie password
1812 *
1813 * @param $str \string New cookie password
1814 * @private
1815 */
1816 function setCookiePassword( $str ) {
1817 $this->load();
1818 $this->mCookiePassword = md5( $str );
1819 }
1820
1821 /**
1822 * Set the password for a password reminder or new account email
1823 *
1824 * @param $str \string New password to set
1825 * @param $throttle \bool If true, reset the throttle timestamp to the present
1826 */
1827 function setNewpassword( $str, $throttle = true ) {
1828 $this->load();
1829 $this->mNewpassword = self::crypt( $str );
1830 if ( $throttle ) {
1831 $this->mNewpassTime = wfTimestampNow();
1832 }
1833 }
1834
1835 /**
1836 * Has password reminder email been sent within the last
1837 * $wgPasswordReminderResendTime hours?
1838 * @return \bool True or false
1839 */
1840 function isPasswordReminderThrottled() {
1841 global $wgPasswordReminderResendTime;
1842 $this->load();
1843 if ( !$this->mNewpassTime || !$wgPasswordReminderResendTime ) {
1844 return false;
1845 }
1846 $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgPasswordReminderResendTime * 3600;
1847 return time() < $expiry;
1848 }
1849
1850 /**
1851 * Get the user's e-mail address
1852 * @return \string User's email address
1853 */
1854 function getEmail() {
1855 $this->load();
1856 wfRunHooks( 'UserGetEmail', array( $this, &$this->mEmail ) );
1857 return $this->mEmail;
1858 }
1859
1860 /**
1861 * Get the timestamp of the user's e-mail authentication
1862 * @return \string TS_MW timestamp
1863 */
1864 function getEmailAuthenticationTimestamp() {
1865 $this->load();
1866 wfRunHooks( 'UserGetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
1867 return $this->mEmailAuthenticated;
1868 }
1869
1870 /**
1871 * Set the user's e-mail address
1872 * @param $str \string New e-mail address
1873 */
1874 function setEmail( $str ) {
1875 $this->load();
1876 $this->mEmail = $str;
1877 wfRunHooks( 'UserSetEmail', array( $this, &$this->mEmail ) );
1878 }
1879
1880 /**
1881 * Get the user's real name
1882 * @return \string User's real name
1883 */
1884 function getRealName() {
1885 $this->load();
1886 return $this->mRealName;
1887 }
1888
1889 /**
1890 * Set the user's real name
1891 * @param $str \string New real name
1892 */
1893 function setRealName( $str ) {
1894 $this->load();
1895 $this->mRealName = $str;
1896 }
1897
1898 /**
1899 * Get the user's current setting for a given option.
1900 *
1901 * @param $oname \string The option to check
1902 * @param $defaultOverride \string A default value returned if the option does not exist
1903 * @return \string User's current value for the option
1904 * @see getBoolOption()
1905 * @see getIntOption()
1906 */
1907 function getOption( $oname, $defaultOverride = null ) {
1908 $this->loadOptions();
1909
1910 if ( is_null( $this->mOptions ) ) {
1911 if($defaultOverride != '') {
1912 return $defaultOverride;
1913 }
1914 $this->mOptions = User::getDefaultOptions();
1915 }
1916
1917 if ( array_key_exists( $oname, $this->mOptions ) ) {
1918 return $this->mOptions[$oname];
1919 } else {
1920 return $defaultOverride;
1921 }
1922 }
1923
1924 /**
1925 * Get the user's current setting for a given option, as a boolean value.
1926 *
1927 * @param $oname \string The option to check
1928 * @return \bool User's current value for the option
1929 * @see getOption()
1930 */
1931 function getBoolOption( $oname ) {
1932 return (bool)$this->getOption( $oname );
1933 }
1934
1935
1936 /**
1937 * Get the user's current setting for a given option, as a boolean value.
1938 *
1939 * @param $oname \string The option to check
1940 * @param $defaultOverride \int A default value returned if the option does not exist
1941 * @return \int User's current value for the option
1942 * @see getOption()
1943 */
1944 function getIntOption( $oname, $defaultOverride=0 ) {
1945 $val = $this->getOption( $oname );
1946 if( $val == '' ) {
1947 $val = $defaultOverride;
1948 }
1949 return intval( $val );
1950 }
1951
1952 /**
1953 * Set the given option for a user.
1954 *
1955 * @param $oname \string The option to set
1956 * @param $val \mixed New value to set
1957 */
1958 function setOption( $oname, $val ) {
1959 $this->load();
1960 $this->loadOptions();
1961
1962 if ( $oname == 'skin' ) {
1963 # Clear cached skin, so the new one displays immediately in Special:Preferences
1964 unset( $this->mSkin );
1965 }
1966
1967 // Explicitly NULL values should refer to defaults
1968 global $wgDefaultUserOptions;
1969 if( is_null($val) && isset($wgDefaultUserOptions[$oname]) ) {
1970 $val = $wgDefaultUserOptions[$oname];
1971 }
1972
1973 $this->mOptions[$oname] = $val;
1974 }
1975
1976 /**
1977 * Reset all options to the site defaults
1978 */
1979 function resetOptions() {
1980 $this->mOptions = User::getDefaultOptions();
1981 }
1982
1983 /**
1984 * Get the user's preferred date format.
1985 * @return \string User's preferred date format
1986 */
1987 function getDatePreference() {
1988 // Important migration for old data rows
1989 if ( is_null( $this->mDatePreference ) ) {
1990 global $wgLang;
1991 $value = $this->getOption( 'date' );
1992 $map = $wgLang->getDatePreferenceMigrationMap();
1993 if ( isset( $map[$value] ) ) {
1994 $value = $map[$value];
1995 }
1996 $this->mDatePreference = $value;
1997 }
1998 return $this->mDatePreference;
1999 }
2000
2001 /**
2002 * Get the permissions this user has.
2003 * @return \type{\arrayof{\string}} Array of permission names
2004 */
2005 function getRights() {
2006 if ( is_null( $this->mRights ) ) {
2007 $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() );
2008 wfRunHooks( 'UserGetRights', array( $this, &$this->mRights ) );
2009 // Force reindexation of rights when a hook has unset one of them
2010 $this->mRights = array_values( $this->mRights );
2011 }
2012 return $this->mRights;
2013 }
2014
2015 /**
2016 * Get the list of explicit group memberships this user has.
2017 * The implicit * and user groups are not included.
2018 * @return \type{\arrayof{\string}} Array of internal group names
2019 */
2020 function getGroups() {
2021 $this->load();
2022 return $this->mGroups;
2023 }
2024
2025 /**
2026 * Get the list of implicit group memberships this user has.
2027 * This includes all explicit groups, plus 'user' if logged in,
2028 * '*' for all accounts and autopromoted groups
2029 * @param $recache \bool Whether to avoid the cache
2030 * @return \type{\arrayof{\string}} Array of internal group names
2031 */
2032 function getEffectiveGroups( $recache = false ) {
2033 if ( $recache || is_null( $this->mEffectiveGroups ) ) {
2034 $this->mEffectiveGroups = $this->getGroups();
2035 $this->mEffectiveGroups[] = '*';
2036 if( $this->getId() ) {
2037 $this->mEffectiveGroups[] = 'user';
2038
2039 $this->mEffectiveGroups = array_unique( array_merge(
2040 $this->mEffectiveGroups,
2041 Autopromote::getAutopromoteGroups( $this )
2042 ) );
2043
2044 # Hook for additional groups
2045 wfRunHooks( 'UserEffectiveGroups', array( &$this, &$this->mEffectiveGroups ) );
2046 }
2047 }
2048 return $this->mEffectiveGroups;
2049 }
2050
2051 /**
2052 * Get the user's edit count.
2053 * @return \int User'e edit count
2054 */
2055 function getEditCount() {
2056 if ($this->getId()) {
2057 if ( !isset( $this->mEditCount ) ) {
2058 /* Populate the count, if it has not been populated yet */
2059 $this->mEditCount = User::edits($this->mId);
2060 }
2061 return $this->mEditCount;
2062 } else {
2063 /* nil */
2064 return null;
2065 }
2066 }
2067
2068 /**
2069 * Add the user to the given group.
2070 * This takes immediate effect.
2071 * @param $group \string Name of the group to add
2072 */
2073 function addGroup( $group ) {
2074 $dbw = wfGetDB( DB_MASTER );
2075 if( $this->getId() ) {
2076 $dbw->insert( 'user_groups',
2077 array(
2078 'ug_user' => $this->getID(),
2079 'ug_group' => $group,
2080 ),
2081 'User::addGroup',
2082 array( 'IGNORE' ) );
2083 }
2084
2085 $this->loadGroups();
2086 $this->mGroups[] = $group;
2087 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
2088
2089 $this->invalidateCache();
2090 }
2091
2092 /**
2093 * Remove the user from the given group.
2094 * This takes immediate effect.
2095 * @param $group \string Name of the group to remove
2096 */
2097 function removeGroup( $group ) {
2098 $this->load();
2099 $dbw = wfGetDB( DB_MASTER );
2100 $dbw->delete( 'user_groups',
2101 array(
2102 'ug_user' => $this->getID(),
2103 'ug_group' => $group,
2104 ),
2105 'User::removeGroup' );
2106
2107 $this->loadGroups();
2108 $this->mGroups = array_diff( $this->mGroups, array( $group ) );
2109 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
2110
2111 $this->invalidateCache();
2112 }
2113
2114
2115 /**
2116 * Get whether the user is logged in
2117 * @return \bool True or false
2118 */
2119 function isLoggedIn() {
2120 return $this->getID() != 0;
2121 }
2122
2123 /**
2124 * Get whether the user is anonymous
2125 * @return \bool True or false
2126 */
2127 function isAnon() {
2128 return !$this->isLoggedIn();
2129 }
2130
2131 /**
2132 * Get whether the user is a bot
2133 * @return \bool True or false
2134 * @deprecated
2135 */
2136 function isBot() {
2137 wfDeprecated( __METHOD__ );
2138 return $this->isAllowed( 'bot' );
2139 }
2140
2141 /**
2142 * Check if user is allowed to access a feature / make an action
2143 * @param $action \string action to be checked
2144 * @return \bool True if action is allowed, else false
2145 */
2146 function isAllowed( $action = '' ) {
2147 if ( $action === '' )
2148 return true; // In the spirit of DWIM
2149 # Patrolling may not be enabled
2150 if( $action === 'patrol' || $action === 'autopatrol' ) {
2151 global $wgUseRCPatrol, $wgUseNPPatrol;
2152 if( !$wgUseRCPatrol && !$wgUseNPPatrol )
2153 return false;
2154 }
2155 # Use strict parameter to avoid matching numeric 0 accidentally inserted
2156 # by misconfiguration: 0 == 'foo'
2157 return in_array( $action, $this->getRights(), true )
2158 || in_array( 'root', $this->getRights(), true );
2159 }
2160
2161 /**
2162 * Check whether to enable recent changes patrol features for this user
2163 * @return \bool True or false
2164 */
2165 public function useRCPatrol() {
2166 global $wgUseRCPatrol;
2167 return( $wgUseRCPatrol && ($this->isAllowed('patrol') || $this->isAllowed('patrolmarks')) );
2168 }
2169
2170 /**
2171 * Check whether to enable new pages patrol features for this user
2172 * @return \bool True or false
2173 */
2174 public function useNPPatrol() {
2175 global $wgUseRCPatrol, $wgUseNPPatrol;
2176 return( ($wgUseRCPatrol || $wgUseNPPatrol) && ($this->isAllowed('patrol') || $this->isAllowed('patrolmarks')) );
2177 }
2178
2179 /**
2180 * Get the current skin, loading it if required, and setting a title
2181 * @param $t Title: the title to use in the skin
2182 * @return Skin The current skin
2183 * @todo FIXME : need to check the old failback system [AV]
2184 */
2185 function &getSkin( $t = null ) {
2186 if ( ! isset( $this->mSkin ) ) {
2187 wfProfileIn( __METHOD__ );
2188
2189 global $wgHiddenPrefs;
2190 if( !in_array( 'skin', $wgHiddenPrefs ) ) {
2191 # get the user skin
2192 global $wgRequest;
2193 $userSkin = $this->getOption( 'skin' );
2194 $userSkin = $wgRequest->getVal('useskin', $userSkin);
2195 } else {
2196 # if we're not allowing users to override, then use the default
2197 global $wgDefaultSkin;
2198 $userSkin = $wgDefaultSkin;
2199 }
2200
2201 $this->mSkin =& Skin::newFromKey( $userSkin );
2202 wfProfileOut( __METHOD__ );
2203 }
2204 if( $t || !$this->mSkin->getTitle() ) {
2205 if ( !$t ) {
2206 global $wgOut;
2207 $t = $wgOut->getTitle();
2208 }
2209 $this->mSkin->setTitle( $t );
2210 }
2211 return $this->mSkin;
2212 }
2213
2214 /**
2215 * Check the watched status of an article.
2216 * @param $title \type{Title} Title of the article to look at
2217 * @return \bool True if article is watched
2218 */
2219 function isWatched( $title ) {
2220 $wl = WatchedItem::fromUserTitle( $this, $title );
2221 return $wl->isWatched();
2222 }
2223
2224 /**
2225 * Watch an article.
2226 * @param $title \type{Title} Title of the article to look at
2227 */
2228 function addWatch( $title ) {
2229 $wl = WatchedItem::fromUserTitle( $this, $title );
2230 $wl->addWatch();
2231 $this->invalidateCache();
2232 }
2233
2234 /**
2235 * Stop watching an article.
2236 * @param $title \type{Title} Title of the article to look at
2237 */
2238 function removeWatch( $title ) {
2239 $wl = WatchedItem::fromUserTitle( $this, $title );
2240 $wl->removeWatch();
2241 $this->invalidateCache();
2242 }
2243
2244 /**
2245 * Clear the user's notification timestamp for the given title.
2246 * If e-notif e-mails are on, they will receive notification mails on
2247 * the next change of the page if it's watched etc.
2248 * @param $title \type{Title} Title of the article to look at
2249 */
2250 function clearNotification( &$title ) {
2251 global $wgUser, $wgUseEnotif, $wgShowUpdatedMarker;
2252
2253 # Do nothing if the database is locked to writes
2254 if( wfReadOnly() ) {
2255 return;
2256 }
2257
2258 if ($title->getNamespace() == NS_USER_TALK &&
2259 $title->getText() == $this->getName() ) {
2260 if (!wfRunHooks('UserClearNewTalkNotification', array(&$this)))
2261 return;
2262 $this->setNewtalk( false );
2263 }
2264
2265 if( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
2266 return;
2267 }
2268
2269 if( $this->isAnon() ) {
2270 // Nothing else to do...
2271 return;
2272 }
2273
2274 // Only update the timestamp if the page is being watched.
2275 // The query to find out if it is watched is cached both in memcached and per-invocation,
2276 // and when it does have to be executed, it can be on a slave
2277 // If this is the user's newtalk page, we always update the timestamp
2278 if ($title->getNamespace() == NS_USER_TALK &&
2279 $title->getText() == $wgUser->getName())
2280 {
2281 $watched = true;
2282 } elseif ( $this->getId() == $wgUser->getId() ) {
2283 $watched = $title->userIsWatching();
2284 } else {
2285 $watched = true;
2286 }
2287
2288 // If the page is watched by the user (or may be watched), update the timestamp on any
2289 // any matching rows
2290 if ( $watched ) {
2291 $dbw = wfGetDB( DB_MASTER );
2292 $dbw->update( 'watchlist',
2293 array( /* SET */
2294 'wl_notificationtimestamp' => NULL
2295 ), array( /* WHERE */
2296 'wl_title' => $title->getDBkey(),
2297 'wl_namespace' => $title->getNamespace(),
2298 'wl_user' => $this->getID()
2299 ), __METHOD__
2300 );
2301 }
2302 }
2303
2304 /**
2305 * Resets all of the given user's page-change notification timestamps.
2306 * If e-notif e-mails are on, they will receive notification mails on
2307 * the next change of any watched page.
2308 *
2309 * @param $currentUser \int User ID
2310 */
2311 function clearAllNotifications( $currentUser ) {
2312 global $wgUseEnotif, $wgShowUpdatedMarker;
2313 if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
2314 $this->setNewtalk( false );
2315 return;
2316 }
2317 if( $currentUser != 0 ) {
2318 $dbw = wfGetDB( DB_MASTER );
2319 $dbw->update( 'watchlist',
2320 array( /* SET */
2321 'wl_notificationtimestamp' => NULL
2322 ), array( /* WHERE */
2323 'wl_user' => $currentUser
2324 ), __METHOD__
2325 );
2326 # We also need to clear here the "you have new message" notification for the own user_talk page
2327 # This is cleared one page view later in Article::viewUpdates();
2328 }
2329 }
2330
2331 /**
2332 * Set this user's options from an encoded string
2333 * @param $str \string Encoded options to import
2334 * @private
2335 */
2336 function decodeOptions( $str ) {
2337 if (!$str)
2338 return;
2339
2340 $this->mOptionsLoaded = true;
2341 $this->mOptionOverrides = array();
2342
2343 $this->mOptions = array();
2344 $a = explode( "\n", $str );
2345 foreach ( $a as $s ) {
2346 $m = array();
2347 if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
2348 $this->mOptions[$m[1]] = $m[2];
2349 $this->mOptionOverrides[$m[1]] = $m[2];
2350 }
2351 }
2352 }
2353
2354 /**
2355 * Set a cookie on the user's client. Wrapper for
2356 * WebResponse::setCookie
2357 * @param $name \string Name of the cookie to set
2358 * @param $value \string Value to set
2359 * @param $exp \int Expiration time, as a UNIX time value;
2360 * if 0 or not specified, use the default $wgCookieExpiration
2361 */
2362 protected function setCookie( $name, $value, $exp=0 ) {
2363 global $wgRequest;
2364 $wgRequest->response()->setcookie( $name, $value, $exp );
2365 }
2366
2367 /**
2368 * Clear a cookie on the user's client
2369 * @param $name \string Name of the cookie to clear
2370 */
2371 protected function clearCookie( $name ) {
2372 $this->setCookie( $name, '', time() - 86400 );
2373 }
2374
2375 /**
2376 * Set the default cookies for this session on the user's client.
2377 */
2378 function setCookies() {
2379 $this->load();
2380 if ( 0 == $this->mId ) return;
2381 $session = array(
2382 'wsUserID' => $this->mId,
2383 'wsToken' => $this->mToken,
2384 'wsUserName' => $this->getName()
2385 );
2386 $cookies = array(
2387 'UserID' => $this->mId,
2388 'UserName' => $this->getName(),
2389 );
2390 if ( 1 == $this->getOption( 'rememberpassword' ) ) {
2391 $cookies['Token'] = $this->mToken;
2392 } else {
2393 $cookies['Token'] = false;
2394 }
2395
2396 wfRunHooks( 'UserSetCookies', array( $this, &$session, &$cookies ) );
2397 #check for null, since the hook could cause a null value
2398 if ( !is_null( $session ) && isset( $_SESSION ) ){
2399 $_SESSION = $session + $_SESSION;
2400 }
2401 foreach ( $cookies as $name => $value ) {
2402 if ( $value === false ) {
2403 $this->clearCookie( $name );
2404 } else {
2405 $this->setCookie( $name, $value );
2406 }
2407 }
2408 }
2409
2410 /**
2411 * Log this user out.
2412 */
2413 function logout() {
2414 if( wfRunHooks( 'UserLogout', array(&$this) ) ) {
2415 $this->doLogout();
2416 }
2417 }
2418
2419 /**
2420 * Clear the user's cookies and session, and reset the instance cache.
2421 * @private
2422 * @see logout()
2423 */
2424 function doLogout() {
2425 $this->clearInstanceCache( 'defaults' );
2426
2427 $_SESSION['wsUserID'] = 0;
2428
2429 $this->clearCookie( 'UserID' );
2430 $this->clearCookie( 'Token' );
2431
2432 # Remember when user logged out, to prevent seeing cached pages
2433 $this->setCookie( 'LoggedOut', wfTimestampNow(), time() + 86400 );
2434 }
2435
2436 /**
2437 * Save this user's settings into the database.
2438 * @todo Only rarely do all these fields need to be set!
2439 */
2440 function saveSettings() {
2441 $this->load();
2442 if ( wfReadOnly() ) { return; }
2443 if ( 0 == $this->mId ) { return; }
2444
2445 $this->mTouched = self::newTouchedTimestamp();
2446
2447 $dbw = wfGetDB( DB_MASTER );
2448 $dbw->update( 'user',
2449 array( /* SET */
2450 'user_name' => $this->mName,
2451 'user_password' => $this->mPassword,
2452 'user_newpassword' => $this->mNewpassword,
2453 'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ),
2454 'user_real_name' => $this->mRealName,
2455 'user_email' => $this->mEmail,
2456 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
2457 'user_options' => '',
2458 'user_touched' => $dbw->timestamp($this->mTouched),
2459 'user_token' => $this->mToken,
2460 'user_email_token' => $this->mEmailToken,
2461 'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
2462 ), array( /* WHERE */
2463 'user_id' => $this->mId
2464 ), __METHOD__
2465 );
2466
2467 $this->saveOptions();
2468
2469 wfRunHooks( 'UserSaveSettings', array( $this ) );
2470 $this->clearSharedCache();
2471 $this->getUserPage()->invalidateCache();
2472 }
2473
2474 /**
2475 * If only this user's username is known, and it exists, return the user ID.
2476 */
2477 function idForName() {
2478 $s = trim( $this->getName() );
2479 if ( $s === '' ) return 0;
2480
2481 $dbr = wfGetDB( DB_SLAVE );
2482 $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), __METHOD__ );
2483 if ( $id === false ) {
2484 $id = 0;
2485 }
2486 return $id;
2487 }
2488
2489 /**
2490 * Add a user to the database, return the user object
2491 *
2492 * @param $name \string Username to add
2493 * @param $params \type{\arrayof{\string}} Non-default parameters to save to the database:
2494 * - password The user's password. Password logins will be disabled if this is omitted.
2495 * - newpassword A temporary password mailed to the user
2496 * - email The user's email address
2497 * - email_authenticated The email authentication timestamp
2498 * - real_name The user's real name
2499 * - options An associative array of non-default options
2500 * - token Random authentication token. Do not set.
2501 * - registration Registration timestamp. Do not set.
2502 *
2503 * @return \type{User} A new User object, or null if the username already exists
2504 */
2505 static function createNew( $name, $params = array() ) {
2506 $user = new User;
2507 $user->load();
2508 if ( isset( $params['options'] ) ) {
2509 $user->mOptions = $params['options'] + (array)$user->mOptions;
2510 unset( $params['options'] );
2511 }
2512 $dbw = wfGetDB( DB_MASTER );
2513 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
2514 $fields = array(
2515 'user_id' => $seqVal,
2516 'user_name' => $name,
2517 'user_password' => $user->mPassword,
2518 'user_newpassword' => $user->mNewpassword,
2519 'user_newpass_time' => $dbw->timestamp( $user->mNewpassTime ),
2520 'user_email' => $user->mEmail,
2521 'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ),
2522 'user_real_name' => $user->mRealName,
2523 'user_options' => '',
2524 'user_token' => $user->mToken,
2525 'user_registration' => $dbw->timestamp( $user->mRegistration ),
2526 'user_editcount' => 0,
2527 );
2528 foreach ( $params as $name => $value ) {
2529 $fields["user_$name"] = $value;
2530 }
2531 $dbw->insert( 'user', $fields, __METHOD__, array( 'IGNORE' ) );
2532 if ( $dbw->affectedRows() ) {
2533 $newUser = User::newFromId( $dbw->insertId() );
2534 } else {
2535 $newUser = null;
2536 }
2537 return $newUser;
2538 }
2539
2540 /**
2541 * Add this existing user object to the database
2542 */
2543 function addToDatabase() {
2544 $this->load();
2545 $dbw = wfGetDB( DB_MASTER );
2546 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
2547 $dbw->insert( 'user',
2548 array(
2549 'user_id' => $seqVal,
2550 'user_name' => $this->mName,
2551 'user_password' => $this->mPassword,
2552 'user_newpassword' => $this->mNewpassword,
2553 'user_newpass_time' => $dbw->timestamp( $this->mNewpassTime ),
2554 'user_email' => $this->mEmail,
2555 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
2556 'user_real_name' => $this->mRealName,
2557 'user_options' => '',
2558 'user_token' => $this->mToken,
2559 'user_registration' => $dbw->timestamp( $this->mRegistration ),
2560 'user_editcount' => 0,
2561 ), __METHOD__
2562 );
2563 $this->mId = $dbw->insertId();
2564
2565 // Clear instance cache other than user table data, which is already accurate
2566 $this->clearInstanceCache();
2567
2568 $this->saveOptions();
2569 }
2570
2571 /**
2572 * If this (non-anonymous) user is blocked, block any IP address
2573 * they've successfully logged in from.
2574 */
2575 function spreadBlock() {
2576 wfDebug( __METHOD__."()\n" );
2577 $this->load();
2578 if ( $this->mId == 0 ) {
2579 return;
2580 }
2581
2582 $userblock = Block::newFromDB( '', $this->mId );
2583 if ( !$userblock ) {
2584 return;
2585 }
2586
2587 $userblock->doAutoblock( wfGetIp() );
2588
2589 }
2590
2591 /**
2592 * Generate a string which will be different for any combination of
2593 * user options which would produce different parser output.
2594 * This will be used as part of the hash key for the parser cache,
2595 * so users will the same options can share the same cached data
2596 * safely.
2597 *
2598 * Extensions which require it should install 'PageRenderingHash' hook,
2599 * which will give them a chance to modify this key based on their own
2600 * settings.
2601 *
2602 * @return \string Page rendering hash
2603 */
2604 function getPageRenderingHash() {
2605 global $wgUseDynamicDates, $wgRenderHashAppend, $wgLang, $wgContLang;
2606 if( $this->mHash ){
2607 return $this->mHash;
2608 }
2609
2610 // stubthreshold is only included below for completeness,
2611 // it will always be 0 when this function is called by parsercache.
2612
2613 $confstr = $this->getOption( 'math' );
2614 $confstr .= '!' . $this->getOption( 'stubthreshold' );
2615 if ( $wgUseDynamicDates ) {
2616 $confstr .= '!' . $this->getDatePreference();
2617 }
2618 $confstr .= '!' . ($this->getOption( 'numberheadings' ) ? '1' : '');
2619 $confstr .= '!' . $wgLang->getCode();
2620 $confstr .= '!' . $this->getOption( 'thumbsize' );
2621 // add in language specific options, if any
2622 $extra = $wgContLang->getExtraHashOptions();
2623 $confstr .= $extra;
2624
2625 $confstr .= $wgRenderHashAppend;
2626
2627 // Give a chance for extensions to modify the hash, if they have
2628 // extra options or other effects on the parser cache.
2629 wfRunHooks( 'PageRenderingHash', array( &$confstr ) );
2630
2631 // Make it a valid memcached key fragment
2632 $confstr = str_replace( ' ', '_', $confstr );
2633 $this->mHash = $confstr;
2634 return $confstr;
2635 }
2636
2637 /**
2638 * Get whether the user is explicitly blocked from account creation.
2639 * @return \bool True if blocked
2640 */
2641 function isBlockedFromCreateAccount() {
2642 $this->getBlockedStatus();
2643 return $this->mBlock && $this->mBlock->mCreateAccount;
2644 }
2645
2646 /**
2647 * Get whether the user is blocked from using Special:Emailuser.
2648 * @return \bool True if blocked
2649 */
2650 function isBlockedFromEmailuser() {
2651 $this->getBlockedStatus();
2652 return $this->mBlock && $this->mBlock->mBlockEmail;
2653 }
2654
2655 /**
2656 * Get whether the user is allowed to create an account.
2657 * @return \bool True if allowed
2658 */
2659 function isAllowedToCreateAccount() {
2660 return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount();
2661 }
2662
2663 /**
2664 * @deprecated
2665 */
2666 function setLoaded( $loaded ) {
2667 wfDeprecated( __METHOD__ );
2668 }
2669
2670 /**
2671 * Get this user's personal page title.
2672 *
2673 * @return \type{Title} User's personal page title
2674 */
2675 function getUserPage() {
2676 return Title::makeTitle( NS_USER, $this->getName() );
2677 }
2678
2679 /**
2680 * Get this user's talk page title.
2681 *
2682 * @return \type{Title} User's talk page title
2683 */
2684 function getTalkPage() {
2685 $title = $this->getUserPage();
2686 return $title->getTalkPage();
2687 }
2688
2689 /**
2690 * Get the maximum valid user ID.
2691 * @return \int User ID
2692 * @static
2693 */
2694 function getMaxID() {
2695 static $res; // cache
2696
2697 if ( isset( $res ) )
2698 return $res;
2699 else {
2700 $dbr = wfGetDB( DB_SLAVE );
2701 return $res = $dbr->selectField( 'user', 'max(user_id)', false, 'User::getMaxID' );
2702 }
2703 }
2704
2705 /**
2706 * Determine whether the user is a newbie. Newbies are either
2707 * anonymous IPs, or the most recently created accounts.
2708 * @return \bool True if the user is a newbie
2709 */
2710 function isNewbie() {
2711 return !$this->isAllowed( 'autoconfirmed' );
2712 }
2713
2714 /**
2715 * Check to see if the given clear-text password is one of the accepted passwords
2716 * @param $password \string user password.
2717 * @return \bool True if the given password is correct, otherwise False.
2718 */
2719 function checkPassword( $password ) {
2720 global $wgAuth;
2721 $this->load();
2722
2723 // Even though we stop people from creating passwords that
2724 // are shorter than this, doesn't mean people wont be able
2725 // to. Certain authentication plugins do NOT want to save
2726 // domain passwords in a mysql database, so we should
2727 // check this (incase $wgAuth->strict() is false).
2728 if( $this->isValidPassword( $password ) !== true ) {
2729 return false;
2730 }
2731
2732 if( $wgAuth->authenticate( $this->getName(), $password ) ) {
2733 return true;
2734 } elseif( $wgAuth->strict() ) {
2735 /* Auth plugin doesn't allow local authentication */
2736 return false;
2737 } elseif( $wgAuth->strictUserAuth( $this->getName() ) ) {
2738 /* Auth plugin doesn't allow local authentication for this user name */
2739 return false;
2740 }
2741 if ( self::comparePasswords( $this->mPassword, $password, $this->mId ) ) {
2742 return true;
2743 } elseif ( function_exists( 'iconv' ) ) {
2744 # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
2745 # Check for this with iconv
2746 $cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password );
2747 if ( self::comparePasswords( $this->mPassword, $cp1252Password, $this->mId ) ) {
2748 return true;
2749 }
2750 }
2751 return false;
2752 }
2753
2754 /**
2755 * Check if the given clear-text password matches the temporary password
2756 * sent by e-mail for password reset operations.
2757 * @return \bool True if matches, false otherwise
2758 */
2759 function checkTemporaryPassword( $plaintext ) {
2760 global $wgNewPasswordExpiry;
2761 if( self::comparePasswords( $this->mNewpassword, $plaintext, $this->getId() ) ) {
2762 $this->load();
2763 $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgNewPasswordExpiry;
2764 return ( time() < $expiry );
2765 } else {
2766 return false;
2767 }
2768 }
2769
2770 /**
2771 * Initialize (if necessary) and return a session token value
2772 * which can be used in edit forms to show that the user's
2773 * login credentials aren't being hijacked with a foreign form
2774 * submission.
2775 *
2776 * @param $salt \types{\string,\arrayof{\string}} Optional function-specific data for hashing
2777 * @return \string The new edit token
2778 */
2779 function editToken( $salt = '' ) {
2780 if ( $this->isAnon() ) {
2781 return EDIT_TOKEN_SUFFIX;
2782 } else {
2783 if( !isset( $_SESSION['wsEditToken'] ) ) {
2784 $token = $this->generateToken();
2785 $_SESSION['wsEditToken'] = $token;
2786 } else {
2787 $token = $_SESSION['wsEditToken'];
2788 }
2789 if( is_array( $salt ) ) {
2790 $salt = implode( '|', $salt );
2791 }
2792 return md5( $token . $salt ) . EDIT_TOKEN_SUFFIX;
2793 }
2794 }
2795
2796 /**
2797 * Generate a looking random token for various uses.
2798 *
2799 * @param $salt \string Optional salt value
2800 * @return \string The new random token
2801 */
2802 function generateToken( $salt = '' ) {
2803 $token = dechex( mt_rand() ) . dechex( mt_rand() );
2804 return md5( $token . $salt );
2805 }
2806
2807 /**
2808 * Check given value against the token value stored in the session.
2809 * A match should confirm that the form was submitted from the
2810 * user's own login session, not a form submission from a third-party
2811 * site.
2812 *
2813 * @param $val \string Input value to compare
2814 * @param $salt \string Optional function-specific data for hashing
2815 * @return \bool Whether the token matches
2816 */
2817 function matchEditToken( $val, $salt = '' ) {
2818 $sessionToken = $this->editToken( $salt );
2819 if ( $val != $sessionToken ) {
2820 wfDebug( "User::matchEditToken: broken session data\n" );
2821 }
2822 return $val == $sessionToken;
2823 }
2824
2825 /**
2826 * Check given value against the token value stored in the session,
2827 * ignoring the suffix.
2828 *
2829 * @param $val \string Input value to compare
2830 * @param $salt \string Optional function-specific data for hashing
2831 * @return \bool Whether the token matches
2832 */
2833 function matchEditTokenNoSuffix( $val, $salt = '' ) {
2834 $sessionToken = $this->editToken( $salt );
2835 return substr( $sessionToken, 0, 32 ) == substr( $val, 0, 32 );
2836 }
2837
2838 /**
2839 * Generate a new e-mail confirmation token and send a confirmation/invalidation
2840 * mail to the user's given address.
2841 *
2842 * @return \types{\bool,\type{WikiError}} True on success, a WikiError object on failure.
2843 */
2844 function sendConfirmationMail() {
2845 global $wgLang;
2846 $expiration = null; // gets passed-by-ref and defined in next line.
2847 $token = $this->confirmationToken( $expiration );
2848 $url = $this->confirmationTokenUrl( $token );
2849 $invalidateURL = $this->invalidationTokenUrl( $token );
2850 $this->saveSettings();
2851
2852 return $this->sendMail( wfMsg( 'confirmemail_subject' ),
2853 wfMsg( 'confirmemail_body',
2854 wfGetIP(),
2855 $this->getName(),
2856 $url,
2857 $wgLang->timeanddate( $expiration, false ),
2858 $invalidateURL,
2859 $wgLang->date( $expiration, false ),
2860 $wgLang->time( $expiration, false ) ) );
2861 }
2862
2863 /**
2864 * Send an e-mail to this user's account. Does not check for
2865 * confirmed status or validity.
2866 *
2867 * @param $subject \string Message subject
2868 * @param $body \string Message body
2869 * @param $from \string Optional From address; if unspecified, default $wgPasswordSender will be used
2870 * @param $replyto \string Reply-To address
2871 * @return \types{\bool,\type{WikiError}} True on success, a WikiError object on failure
2872 */
2873 function sendMail( $subject, $body, $from = null, $replyto = null ) {
2874 if( is_null( $from ) ) {
2875 global $wgPasswordSender;
2876 $from = $wgPasswordSender;
2877 }
2878
2879 $to = new MailAddress( $this );
2880 $sender = new MailAddress( $from );
2881 return UserMailer::send( $to, $sender, $subject, $body, $replyto );
2882 }
2883
2884 /**
2885 * Generate, store, and return a new e-mail confirmation code.
2886 * A hash (unsalted, since it's used as a key) is stored.
2887 *
2888 * @note Call saveSettings() after calling this function to commit
2889 * this change to the database.
2890 *
2891 * @param[out] &$expiration \mixed Accepts the expiration time
2892 * @return \string New token
2893 * @private
2894 */
2895 function confirmationToken( &$expiration ) {
2896 $now = time();
2897 $expires = $now + 7 * 24 * 60 * 60;
2898 $expiration = wfTimestamp( TS_MW, $expires );
2899 $token = $this->generateToken( $this->mId . $this->mEmail . $expires );
2900 $hash = md5( $token );
2901 $this->load();
2902 $this->mEmailToken = $hash;
2903 $this->mEmailTokenExpires = $expiration;
2904 return $token;
2905 }
2906
2907 /**
2908 * Return a URL the user can use to confirm their email address.
2909 * @param $token \string Accepts the email confirmation token
2910 * @return \string New token URL
2911 * @private
2912 */
2913 function confirmationTokenUrl( $token ) {
2914 return $this->getTokenUrl( 'ConfirmEmail', $token );
2915 }
2916 /**
2917 * Return a URL the user can use to invalidate their email address.
2918 * @param $token \string Accepts the email confirmation token
2919 * @return \string New token URL
2920 * @private
2921 */
2922 function invalidationTokenUrl( $token ) {
2923 return $this->getTokenUrl( 'Invalidateemail', $token );
2924 }
2925
2926 /**
2927 * Internal function to format the e-mail validation/invalidation URLs.
2928 * This uses $wgArticlePath directly as a quickie hack to use the
2929 * hardcoded English names of the Special: pages, for ASCII safety.
2930 *
2931 * @note Since these URLs get dropped directly into emails, using the
2932 * short English names avoids insanely long URL-encoded links, which
2933 * also sometimes can get corrupted in some browsers/mailers
2934 * (bug 6957 with Gmail and Internet Explorer).
2935 *
2936 * @param $page \string Special page
2937 * @param $token \string Token
2938 * @return \string Formatted URL
2939 */
2940 protected function getTokenUrl( $page, $token ) {
2941 global $wgArticlePath;
2942 return wfExpandUrl(
2943 str_replace(
2944 '$1',
2945 "Special:$page/$token",
2946 $wgArticlePath ) );
2947 }
2948
2949 /**
2950 * Mark the e-mail address confirmed.
2951 *
2952 * @note Call saveSettings() after calling this function to commit the change.
2953 */
2954 function confirmEmail() {
2955 $this->setEmailAuthenticationTimestamp( wfTimestampNow() );
2956 return true;
2957 }
2958
2959 /**
2960 * Invalidate the user's e-mail confirmation, and unauthenticate the e-mail
2961 * address if it was already confirmed.
2962 *
2963 * @note Call saveSettings() after calling this function to commit the change.
2964 */
2965 function invalidateEmail() {
2966 $this->load();
2967 $this->mEmailToken = null;
2968 $this->mEmailTokenExpires = null;
2969 $this->setEmailAuthenticationTimestamp( null );
2970 return true;
2971 }
2972
2973 /**
2974 * Set the e-mail authentication timestamp.
2975 * @param $timestamp \string TS_MW timestamp
2976 */
2977 function setEmailAuthenticationTimestamp( $timestamp ) {
2978 $this->load();
2979 $this->mEmailAuthenticated = $timestamp;
2980 wfRunHooks( 'UserSetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
2981 }
2982
2983 /**
2984 * Is this user allowed to send e-mails within limits of current
2985 * site configuration?
2986 * @return \bool True if allowed
2987 */
2988 function canSendEmail() {
2989 global $wgEnableEmail, $wgEnableUserEmail, $wgUser;
2990 if( !$wgEnableEmail || !$wgEnableUserEmail || !$wgUser->isAllowed( 'sendemail' ) ) {
2991 return false;
2992 }
2993 $canSend = $this->isEmailConfirmed();
2994 wfRunHooks( 'UserCanSendEmail', array( &$this, &$canSend ) );
2995 return $canSend;
2996 }
2997
2998 /**
2999 * Is this user allowed to receive e-mails within limits of current
3000 * site configuration?
3001 * @return \bool True if allowed
3002 */
3003 function canReceiveEmail() {
3004 return $this->isEmailConfirmed() && !$this->getOption( 'disablemail' );
3005 }
3006
3007 /**
3008 * Is this user's e-mail address valid-looking and confirmed within
3009 * limits of the current site configuration?
3010 *
3011 * @note If $wgEmailAuthentication is on, this may require the user to have
3012 * confirmed their address by returning a code or using a password
3013 * sent to the address from the wiki.
3014 *
3015 * @return \bool True if confirmed
3016 */
3017 function isEmailConfirmed() {
3018 global $wgEmailAuthentication;
3019 $this->load();
3020 $confirmed = true;
3021 if( wfRunHooks( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) {
3022 if( $this->isAnon() )
3023 return false;
3024 if( !self::isValidEmailAddr( $this->mEmail ) )
3025 return false;
3026 if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() )
3027 return false;
3028 return true;
3029 } else {
3030 return $confirmed;
3031 }
3032 }
3033
3034 /**
3035 * Check whether there is an outstanding request for e-mail confirmation.
3036 * @return \bool True if pending
3037 */
3038 function isEmailConfirmationPending() {
3039 global $wgEmailAuthentication;
3040 return $wgEmailAuthentication &&
3041 !$this->isEmailConfirmed() &&
3042 $this->mEmailToken &&
3043 $this->mEmailTokenExpires > wfTimestamp();
3044 }
3045
3046 /**
3047 * Get the timestamp of account creation.
3048 *
3049 * @return \types{\string,\bool} string Timestamp of account creation, or false for
3050 * non-existent/anonymous user accounts.
3051 */
3052 public function getRegistration() {
3053 return $this->getId() > 0
3054 ? $this->mRegistration
3055 : false;
3056 }
3057
3058 /**
3059 * Get the timestamp of the first edit
3060 *
3061 * @return \types{\string,\bool} string Timestamp of first edit, or false for
3062 * non-existent/anonymous user accounts.
3063 */
3064 public function getFirstEditTimestamp() {
3065 if( $this->getId() == 0 ) return false; // anons
3066 $dbr = wfGetDB( DB_SLAVE );
3067 $time = $dbr->selectField( 'revision', 'rev_timestamp',
3068 array( 'rev_user' => $this->getId() ),
3069 __METHOD__,
3070 array( 'ORDER BY' => 'rev_timestamp ASC' )
3071 );
3072 if( !$time ) return false; // no edits
3073 return wfTimestamp( TS_MW, $time );
3074 }
3075
3076 /**
3077 * Get the permissions associated with a given list of groups
3078 *
3079 * @param $groups \type{\arrayof{\string}} List of internal group names
3080 * @return \type{\arrayof{\string}} List of permission key names for given groups combined
3081 */
3082 static function getGroupPermissions( $groups ) {
3083 global $wgGroupPermissions, $wgRevokePermissions;
3084 $rights = array();
3085 // grant every granted permission first
3086 foreach( $groups as $group ) {
3087 if( isset( $wgGroupPermissions[$group] ) ) {
3088 $rights = array_merge( $rights,
3089 // array_filter removes empty items
3090 array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
3091 }
3092 }
3093 // now revoke the revoked permissions
3094 foreach( $groups as $group ) {
3095 if( isset( $wgRevokePermissions[$group] ) ) {
3096 $rights = array_diff( $rights,
3097 array_keys( array_filter( $wgRevokePermissions[$group] ) ) );
3098 }
3099 }
3100 return array_unique($rights);
3101 }
3102
3103 /**
3104 * Get all the groups who have a given permission
3105 *
3106 * @param $role \string Role to check
3107 * @return \type{\arrayof{\string}} List of internal group names with the given permission
3108 */
3109 static function getGroupsWithPermission( $role ) {
3110 global $wgGroupPermissions;
3111 $allowedGroups = array();
3112 foreach ( $wgGroupPermissions as $group => $rights ) {
3113 if ( isset( $rights[$role] ) && $rights[$role] ) {
3114 $allowedGroups[] = $group;
3115 }
3116 }
3117 return $allowedGroups;
3118 }
3119
3120 /**
3121 * Get the localized descriptive name for a group, if it exists
3122 *
3123 * @param $group \string Internal group name
3124 * @return \string Localized descriptive group name
3125 */
3126 static function getGroupName( $group ) {
3127 global $wgMessageCache;
3128 $wgMessageCache->loadAllMessages();
3129 $key = "group-$group";
3130 $name = wfMsg( $key );
3131 return $name == '' || wfEmptyMsg( $key, $name )
3132 ? $group
3133 : $name;
3134 }
3135
3136 /**
3137 * Get the localized descriptive name for a member of a group, if it exists
3138 *
3139 * @param $group \string Internal group name
3140 * @return \string Localized name for group member
3141 */
3142 static function getGroupMember( $group ) {
3143 global $wgMessageCache;
3144 $wgMessageCache->loadAllMessages();
3145 $key = "group-$group-member";
3146 $name = wfMsg( $key );
3147 return $name == '' || wfEmptyMsg( $key, $name )
3148 ? $group
3149 : $name;
3150 }
3151
3152 /**
3153 * Return the set of defined explicit groups.
3154 * The implicit groups (by default *, 'user' and 'autoconfirmed')
3155 * are not included, as they are defined automatically, not in the database.
3156 * @return \type{\arrayof{\string}} Array of internal group names
3157 */
3158 static function getAllGroups() {
3159 global $wgGroupPermissions, $wgRevokePermissions;
3160 return array_diff(
3161 array_merge( array_keys( $wgGroupPermissions ), array_keys( $wgRevokePermissions ) ),
3162 self::getImplicitGroups()
3163 );
3164 }
3165
3166 /**
3167 * Get a list of all available permissions.
3168 * @return \type{\arrayof{\string}} Array of permission names
3169 */
3170 static function getAllRights() {
3171 if ( self::$mAllRights === false ) {
3172 global $wgAvailableRights;
3173 if ( count( $wgAvailableRights ) ) {
3174 self::$mAllRights = array_unique( array_merge( self::$mCoreRights, $wgAvailableRights ) );
3175 } else {
3176 self::$mAllRights = self::$mCoreRights;
3177 }
3178 wfRunHooks( 'UserGetAllRights', array( &self::$mAllRights ) );
3179 }
3180 return self::$mAllRights;
3181 }
3182
3183 /**
3184 * Get a list of implicit groups
3185 * @return \type{\arrayof{\string}} Array of internal group names
3186 */
3187 public static function getImplicitGroups() {
3188 global $wgImplicitGroups;
3189 $groups = $wgImplicitGroups;
3190 wfRunHooks( 'UserGetImplicitGroups', array( &$groups ) ); #deprecated, use $wgImplictGroups instead
3191 return $groups;
3192 }
3193
3194 /**
3195 * Get the title of a page describing a particular group
3196 *
3197 * @param $group \string Internal group name
3198 * @return \types{\type{Title},\bool} Title of the page if it exists, false otherwise
3199 */
3200 static function getGroupPage( $group ) {
3201 global $wgMessageCache;
3202 $wgMessageCache->loadAllMessages();
3203 $page = wfMsgForContent( 'grouppage-' . $group );
3204 if( !wfEmptyMsg( 'grouppage-' . $group, $page ) ) {
3205 $title = Title::newFromText( $page );
3206 if( is_object( $title ) )
3207 return $title;
3208 }
3209 return false;
3210 }
3211
3212 /**
3213 * Create a link to the group in HTML, if available;
3214 * else return the group name.
3215 *
3216 * @param $group \string Internal name of the group
3217 * @param $text \string The text of the link
3218 * @return \string HTML link to the group
3219 */
3220 static function makeGroupLinkHTML( $group, $text = '' ) {
3221 if( $text == '' ) {
3222 $text = self::getGroupName( $group );
3223 }
3224 $title = self::getGroupPage( $group );
3225 if( $title ) {
3226 global $wgUser;
3227 $sk = $wgUser->getSkin();
3228 return $sk->link( $title, htmlspecialchars( $text ) );
3229 } else {
3230 return $text;
3231 }
3232 }
3233
3234 /**
3235 * Create a link to the group in Wikitext, if available;
3236 * else return the group name.
3237 *
3238 * @param $group \string Internal name of the group
3239 * @param $text \string The text of the link
3240 * @return \string Wikilink to the group
3241 */
3242 static function makeGroupLinkWiki( $group, $text = '' ) {
3243 if( $text == '' ) {
3244 $text = self::getGroupName( $group );
3245 }
3246 $title = self::getGroupPage( $group );
3247 if( $title ) {
3248 $page = $title->getPrefixedText();
3249 return "[[$page|$text]]";
3250 } else {
3251 return $text;
3252 }
3253 }
3254
3255 /**
3256 * Returns an array of the groups that a particular group can add/remove.
3257 *
3258 * @param $group String: the group to check for whether it can add/remove
3259 * @return Array array( 'add' => array( addablegroups ),
3260 * 'remove' => array( removablegroups ),
3261 * 'add-self' => array( addablegroups to self),
3262 * 'remove-self' => array( removable groups from self) )
3263 */
3264 static function changeableByGroup( $group ) {
3265 global $wgAddGroups, $wgRemoveGroups, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
3266
3267 $groups = array( 'add' => array(), 'remove' => array(), 'add-self' => array(), 'remove-self' => array() );
3268 if( empty($wgAddGroups[$group]) ) {
3269 // Don't add anything to $groups
3270 } elseif( $wgAddGroups[$group] === true ) {
3271 // You get everything
3272 $groups['add'] = self::getAllGroups();
3273 } elseif( is_array($wgAddGroups[$group]) ) {
3274 $groups['add'] = $wgAddGroups[$group];
3275 }
3276
3277 // Same thing for remove
3278 if( empty($wgRemoveGroups[$group]) ) {
3279 } elseif($wgRemoveGroups[$group] === true ) {
3280 $groups['remove'] = self::getAllGroups();
3281 } elseif( is_array($wgRemoveGroups[$group]) ) {
3282 $groups['remove'] = $wgRemoveGroups[$group];
3283 }
3284
3285 // Re-map numeric keys of AddToSelf/RemoveFromSelf to the 'user' key for backwards compatibility
3286 if( empty($wgGroupsAddToSelf['user']) || $wgGroupsAddToSelf['user'] !== true ) {
3287 foreach($wgGroupsAddToSelf as $key => $value) {
3288 if( is_int($key) ) {
3289 $wgGroupsAddToSelf['user'][] = $value;
3290 }
3291 }
3292 }
3293
3294 if( empty($wgGroupsRemoveFromSelf['user']) || $wgGroupsRemoveFromSelf['user'] !== true ) {
3295 foreach($wgGroupsRemoveFromSelf as $key => $value) {
3296 if( is_int($key) ) {
3297 $wgGroupsRemoveFromSelf['user'][] = $value;
3298 }
3299 }
3300 }
3301
3302 // Now figure out what groups the user can add to him/herself
3303 if( empty($wgGroupsAddToSelf[$group]) ) {
3304 } elseif( $wgGroupsAddToSelf[$group] === true ) {
3305 // No idea WHY this would be used, but it's there
3306 $groups['add-self'] = User::getAllGroups();
3307 } elseif( is_array($wgGroupsAddToSelf[$group]) ) {
3308 $groups['add-self'] = $wgGroupsAddToSelf[$group];
3309 }
3310
3311 if( empty($wgGroupsRemoveFromSelf[$group]) ) {
3312 } elseif( $wgGroupsRemoveFromSelf[$group] === true ) {
3313 $groups['remove-self'] = User::getAllGroups();
3314 } elseif( is_array($wgGroupsRemoveFromSelf[$group]) ) {
3315 $groups['remove-self'] = $wgGroupsRemoveFromSelf[$group];
3316 }
3317
3318 return $groups;
3319 }
3320
3321 /**
3322 * Returns an array of groups that this user can add and remove
3323 * @return Array array( 'add' => array( addablegroups ),
3324 * 'remove' => array( removablegroups ),
3325 * 'add-self' => array( addablegroups to self),
3326 * 'remove-self' => array( removable groups from self) )
3327 */
3328 function changeableGroups() {
3329 if( $this->isAllowed( 'userrights' ) ) {
3330 // This group gives the right to modify everything (reverse-
3331 // compatibility with old "userrights lets you change
3332 // everything")
3333 // Using array_merge to make the groups reindexed
3334 $all = array_merge( User::getAllGroups() );
3335 return array(
3336 'add' => $all,
3337 'remove' => $all,
3338 'add-self' => array(),
3339 'remove-self' => array()
3340 );
3341 }
3342
3343 // Okay, it's not so simple, we will have to go through the arrays
3344 $groups = array(
3345 'add' => array(),
3346 'remove' => array(),
3347 'add-self' => array(),
3348 'remove-self' => array() );
3349 $addergroups = $this->getEffectiveGroups();
3350
3351 foreach ($addergroups as $addergroup) {
3352 $groups = array_merge_recursive(
3353 $groups, $this->changeableByGroup($addergroup)
3354 );
3355 $groups['add'] = array_unique( $groups['add'] );
3356 $groups['remove'] = array_unique( $groups['remove'] );
3357 $groups['add-self'] = array_unique( $groups['add-self'] );
3358 $groups['remove-self'] = array_unique( $groups['remove-self'] );
3359 }
3360 return $groups;
3361 }
3362
3363 /**
3364 * Increment the user's edit-count field.
3365 * Will have no effect for anonymous users.
3366 */
3367 function incEditCount() {
3368 if( !$this->isAnon() ) {
3369 $dbw = wfGetDB( DB_MASTER );
3370 $dbw->update( 'user',
3371 array( 'user_editcount=user_editcount+1' ),
3372 array( 'user_id' => $this->getId() ),
3373 __METHOD__ );
3374
3375 // Lazy initialization check...
3376 if( $dbw->affectedRows() == 0 ) {
3377 // Pull from a slave to be less cruel to servers
3378 // Accuracy isn't the point anyway here
3379 $dbr = wfGetDB( DB_SLAVE );
3380 $count = $dbr->selectField( 'revision',
3381 'COUNT(rev_user)',
3382 array( 'rev_user' => $this->getId() ),
3383 __METHOD__ );
3384
3385 // Now here's a goddamn hack...
3386 if( $dbr !== $dbw ) {
3387 // If we actually have a slave server, the count is
3388 // at least one behind because the current transaction
3389 // has not been committed and replicated.
3390 $count++;
3391 } else {
3392 // But if DB_SLAVE is selecting the master, then the
3393 // count we just read includes the revision that was
3394 // just added in the working transaction.
3395 }
3396
3397 $dbw->update( 'user',
3398 array( 'user_editcount' => $count ),
3399 array( 'user_id' => $this->getId() ),
3400 __METHOD__ );
3401 }
3402 }
3403 // edit count in user cache too
3404 $this->invalidateCache();
3405 }
3406
3407 /**
3408 * Get the description of a given right
3409 *
3410 * @param $right \string Right to query
3411 * @return \string Localized description of the right
3412 */
3413 static function getRightDescription( $right ) {
3414 global $wgMessageCache;
3415 $wgMessageCache->loadAllMessages();
3416 $key = "right-$right";
3417 $name = wfMsg( $key );
3418 return $name == '' || wfEmptyMsg( $key, $name )
3419 ? $right
3420 : $name;
3421 }
3422
3423 /**
3424 * Make an old-style password hash
3425 *
3426 * @param $password \string Plain-text password
3427 * @param $userId \string User ID
3428 * @return \string Password hash
3429 */
3430 static function oldCrypt( $password, $userId ) {
3431 global $wgPasswordSalt;
3432 if ( $wgPasswordSalt ) {
3433 return md5( $userId . '-' . md5( $password ) );
3434 } else {
3435 return md5( $password );
3436 }
3437 }
3438
3439 /**
3440 * Make a new-style password hash
3441 *
3442 * @param $password \string Plain-text password
3443 * @param $salt \string Optional salt, may be random or the user ID.
3444 * If unspecified or false, will generate one automatically
3445 * @return \string Password hash
3446 */
3447 static function crypt( $password, $salt = false ) {
3448 global $wgPasswordSalt;
3449
3450 $hash = '';
3451 if( !wfRunHooks( 'UserCryptPassword', array( &$password, &$salt, &$wgPasswordSalt, &$hash ) ) ) {
3452 return $hash;
3453 }
3454
3455 if( $wgPasswordSalt ) {
3456 if ( $salt === false ) {
3457 $salt = substr( wfGenerateToken(), 0, 8 );
3458 }
3459 return ':B:' . $salt . ':' . md5( $salt . '-' . md5( $password ) );
3460 } else {
3461 return ':A:' . md5( $password );
3462 }
3463 }
3464
3465 /**
3466 * Compare a password hash with a plain-text password. Requires the user
3467 * ID if there's a chance that the hash is an old-style hash.
3468 *
3469 * @param $hash \string Password hash
3470 * @param $password \string Plain-text password to compare
3471 * @param $userId \string User ID for old-style password salt
3472 * @return \bool
3473 */
3474 static function comparePasswords( $hash, $password, $userId = false ) {
3475 $m = false;
3476 $type = substr( $hash, 0, 3 );
3477
3478 $result = false;
3479 if( !wfRunHooks( 'UserComparePasswords', array( &$hash, &$password, &$userId, &$result ) ) ) {
3480 return $result;
3481 }
3482
3483 if ( $type == ':A:' ) {
3484 # Unsalted
3485 return md5( $password ) === substr( $hash, 3 );
3486 } elseif ( $type == ':B:' ) {
3487 # Salted
3488 list( $salt, $realHash ) = explode( ':', substr( $hash, 3 ), 2 );
3489 return md5( $salt.'-'.md5( $password ) ) == $realHash;
3490 } else {
3491 # Old-style
3492 return self::oldCrypt( $password, $userId ) === $hash;
3493 }
3494 }
3495
3496 /**
3497 * Add a newuser log entry for this user
3498 * @param $byEmail Boolean: account made by email?
3499 */
3500 public function addNewUserLogEntry( $byEmail = false ) {
3501 global $wgUser, $wgContLang, $wgNewUserLog;
3502 if( empty($wgNewUserLog) ) {
3503 return true; // disabled
3504 }
3505 $talk = $wgContLang->getFormattedNsText( NS_TALK );
3506 if( $this->getName() == $wgUser->getName() ) {
3507 $action = 'create';
3508 $message = '';
3509 } else {
3510 $action = 'create2';
3511 $message = $byEmail
3512 ? wfMsgForContent( 'newuserlog-byemail' )
3513 : '';
3514 }
3515 $log = new LogPage( 'newusers' );
3516 $log->addEntry(
3517 $action,
3518 $this->getUserPage(),
3519 $message,
3520 array( $this->getId() )
3521 );
3522 return true;
3523 }
3524
3525 /**
3526 * Add an autocreate newuser log entry for this user
3527 * Used by things like CentralAuth and perhaps other authplugins.
3528 */
3529 public function addNewUserLogEntryAutoCreate() {
3530 global $wgNewUserLog;
3531 if( empty($wgNewUserLog) ) {
3532 return true; // disabled
3533 }
3534 $log = new LogPage( 'newusers', false );
3535 $log->addEntry( 'autocreate', $this->getUserPage(), '', array( $this->getId() ) );
3536 return true;
3537 }
3538
3539 protected function loadOptions() {
3540 $this->load();
3541 if ($this->mOptionsLoaded || !$this->getId() )
3542 return;
3543
3544 $this->mOptions = self::getDefaultOptions();
3545
3546 // Maybe load from the object
3547
3548 if ( !is_null($this->mOptionOverrides) ) {
3549 wfDebug( "Loading options for user ".$this->getId()." from override cache.\n" );
3550 foreach( $this->mOptionOverrides as $key => $value ) {
3551 $this->mOptions[$key] = $value;
3552 }
3553 } else {
3554 wfDebug( "Loading options for user ".$this->getId()." from database.\n" );
3555 // Load from database
3556 $dbr = wfGetDB( DB_SLAVE );
3557
3558 $res = $dbr->select( 'user_properties',
3559 '*',
3560 array('up_user' => $this->getId()),
3561 __METHOD__
3562 );
3563
3564 while( $row = $dbr->fetchObject( $res ) ) {
3565 $this->mOptionOverrides[$row->up_property] = $row->up_value;
3566 $this->mOptions[$row->up_property] = $row->up_value;
3567 }
3568 }
3569
3570 $this->mOptionsLoaded = true;
3571
3572 wfRunHooks( 'UserLoadOptions', array( $this, &$this->mOptions ) );
3573 }
3574
3575 protected function saveOptions() {
3576 global $wgAllowPrefChange;
3577
3578 $extuser = ExternalUser::newFromUser( $this );
3579
3580 $this->loadOptions();
3581 $dbw = wfGetDB( DB_MASTER );
3582
3583 $insert_rows = array();
3584
3585 $saveOptions = $this->mOptions;
3586
3587 // Allow hooks to abort, for instance to save to a global profile.
3588 // Reset options to default state before saving.
3589 if (!wfRunHooks( 'UserSaveOptions', array($this, &$saveOptions) ) )
3590 return;
3591
3592 foreach( $saveOptions as $key => $value ) {
3593 # Don't bother storing default values
3594 if ( ( is_null( self::getDefaultOption( $key ) ) &&
3595 !( $value === false || is_null($value) ) ) ||
3596 $value != self::getDefaultOption( $key ) ) {
3597 $insert_rows[] = array(
3598 'up_user' => $this->getId(),
3599 'up_property' => $key,
3600 'up_value' => $value,
3601 );
3602 }
3603 if ( $extuser && isset( $wgAllowPrefChange[$key] ) ) {
3604 switch ( $wgAllowPrefChange[$key] ) {
3605 case 'local': case 'message':
3606 break;
3607 case 'semiglobal': case 'global':
3608 $extuser->setPref( $key, $value );
3609 }
3610 }
3611 }
3612
3613 $dbw->begin();
3614 $dbw->delete( 'user_properties', array( 'up_user' => $this->getId() ), __METHOD__ );
3615 $dbw->insert( 'user_properties', $insert_rows, __METHOD__ );
3616 $dbw->commit();
3617 }
3618
3619 /**
3620 * Provide an array of HTML 5 attributes to put on an input element
3621 * intended for the user to enter a new password. This may include
3622 * required, title, and/or pattern, depending on $wgMinimalPasswordLength.
3623 *
3624 * Do *not* use this when asking the user to enter his current password!
3625 * Regardless of configuration, users may have invalid passwords for whatever
3626 * reason (e.g., they were set before requirements were tightened up).
3627 * Only use it when asking for a new password, like on account creation or
3628 * ResetPass.
3629 *
3630 * Obviously, you still need to do server-side checking.
3631 *
3632 * @return array Array of HTML attributes suitable for feeding to
3633 * Html::element(), directly or indirectly. (Don't feed to Xml::*()!
3634 * That will potentially output invalid XHTML 1.0 Transitional, and will
3635 * get confused by the boolean attribute syntax used.)
3636 */
3637 public static function passwordChangeInputAttribs() {
3638 global $wgMinimalPasswordLength;
3639
3640 if ( $wgMinimalPasswordLength == 0 ) {
3641 return array();
3642 }
3643
3644 # Note that the pattern requirement will always be satisfied if the
3645 # input is empty, so we need required in all cases.
3646 $ret = array( 'required' );
3647
3648 # We can't actually do this right now, because Opera 9.6 will print out
3649 # the entered password visibly in its error message! When other
3650 # browsers add support for this attribute, or Opera fixes its support,
3651 # we can add support with a version check to avoid doing this on Opera
3652 # versions where it will be a problem. Reported to Opera as
3653 # DSK-262266, but they don't have a public bug tracker for us to follow.
3654 /*
3655 if ( $wgMinimalPasswordLength > 1 ) {
3656 $ret['pattern'] = '.{' . intval( $wgMinimalPasswordLength ) . ',}';
3657 $ret['title'] = wfMsgExt( 'passwordtooshort', 'parsemag',
3658 $wgMinimalPasswordLength );
3659 }
3660 */
3661
3662 return $ret;
3663 }
3664 }