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