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