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