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