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