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