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