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