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