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