someone was lost in type conversion too ;-)
[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 * Not really private cause it's called by Language class
764 * @return array
765 * @static
766 * @private
767 */
768 static 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 = '';
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( $newtalk != "" ) {
1125 $this->mNewtalk = (bool)$newtalk;
1126 } else {
1127 $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName() );
1128 $wgMemc->set( $key, (int)$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 if( $this->getId() ) {
1579 $dbw->insert( 'user_groups',
1580 array(
1581 'ug_user' => $this->getID(),
1582 'ug_group' => $group,
1583 ),
1584 'User::addGroup',
1585 array( 'IGNORE' ) );
1586 }
1587
1588 $this->mGroups[] = $group;
1589 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
1590
1591 $this->invalidateCache();
1592 }
1593
1594 /**
1595 * Remove the user from the given group.
1596 * This takes immediate effect.
1597 * @string $group
1598 */
1599 function removeGroup( $group ) {
1600 $this->load();
1601 $dbw =& wfGetDB( DB_MASTER );
1602 $dbw->delete( 'user_groups',
1603 array(
1604 'ug_user' => $this->getID(),
1605 'ug_group' => $group,
1606 ),
1607 'User::removeGroup' );
1608
1609 $this->mGroups = array_diff( $this->mGroups, array( $group ) );
1610 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
1611
1612 $this->invalidateCache();
1613 }
1614
1615
1616 /**
1617 * A more legible check for non-anonymousness.
1618 * Returns true if the user is not an anonymous visitor.
1619 *
1620 * @return bool
1621 */
1622 function isLoggedIn() {
1623 return( $this->getID() != 0 );
1624 }
1625
1626 /**
1627 * A more legible check for anonymousness.
1628 * Returns true if the user is an anonymous visitor.
1629 *
1630 * @return bool
1631 */
1632 function isAnon() {
1633 return !$this->isLoggedIn();
1634 }
1635
1636 /**
1637 * Whether the user is a bot
1638 * @deprecated
1639 */
1640 function isBot() {
1641 return $this->isAllowed( 'bot' );
1642 }
1643
1644 /**
1645 * Check if user is allowed to access a feature / make an action
1646 * @param string $action Action to be checked
1647 * @return boolean True: action is allowed, False: action should not be allowed
1648 */
1649 function isAllowed($action='') {
1650 if ( $action === '' )
1651 // In the spirit of DWIM
1652 return true;
1653
1654 return in_array( $action, $this->getRights() );
1655 }
1656
1657 /**
1658 * Load a skin if it doesn't exist or return it
1659 * @todo FIXME : need to check the old failback system [AV]
1660 */
1661 function &getSkin() {
1662 global $wgRequest;
1663 if ( ! isset( $this->mSkin ) ) {
1664 wfProfileIn( __METHOD__ );
1665
1666 # get the user skin
1667 $userSkin = $this->getOption( 'skin' );
1668 $userSkin = $wgRequest->getVal('useskin', $userSkin);
1669
1670 $this->mSkin =& Skin::newFromKey( $userSkin );
1671 wfProfileOut( __METHOD__ );
1672 }
1673 return $this->mSkin;
1674 }
1675
1676 /**#@+
1677 * @param string $title Article title to look at
1678 */
1679
1680 /**
1681 * Check watched status of an article
1682 * @return bool True if article is watched
1683 */
1684 function isWatched( $title ) {
1685 $wl = WatchedItem::fromUserTitle( $this, $title );
1686 return $wl->isWatched();
1687 }
1688
1689 /**
1690 * Watch an article
1691 */
1692 function addWatch( $title ) {
1693 $wl = WatchedItem::fromUserTitle( $this, $title );
1694 $wl->addWatch();
1695 $this->invalidateCache();
1696 }
1697
1698 /**
1699 * Stop watching an article
1700 */
1701 function removeWatch( $title ) {
1702 $wl = WatchedItem::fromUserTitle( $this, $title );
1703 $wl->removeWatch();
1704 $this->invalidateCache();
1705 }
1706
1707 /**
1708 * Clear the user's notification timestamp for the given title.
1709 * If e-notif e-mails are on, they will receive notification mails on
1710 * the next change of the page if it's watched etc.
1711 */
1712 function clearNotification( &$title ) {
1713 global $wgUser, $wgUseEnotif;
1714
1715 # Do nothing if the database is locked to writes
1716 if( wfReadOnly() ) {
1717 return;
1718 }
1719
1720 if ($title->getNamespace() == NS_USER_TALK &&
1721 $title->getText() == $this->getName() ) {
1722 if (!wfRunHooks('UserClearNewTalkNotification', array(&$this)))
1723 return;
1724 $this->setNewtalk( false );
1725 }
1726
1727 if( !$wgUseEnotif ) {
1728 return;
1729 }
1730
1731 if( $this->isAnon() ) {
1732 // Nothing else to do...
1733 return;
1734 }
1735
1736 // Only update the timestamp if the page is being watched.
1737 // The query to find out if it is watched is cached both in memcached and per-invocation,
1738 // and when it does have to be executed, it can be on a slave
1739 // If this is the user's newtalk page, we always update the timestamp
1740 if ($title->getNamespace() == NS_USER_TALK &&
1741 $title->getText() == $wgUser->getName())
1742 {
1743 $watched = true;
1744 } elseif ( $this->getID() == $wgUser->getID() ) {
1745 $watched = $title->userIsWatching();
1746 } else {
1747 $watched = true;
1748 }
1749
1750 // If the page is watched by the user (or may be watched), update the timestamp on any
1751 // any matching rows
1752 if ( $watched ) {
1753 $dbw =& wfGetDB( DB_MASTER );
1754 $dbw->update( 'watchlist',
1755 array( /* SET */
1756 'wl_notificationtimestamp' => NULL
1757 ), array( /* WHERE */
1758 'wl_title' => $title->getDBkey(),
1759 'wl_namespace' => $title->getNamespace(),
1760 'wl_user' => $this->getID()
1761 ), 'User::clearLastVisited'
1762 );
1763 }
1764 }
1765
1766 /**#@-*/
1767
1768 /**
1769 * Resets all of the given user's page-change notification timestamps.
1770 * If e-notif e-mails are on, they will receive notification mails on
1771 * the next change of any watched page.
1772 *
1773 * @param int $currentUser user ID number
1774 * @public
1775 */
1776 function clearAllNotifications( $currentUser ) {
1777 global $wgUseEnotif;
1778 if ( !$wgUseEnotif ) {
1779 $this->setNewtalk( false );
1780 return;
1781 }
1782 if( $currentUser != 0 ) {
1783
1784 $dbw =& wfGetDB( DB_MASTER );
1785 $dbw->update( 'watchlist',
1786 array( /* SET */
1787 'wl_notificationtimestamp' => NULL
1788 ), array( /* WHERE */
1789 'wl_user' => $currentUser
1790 ), 'UserMailer::clearAll'
1791 );
1792
1793 # we also need to clear here the "you have new message" notification for the own user_talk page
1794 # This is cleared one page view later in Article::viewUpdates();
1795 }
1796 }
1797
1798 /**
1799 * @private
1800 * @return string Encoding options
1801 */
1802 function encodeOptions() {
1803 $this->load();
1804 if ( is_null( $this->mOptions ) ) {
1805 $this->mOptions = User::getDefaultOptions();
1806 }
1807 $a = array();
1808 foreach ( $this->mOptions as $oname => $oval ) {
1809 array_push( $a, $oname.'='.$oval );
1810 }
1811 $s = implode( "\n", $a );
1812 return $s;
1813 }
1814
1815 /**
1816 * @private
1817 */
1818 function decodeOptions( $str ) {
1819 $this->mOptions = array();
1820 $a = explode( "\n", $str );
1821 foreach ( $a as $s ) {
1822 $m = array();
1823 if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
1824 $this->mOptions[$m[1]] = $m[2];
1825 }
1826 }
1827 }
1828
1829 function setCookies() {
1830 global $wgCookieExpiration, $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookiePrefix;
1831 $this->load();
1832 if ( 0 == $this->mId ) return;
1833 $exp = time() + $wgCookieExpiration;
1834
1835 $_SESSION['wsUserID'] = $this->mId;
1836 setcookie( $wgCookiePrefix.'UserID', $this->mId, $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1837
1838 $_SESSION['wsUserName'] = $this->getName();
1839 setcookie( $wgCookiePrefix.'UserName', $this->getName(), $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1840
1841 $_SESSION['wsToken'] = $this->mToken;
1842 if ( 1 == $this->getOption( 'rememberpassword' ) ) {
1843 setcookie( $wgCookiePrefix.'Token', $this->mToken, $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1844 } else {
1845 setcookie( $wgCookiePrefix.'Token', '', time() - 3600 );
1846 }
1847 }
1848
1849 /**
1850 * Logout user
1851 * Clears the cookies and session, resets the instance cache
1852 */
1853 function logout() {
1854 global $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookiePrefix;
1855 $this->clearInstanceCache( 'defaults' );
1856
1857 $_SESSION['wsUserID'] = 0;
1858
1859 setcookie( $wgCookiePrefix.'UserID', '', time() - 3600, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1860 setcookie( $wgCookiePrefix.'Token', '', time() - 3600, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1861
1862 # Remember when user logged out, to prevent seeing cached pages
1863 setcookie( $wgCookiePrefix.'LoggedOut', wfTimestampNow(), time() + 86400, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1864 }
1865
1866 /**
1867 * Save object settings into database
1868 * @fixme Only rarely do all these fields need to be set!
1869 */
1870 function saveSettings() {
1871 $this->load();
1872 if ( wfReadOnly() ) { return; }
1873 if ( 0 == $this->mId ) { return; }
1874
1875 $this->mTouched = self::newTouchedTimestamp();
1876
1877 $dbw =& wfGetDB( DB_MASTER );
1878 $dbw->update( 'user',
1879 array( /* SET */
1880 'user_name' => $this->mName,
1881 'user_password' => $this->mPassword,
1882 'user_newpassword' => $this->mNewpassword,
1883 'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ),
1884 'user_real_name' => $this->mRealName,
1885 'user_email' => $this->mEmail,
1886 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
1887 'user_options' => $this->encodeOptions(),
1888 'user_touched' => $dbw->timestamp($this->mTouched),
1889 'user_token' => $this->mToken
1890 ), array( /* WHERE */
1891 'user_id' => $this->mId
1892 ), __METHOD__
1893 );
1894 $this->clearSharedCache();
1895 }
1896
1897
1898 /**
1899 * Checks if a user with the given name exists, returns the ID
1900 */
1901 function idForName() {
1902 $s = trim( $this->getName() );
1903 if ( 0 == strcmp( '', $s ) ) return 0;
1904
1905 $dbr =& wfGetDB( DB_SLAVE );
1906 $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), __METHOD__ );
1907 if ( $id === false ) {
1908 $id = 0;
1909 }
1910 return $id;
1911 }
1912
1913 /**
1914 * Add a user to the database, return the user object
1915 *
1916 * @param string $name The user's name
1917 * @param array $params Associative array of non-default parameters to save to the database:
1918 * password The user's password. Password logins will be disabled if this is omitted.
1919 * newpassword A temporary password mailed to the user
1920 * email The user's email address
1921 * email_authenticated The email authentication timestamp
1922 * real_name The user's real name
1923 * options An associative array of non-default options
1924 * token Random authentication token. Do not set.
1925 * registration Registration timestamp. Do not set.
1926 *
1927 * @return User object, or null if the username already exists
1928 */
1929 static function createNew( $name, $params = array() ) {
1930 $user = new User;
1931 $user->load();
1932 if ( isset( $params['options'] ) ) {
1933 $user->mOptions = $params['options'] + $user->mOptions;
1934 unset( $params['options'] );
1935 }
1936 $dbw =& wfGetDB( DB_MASTER );
1937 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
1938 $fields = array(
1939 'user_id' => $seqVal,
1940 'user_name' => $name,
1941 'user_password' => $user->mPassword,
1942 'user_newpassword' => $user->mNewpassword,
1943 'user_newpass_time' => $dbw->timestamp( $user->mNewpassTime ),
1944 'user_email' => $user->mEmail,
1945 'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ),
1946 'user_real_name' => $user->mRealName,
1947 'user_options' => $user->encodeOptions(),
1948 'user_token' => $user->mToken,
1949 'user_registration' => $dbw->timestamp( $user->mRegistration ),
1950 'user_editcount' => 0,
1951 );
1952 foreach ( $params as $name => $value ) {
1953 $fields["user_$name"] = $value;
1954 }
1955 $dbw->insert( 'user', $fields, __METHOD__, array( 'IGNORE' ) );
1956 if ( $dbw->affectedRows() ) {
1957 $newUser = User::newFromId( $dbw->insertId() );
1958 } else {
1959 $newUser = null;
1960 }
1961 return $newUser;
1962 }
1963
1964 /**
1965 * Add an existing user object to the database
1966 */
1967 function addToDatabase() {
1968 $this->load();
1969 $dbw =& wfGetDB( DB_MASTER );
1970 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
1971 $dbw->insert( 'user',
1972 array(
1973 'user_id' => $seqVal,
1974 'user_name' => $this->mName,
1975 'user_password' => $this->mPassword,
1976 'user_newpassword' => $this->mNewpassword,
1977 'user_newpass_time' => $dbw->timestamp( $this->mNewpassTime ),
1978 'user_email' => $this->mEmail,
1979 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
1980 'user_real_name' => $this->mRealName,
1981 'user_options' => $this->encodeOptions(),
1982 'user_token' => $this->mToken,
1983 'user_registration' => $dbw->timestamp( $this->mRegistration ),
1984 'user_editcount' => 0,
1985 ), __METHOD__
1986 );
1987 $this->mId = $dbw->insertId();
1988
1989 # Clear instance cache other than user table data, which is already accurate
1990 $this->clearInstanceCache();
1991 }
1992
1993 /**
1994 * If the (non-anonymous) user is blocked, this function will block any IP address
1995 * that they successfully log on from.
1996 */
1997 function spreadBlock() {
1998 wfDebug( __METHOD__."()\n" );
1999 $this->load();
2000 if ( $this->mId == 0 ) {
2001 return;
2002 }
2003
2004 $userblock = Block::newFromDB( '', $this->mId );
2005 if ( !$userblock ) {
2006 return;
2007 }
2008
2009 $userblock->doAutoblock( wfGetIp() );
2010
2011 }
2012
2013 /**
2014 * Generate a string which will be different for any combination of
2015 * user options which would produce different parser output.
2016 * This will be used as part of the hash key for the parser cache,
2017 * so users will the same options can share the same cached data
2018 * safely.
2019 *
2020 * Extensions which require it should install 'PageRenderingHash' hook,
2021 * which will give them a chance to modify this key based on their own
2022 * settings.
2023 *
2024 * @return string
2025 */
2026 function getPageRenderingHash() {
2027 global $wgContLang, $wgUseDynamicDates, $wgLang;
2028 if( $this->mHash ){
2029 return $this->mHash;
2030 }
2031
2032 // stubthreshold is only included below for completeness,
2033 // it will always be 0 when this function is called by parsercache.
2034
2035 $confstr = $this->getOption( 'math' );
2036 $confstr .= '!' . $this->getOption( 'stubthreshold' );
2037 if ( $wgUseDynamicDates ) {
2038 $confstr .= '!' . $this->getDatePreference();
2039 }
2040 $confstr .= '!' . ($this->getOption( 'numberheadings' ) ? '1' : '');
2041 $confstr .= '!' . $wgLang->getCode();
2042 $confstr .= '!' . $this->getOption( 'thumbsize' );
2043 // add in language specific options, if any
2044 $extra = $wgContLang->getExtraHashOptions();
2045 $confstr .= $extra;
2046
2047 // Give a chance for extensions to modify the hash, if they have
2048 // extra options or other effects on the parser cache.
2049 wfRunHooks( 'PageRenderingHash', array( &$confstr ) );
2050
2051 $this->mHash = $confstr;
2052 return $confstr;
2053 }
2054
2055 function isBlockedFromCreateAccount() {
2056 $this->getBlockedStatus();
2057 return $this->mBlock && $this->mBlock->mCreateAccount;
2058 }
2059
2060 function isAllowedToCreateAccount() {
2061 return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount();
2062 }
2063
2064 /**
2065 * @deprecated
2066 */
2067 function setLoaded( $loaded ) {}
2068
2069 /**
2070 * Get this user's personal page title.
2071 *
2072 * @return Title
2073 * @public
2074 */
2075 function getUserPage() {
2076 return Title::makeTitle( NS_USER, $this->getName() );
2077 }
2078
2079 /**
2080 * Get this user's talk page title.
2081 *
2082 * @return Title
2083 * @public
2084 */
2085 function getTalkPage() {
2086 $title = $this->getUserPage();
2087 return $title->getTalkPage();
2088 }
2089
2090 /**
2091 * @static
2092 */
2093 function getMaxID() {
2094 static $res; // cache
2095
2096 if ( isset( $res ) )
2097 return $res;
2098 else {
2099 $dbr =& wfGetDB( DB_SLAVE );
2100 return $res = $dbr->selectField( 'user', 'max(user_id)', false, 'User::getMaxID' );
2101 }
2102 }
2103
2104 /**
2105 * Determine whether the user is a newbie. Newbies are either
2106 * anonymous IPs, or the most recently created accounts.
2107 * @return bool True if it is a newbie.
2108 */
2109 function isNewbie() {
2110 return !$this->isAllowed( 'autoconfirmed' );
2111 }
2112
2113 /**
2114 * Check to see if the given clear-text password is one of the accepted passwords
2115 * @param string $password User password.
2116 * @return bool True if the given password is correct otherwise False.
2117 */
2118 function checkPassword( $password ) {
2119 global $wgAuth;
2120 $this->load();
2121
2122 // Even though we stop people from creating passwords that
2123 // are shorter than this, doesn't mean people wont be able
2124 // to. Certain authentication plugins do NOT want to save
2125 // domain passwords in a mysql database, so we should
2126 // check this (incase $wgAuth->strict() is false).
2127 if( !$this->isValidPassword( $password ) ) {
2128 return false;
2129 }
2130
2131 if( $wgAuth->authenticate( $this->getName(), $password ) ) {
2132 return true;
2133 } elseif( $wgAuth->strict() ) {
2134 /* Auth plugin doesn't allow local authentication */
2135 return false;
2136 }
2137 $ep = $this->encryptPassword( $password );
2138 if ( 0 == strcmp( $ep, $this->mPassword ) ) {
2139 return true;
2140 } elseif ( function_exists( 'iconv' ) ) {
2141 # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
2142 # Check for this with iconv
2143 $cp1252hash = $this->encryptPassword( iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password ) );
2144 if ( 0 == strcmp( $cp1252hash, $this->mPassword ) ) {
2145 return true;
2146 }
2147 }
2148 return false;
2149 }
2150
2151 /**
2152 * Check if the given clear-text password matches the temporary password
2153 * sent by e-mail for password reset operations.
2154 * @return bool
2155 */
2156 function checkTemporaryPassword( $plaintext ) {
2157 $hash = $this->encryptPassword( $plaintext );
2158 return $hash === $this->mNewpassword;
2159 }
2160
2161 /**
2162 * Initialize (if necessary) and return a session token value
2163 * which can be used in edit forms to show that the user's
2164 * login credentials aren't being hijacked with a foreign form
2165 * submission.
2166 *
2167 * @param mixed $salt - Optional function-specific data for hash.
2168 * Use a string or an array of strings.
2169 * @return string
2170 * @public
2171 */
2172 function editToken( $salt = '' ) {
2173 if( !isset( $_SESSION['wsEditToken'] ) ) {
2174 $token = $this->generateToken();
2175 $_SESSION['wsEditToken'] = $token;
2176 } else {
2177 $token = $_SESSION['wsEditToken'];
2178 }
2179 if( is_array( $salt ) ) {
2180 $salt = implode( '|', $salt );
2181 }
2182 return md5( $token . $salt ) . EDIT_TOKEN_SUFFIX;
2183 }
2184
2185 /**
2186 * Generate a hex-y looking random token for various uses.
2187 * Could be made more cryptographically sure if someone cares.
2188 * @return string
2189 */
2190 function generateToken( $salt = '' ) {
2191 $token = dechex( mt_rand() ) . dechex( mt_rand() );
2192 return md5( $token . $salt );
2193 }
2194
2195 /**
2196 * Check given value against the token value stored in the session.
2197 * A match should confirm that the form was submitted from the
2198 * user's own login session, not a form submission from a third-party
2199 * site.
2200 *
2201 * @param string $val - the input value to compare
2202 * @param string $salt - Optional function-specific data for hash
2203 * @return bool
2204 * @public
2205 */
2206 function matchEditToken( $val, $salt = '' ) {
2207 global $wgMemc;
2208 $sessionToken = $this->editToken( $salt );
2209 if ( $val != $sessionToken ) {
2210 wfDebug( "User::matchEditToken: broken session data\n" );
2211 }
2212 return $val == $sessionToken;
2213 }
2214
2215 /**
2216 * Generate a new e-mail confirmation token and send a confirmation
2217 * mail to the user's given address.
2218 *
2219 * @return mixed True on success, a WikiError object on failure.
2220 */
2221 function sendConfirmationMail() {
2222 global $wgContLang;
2223 $expiration = null; // gets passed-by-ref and defined in next line.
2224 $url = $this->confirmationTokenUrl( $expiration );
2225 return $this->sendMail( wfMsg( 'confirmemail_subject' ),
2226 wfMsg( 'confirmemail_body',
2227 wfGetIP(),
2228 $this->getName(),
2229 $url,
2230 $wgContLang->timeanddate( $expiration, false ) ) );
2231 }
2232
2233 /**
2234 * Send an e-mail to this user's account. Does not check for
2235 * confirmed status or validity.
2236 *
2237 * @param string $subject
2238 * @param string $body
2239 * @param strong $from Optional from address; default $wgPasswordSender will be used otherwise.
2240 * @return mixed True on success, a WikiError object on failure.
2241 */
2242 function sendMail( $subject, $body, $from = null ) {
2243 if( is_null( $from ) ) {
2244 global $wgPasswordSender;
2245 $from = $wgPasswordSender;
2246 }
2247
2248 require_once( 'UserMailer.php' );
2249 $to = new MailAddress( $this );
2250 $sender = new MailAddress( $from );
2251 $error = userMailer( $to, $sender, $subject, $body );
2252
2253 if( $error == '' ) {
2254 return true;
2255 } else {
2256 return new WikiError( $error );
2257 }
2258 }
2259
2260 /**
2261 * Generate, store, and return a new e-mail confirmation code.
2262 * A hash (unsalted since it's used as a key) is stored.
2263 * @param &$expiration mixed output: accepts the expiration time
2264 * @return string
2265 * @private
2266 */
2267 function confirmationToken( &$expiration ) {
2268 $now = time();
2269 $expires = $now + 7 * 24 * 60 * 60;
2270 $expiration = wfTimestamp( TS_MW, $expires );
2271
2272 $token = $this->generateToken( $this->mId . $this->mEmail . $expires );
2273 $hash = md5( $token );
2274
2275 $dbw =& wfGetDB( DB_MASTER );
2276 $dbw->update( 'user',
2277 array( 'user_email_token' => $hash,
2278 'user_email_token_expires' => $dbw->timestamp( $expires ) ),
2279 array( 'user_id' => $this->mId ),
2280 __METHOD__ );
2281
2282 return $token;
2283 }
2284
2285 /**
2286 * Generate and store a new e-mail confirmation token, and return
2287 * the URL the user can use to confirm.
2288 * @param &$expiration mixed output: accepts the expiration time
2289 * @return string
2290 * @private
2291 */
2292 function confirmationTokenUrl( &$expiration ) {
2293 $token = $this->confirmationToken( $expiration );
2294 $title = SpecialPage::getTitleFor( 'Confirmemail', $token );
2295 return $title->getFullUrl();
2296 }
2297
2298 /**
2299 * Mark the e-mail address confirmed and save.
2300 */
2301 function confirmEmail() {
2302 $this->load();
2303 $this->mEmailAuthenticated = wfTimestampNow();
2304 $this->saveSettings();
2305 return true;
2306 }
2307
2308 /**
2309 * Is this user allowed to send e-mails within limits of current
2310 * site configuration?
2311 * @return bool
2312 */
2313 function canSendEmail() {
2314 return $this->isEmailConfirmed();
2315 }
2316
2317 /**
2318 * Is this user allowed to receive e-mails within limits of current
2319 * site configuration?
2320 * @return bool
2321 */
2322 function canReceiveEmail() {
2323 return $this->canSendEmail() && !$this->getOption( 'disablemail' );
2324 }
2325
2326 /**
2327 * Is this user's e-mail address valid-looking and confirmed within
2328 * limits of the current site configuration?
2329 *
2330 * If $wgEmailAuthentication is on, this may require the user to have
2331 * confirmed their address by returning a code or using a password
2332 * sent to the address from the wiki.
2333 *
2334 * @return bool
2335 */
2336 function isEmailConfirmed() {
2337 global $wgEmailAuthentication;
2338 $this->load();
2339 $confirmed = true;
2340 if( wfRunHooks( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) {
2341 if( $this->isAnon() )
2342 return false;
2343 if( !self::isValidEmailAddr( $this->mEmail ) )
2344 return false;
2345 if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() )
2346 return false;
2347 return true;
2348 } else {
2349 return $confirmed;
2350 }
2351 }
2352
2353 /**
2354 * Return true if there is an outstanding request for e-mail confirmation.
2355 * @return bool
2356 */
2357 function isEmailConfirmationPending() {
2358 global $wgEmailAuthentication;
2359 return $wgEmailAuthentication &&
2360 !$this->isEmailConfirmed() &&
2361 $this->mEmailToken &&
2362 $this->mEmailTokenExpires > wfTimestamp();
2363 }
2364
2365 /**
2366 * @param array $groups list of groups
2367 * @return array list of permission key names for given groups combined
2368 * @static
2369 */
2370 static function getGroupPermissions( $groups ) {
2371 global $wgGroupPermissions;
2372 $rights = array();
2373 foreach( $groups as $group ) {
2374 if( isset( $wgGroupPermissions[$group] ) ) {
2375 $rights = array_merge( $rights,
2376 array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
2377 }
2378 }
2379 return $rights;
2380 }
2381
2382 /**
2383 * @param string $group key name
2384 * @return string localized descriptive name for group, if provided
2385 * @static
2386 */
2387 static function getGroupName( $group ) {
2388 $key = "group-$group";
2389 $name = wfMsg( $key );
2390 if( $name == '' || wfEmptyMsg( $key, $name ) ) {
2391 return $group;
2392 } else {
2393 return $name;
2394 }
2395 }
2396
2397 /**
2398 * @param string $group key name
2399 * @return string localized descriptive name for member of a group, if provided
2400 * @static
2401 */
2402 static function getGroupMember( $group ) {
2403 $key = "group-$group-member";
2404 $name = wfMsg( $key );
2405 if( $name == '' || wfEmptyMsg( $key, $name ) ) {
2406 return $group;
2407 } else {
2408 return $name;
2409 }
2410 }
2411
2412 /**
2413 * Return the set of defined explicit groups.
2414 * The *, 'user', 'autoconfirmed' and 'emailconfirmed'
2415 * groups are not included, as they are defined
2416 * automatically, not in the database.
2417 * @return array
2418 * @static
2419 */
2420 static function getAllGroups() {
2421 global $wgGroupPermissions;
2422 return array_diff(
2423 array_keys( $wgGroupPermissions ),
2424 array( '*', 'user', 'autoconfirmed', 'emailconfirmed' ) );
2425 }
2426
2427 /**
2428 * Get the title of a page describing a particular group
2429 *
2430 * @param $group Name of the group
2431 * @return mixed
2432 */
2433 static function getGroupPage( $group ) {
2434 $page = wfMsgForContent( 'grouppage-' . $group );
2435 if( !wfEmptyMsg( 'grouppage-' . $group, $page ) ) {
2436 $title = Title::newFromText( $page );
2437 if( is_object( $title ) )
2438 return $title;
2439 }
2440 return false;
2441 }
2442
2443 /**
2444 * Create a link to the group in HTML, if available
2445 *
2446 * @param $group Name of the group
2447 * @param $text The text of the link
2448 * @return mixed
2449 */
2450 static function makeGroupLinkHTML( $group, $text = '' ) {
2451 if( $text == '' ) {
2452 $text = self::getGroupName( $group );
2453 }
2454 $title = self::getGroupPage( $group );
2455 if( $title ) {
2456 global $wgUser;
2457 $sk = $wgUser->getSkin();
2458 return $sk->makeLinkObj( $title, $text );
2459 } else {
2460 return $text;
2461 }
2462 }
2463
2464 /**
2465 * Create a link to the group in Wikitext, if available
2466 *
2467 * @param $group Name of the group
2468 * @param $text The text of the link (by default, the name of the group)
2469 * @return mixed
2470 */
2471 static function makeGroupLinkWiki( $group, $text = '' ) {
2472 if( $text == '' ) {
2473 $text = self::getGroupName( $group );
2474 }
2475 $title = self::getGroupPage( $group );
2476 if( $title ) {
2477 $page = $title->getPrefixedText();
2478 return "[[$page|$text]]";
2479 } else {
2480 return $text;
2481 }
2482 }
2483
2484 /**
2485 * Increment the user's edit-count field.
2486 * Will have no effect for anonymous users.
2487 */
2488 function incEditCount() {
2489 if( !$this->isAnon() ) {
2490 $dbw = wfGetDB( DB_MASTER );
2491 $dbw->update( 'user',
2492 array( 'user_editcount=user_editcount+1' ),
2493 array( 'user_id' => $this->getId() ),
2494 __METHOD__ );
2495
2496 // Lazy initialization check...
2497 if( $dbw->affectedRows() == 0 ) {
2498 // Pull from a slave to be less cruel to servers
2499 // Accuracy isn't the point anyway here
2500 $dbr = wfGetDB( DB_SLAVE );
2501 $count = $dbr->selectField( 'revision',
2502 'COUNT(rev_user)',
2503 array( 'rev_user' => $this->getId() ),
2504 __METHOD__ );
2505
2506 // Now here's a goddamn hack...
2507 if( $dbr !== $dbw ) {
2508 // If we actually have a slave server, the count is
2509 // at least one behind because the current transaction
2510 // has not been committed and replicated.
2511 $count++;
2512 } else {
2513 // But if DB_SLAVE is selecting the master, then the
2514 // count we just read includes the revision that was
2515 // just added in the working transaction.
2516 }
2517
2518 $dbw->update( 'user',
2519 array( 'user_editcount' => $count ),
2520 array( 'user_id' => $this->getId() ),
2521 __METHOD__ );
2522 }
2523 }
2524 }
2525 }
2526
2527 ?>