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