* (bug 6029) Improvement to German localisation (de)
[mediawiki.git] / includes / User.php
blob2a3a8cf19a5fefd29afc6b73a536c1184ffb285c
1 <?php
2 /**
3 * See user.txt
5 * @package MediaWiki
6 */
8 /**
11 require_once( 'WatchedItem.php' );
13 # Number of characters in user_token field
14 define( 'USER_TOKEN_LENGTH', 32 );
16 # Serialized record version
17 define( 'MW_USER_VERSION', 3 );
19 /**
21 * @package MediaWiki
23 class User {
25 * When adding a new private variable, dont forget to add it to __sleep()
27 /**@{{
28 * @private
30 var $mBlockedby; //!<
31 var $mBlockreason; //!<
32 var $mDataLoaded; //!<
33 var $mEmail; //!<
34 var $mEmailAuthenticated; //!<
35 var $mGroups; //!<
36 var $mHash; //!<
37 var $mId; //!<
38 var $mName; //!<
39 var $mNewpassword; //!<
40 var $mNewtalk; //!<
41 var $mOptions; //!<
42 var $mPassword; //!<
43 var $mRealName; //!<
44 var $mRegistration; //!<
45 var $mRights; //!<
46 var $mSkin; //!<
47 var $mToken; //!<
48 var $mTouched; //!<
49 var $mVersion; //!< serialized version
50 /**@}} */
52 /** Constructor using User:loadDefaults() */
53 function User() {
54 $this->loadDefaults();
55 $this->mVersion = MW_USER_VERSION;
58 /**
59 * Static factory method
60 * @param string $name Username, validated by Title:newFromText()
61 * @param bool $validate Validate username
62 * @return User
63 * @static
65 function newFromName( $name, $validate = true ) {
66 # Force usernames to capital
67 global $wgContLang;
68 $name = $wgContLang->ucfirst( $name );
70 # Clean up name according to title rules
71 $t = Title::newFromText( $name );
72 if( is_null( $t ) ) {
73 return null;
76 # Reject various classes of invalid names
77 $canonicalName = $t->getText();
78 global $wgAuth;
79 $canonicalName = $wgAuth->getCanonicalName( $t->getText() );
81 if( $validate && !User::isValidUserName( $canonicalName ) ) {
82 return null;
85 $u = new User();
86 $u->setName( $canonicalName );
87 $u->setId( $u->idFromName( $canonicalName ) );
88 return $u;
91 /**
92 * Factory method to fetch whichever use has a given email confirmation code.
93 * This code is generated when an account is created or its e-mail address
94 * has changed.
96 * If the code is invalid or has expired, returns NULL.
98 * @param string $code
99 * @return User
100 * @static
102 function newFromConfirmationCode( $code ) {
103 $dbr =& wfGetDB( DB_SLAVE );
104 $name = $dbr->selectField( 'user', 'user_name', array(
105 'user_email_token' => md5( $code ),
106 'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ),
107 ) );
108 if( is_string( $name ) ) {
109 return User::newFromName( $name );
110 } else {
111 return null;
116 * Serialze sleep function, for better cache efficiency and avoidance of
117 * silly "incomplete type" errors when skins are cached. The array should
118 * contain names of private variables (see at top of User.php).
120 function __sleep() {
121 return array(
122 'mBlockedby',
123 'mBlockreason',
124 'mDataLoaded',
125 'mEmail',
126 'mEmailAuthenticated',
127 'mGroups',
128 'mHash',
129 'mId',
130 'mName',
131 'mNewpassword',
132 'mNewtalk',
133 'mOptions',
134 'mPassword',
135 'mRealName',
136 'mRegistration',
137 'mRights',
138 'mToken',
139 'mTouched',
140 'mVersion',
145 * Get username given an id.
146 * @param integer $id Database user id
147 * @return string Nickname of a user
148 * @static
150 function whoIs( $id ) {
151 $dbr =& wfGetDB( DB_SLAVE );
152 return $dbr->selectField( 'user', 'user_name', array( 'user_id' => $id ), 'User::whoIs' );
156 * Get real username given an id.
157 * @param integer $id Database user id
158 * @return string Realname of a user
159 * @static
161 function whoIsReal( $id ) {
162 $dbr =& wfGetDB( DB_SLAVE );
163 return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ), 'User::whoIsReal' );
167 * Get database id given a user name
168 * @param string $name Nickname of a user
169 * @return integer|null Database user id (null: if non existent
170 * @static
172 function idFromName( $name ) {
173 $fname = "User::idFromName";
175 $nt = Title::newFromText( $name );
176 if( is_null( $nt ) ) {
177 # Illegal name
178 return null;
180 $dbr =& wfGetDB( DB_SLAVE );
181 $s = $dbr->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), $fname );
183 if ( $s === false ) {
184 return 0;
185 } else {
186 return $s->user_id;
191 * does the string match an anonymous IPv4 address?
193 * Note: We match \d{1,3}\.\d{1,3}\.\d{1,3}\.xxx as an anonymous IP
194 * address because the usemod software would "cloak" anonymous IP
195 * addresses like this, if we allowed accounts like this to be created
196 * new users could get the old edits of these anonymous users.
198 * @bug 3631
200 * @static
201 * @param string $name Nickname of a user
202 * @return bool
204 function isIP( $name ) {
205 return preg_match("/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/",$name);
206 /*return preg_match("/^
207 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
208 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
209 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
210 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))
211 $/x", $name);*/
215 * Is the input a valid username?
217 * Checks if the input is a valid username, we don't want an empty string,
218 * an IP address, anything that containins slashes (would mess up subpages),
219 * is longer than the maximum allowed username size or doesn't begin with
220 * a capital letter.
222 * @param string $name
223 * @return bool
224 * @static
226 function isValidUserName( $name ) {
227 global $wgContLang, $wgMaxNameChars;
229 if ( $name == ''
230 || User::isIP( $name )
231 || strpos( $name, '/' ) !== false
232 || strlen( $name ) > $wgMaxNameChars
233 || $name != $wgContLang->ucfirst( $name ) )
234 return false;
236 // Ensure that the name can't be misresolved as a different title,
237 // such as with extra namespace keys at the start.
238 $parsed = Title::newFromText( $name );
239 if( is_null( $parsed )
240 || $parsed->getNamespace()
241 || strcmp( $name, $parsed->getPrefixedText() ) )
242 return false;
244 // Check an additional blacklist of troublemaker characters.
245 // Should these be merged into the title char list?
246 $unicodeBlacklist = '/[' .
247 '\x{0080}-\x{009f}' . # iso-8859-1 control chars
248 '\x{00a0}' . # non-breaking space
249 '\x{2000}-\x{200f}' . # various whitespace
250 '\x{2028}-\x{202f}' . # breaks and control chars
251 '\x{3000}' . # ideographic space
252 '\x{e000}-\x{f8ff}' . # private use
253 ']/u';
254 if( preg_match( $unicodeBlacklist, $name ) ) {
255 return false;
258 return true;
262 * Is the input a valid password?
264 * @param string $password
265 * @return bool
266 * @static
268 function isValidPassword( $password ) {
269 global $wgMinimalPasswordLength;
270 return strlen( $password ) >= $wgMinimalPasswordLength;
274 * Does the string match roughly an email address ?
276 * There used to be a regular expression here, it got removed because it
277 * rejected valid addresses. Actually just check if there is '@' somewhere
278 * in the given address.
280 * @todo Check for RFC 2822 compilance
281 * @bug 959
283 * @param string $addr email address
284 * @static
285 * @return bool
287 function isValidEmailAddr ( $addr ) {
288 return ( trim( $addr ) != '' ) &&
289 (false !== strpos( $addr, '@' ) );
293 * Count the number of edits of a user
295 * @param int $uid The user ID to check
296 * @return int
298 function edits( $uid ) {
299 $fname = 'User::edits';
301 $dbr =& wfGetDB( DB_SLAVE );
302 return $dbr->selectField(
303 'revision', 'count(*)',
304 array( 'rev_user' => $uid ),
305 $fname
310 * probably return a random password
311 * @return string probably a random password
312 * @static
313 * @todo Check what is doing really [AV]
315 function randomPassword() {
316 global $wgMinimalPasswordLength;
317 $pwchars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz';
318 $l = strlen( $pwchars ) - 1;
320 $pwlength = max( 7, $wgMinimalPasswordLength );
321 $digit = mt_rand(0, $pwlength - 1);
322 $np = '';
323 for ( $i = 0; $i < $pwlength; $i++ ) {
324 $np .= $i == $digit ? chr( mt_rand(48, 57) ) : $pwchars{ mt_rand(0, $l)};
326 return $np;
330 * Set properties to default
331 * Used at construction. It will load per language default settings only
332 * if we have an available language object.
334 function loadDefaults() {
335 static $n=0;
336 $n++;
337 $fname = 'User::loadDefaults' . $n;
338 wfProfileIn( $fname );
340 global $wgCookiePrefix;
341 global $wgNamespacesToBeSearchedDefault;
343 $this->mId = 0;
344 $this->mNewtalk = -1;
345 $this->mName = false;
346 $this->mRealName = $this->mEmail = '';
347 $this->mEmailAuthenticated = null;
348 $this->mPassword = $this->mNewpassword = '';
349 $this->mRights = array();
350 $this->mGroups = array();
351 $this->mOptions = User::getDefaultOptions();
353 foreach( $wgNamespacesToBeSearchedDefault as $nsnum => $val ) {
354 $this->mOptions['searchNs'.$nsnum] = $val;
356 unset( $this->mSkin );
357 $this->mDataLoaded = false;
358 $this->mBlockedby = -1; # Unset
359 $this->setToken(); # Random
360 $this->mHash = false;
362 if ( isset( $_COOKIE[$wgCookiePrefix.'LoggedOut'] ) ) {
363 $this->mTouched = wfTimestamp( TS_MW, $_COOKIE[$wgCookiePrefix.'LoggedOut'] );
365 else {
366 $this->mTouched = '0'; # Allow any pages to be cached
369 $this->mRegistration = wfTimestamp( TS_MW );
371 wfProfileOut( $fname );
375 * Combine the language default options with any site-specific options
376 * and add the default language variants.
378 * @return array
379 * @static
380 * @private
382 function getDefaultOptions() {
384 * Site defaults will override the global/language defaults
386 global $wgContLang, $wgDefaultUserOptions;
387 $defOpt = $wgDefaultUserOptions + $wgContLang->getDefaultUserOptions();
390 * default language setting
392 $variant = $wgContLang->getPreferredVariant();
393 $defOpt['variant'] = $variant;
394 $defOpt['language'] = $variant;
396 return $defOpt;
400 * Get a given default option value.
402 * @param string $opt
403 * @return string
404 * @static
405 * @public
407 function getDefaultOption( $opt ) {
408 $defOpts = User::getDefaultOptions();
409 if( isset( $defOpts[$opt] ) ) {
410 return $defOpts[$opt];
411 } else {
412 return '';
417 * Get blocking information
418 * @private
419 * @param bool $bFromSlave Specify whether to check slave or master. To improve performance,
420 * non-critical checks are done against slaves. Check when actually saving should be done against
421 * master.
423 function getBlockedStatus( $bFromSlave = true ) {
424 global $wgEnableSorbs, $wgProxyWhitelist;
426 if ( -1 != $this->mBlockedby ) {
427 wfDebug( "User::getBlockedStatus: already loaded.\n" );
428 return;
431 $fname = 'User::getBlockedStatus';
432 wfProfileIn( $fname );
433 wfDebug( "$fname: checking...\n" );
435 $this->mBlockedby = 0;
436 $ip = wfGetIP();
438 # User/IP blocking
439 $block = new Block();
440 $block->fromMaster( !$bFromSlave );
441 if ( $block->load( $ip , $this->mId ) ) {
442 wfDebug( "$fname: Found block.\n" );
443 $this->mBlockedby = $block->mBy;
444 $this->mBlockreason = $block->mReason;
445 if ( $this->isLoggedIn() ) {
446 $this->spreadBlock();
448 } else {
449 wfDebug( "$fname: No block.\n" );
452 # Proxy blocking
453 # FIXME ? proxyunbannable is to deprecate the old isSysop()
454 if ( !$this->isAllowed('proxyunbannable') && !in_array( $ip, $wgProxyWhitelist ) ) {
456 # Local list
457 if ( wfIsLocallyBlockedProxy( $ip ) ) {
458 $this->mBlockedby = wfMsg( 'proxyblocker' );
459 $this->mBlockreason = wfMsg( 'proxyblockreason' );
462 # DNSBL
463 if ( !$this->mBlockedby && $wgEnableSorbs && !$this->getID() ) {
464 if ( $this->inSorbsBlacklist( $ip ) ) {
465 $this->mBlockedby = wfMsg( 'sorbs' );
466 $this->mBlockreason = wfMsg( 'sorbsreason' );
471 # Extensions
472 wfRunHooks( 'GetBlockedStatus', array( &$this ) );
474 wfProfileOut( $fname );
477 function inSorbsBlacklist( $ip ) {
478 global $wgEnableSorbs;
479 return $wgEnableSorbs &&
480 $this->inDnsBlacklist( $ip, 'http.dnsbl.sorbs.net.' );
483 function inOpmBlacklist( $ip ) {
484 global $wgEnableOpm;
485 return $wgEnableOpm &&
486 $this->inDnsBlacklist( $ip, 'opm.blitzed.org.' );
489 function inDnsBlacklist( $ip, $base ) {
490 $fname = 'User::inDnsBlacklist';
491 wfProfileIn( $fname );
493 $found = false;
494 $host = '';
496 if ( preg_match( '/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $ip, $m ) ) {
497 # Make hostname
498 for ( $i=4; $i>=1; $i-- ) {
499 $host .= $m[$i] . '.';
501 $host .= $base;
503 # Send query
504 $ipList = gethostbynamel( $host );
506 if ( $ipList ) {
507 wfDebug( "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" );
508 $found = true;
509 } else {
510 wfDebug( "Requested $host, not found in $base.\n" );
514 wfProfileOut( $fname );
515 return $found;
519 * Primitive rate limits: enforce maximum actions per time period
520 * to put a brake on flooding.
522 * Note: when using a shared cache like memcached, IP-address
523 * last-hit counters will be shared across wikis.
525 * @return bool true if a rate limiter was tripped
526 * @public
528 function pingLimiter( $action='edit' ) {
529 global $wgRateLimits, $wgRateLimitsExcludedGroups;
530 if( !isset( $wgRateLimits[$action] ) ) {
531 return false;
534 # Some groups shouldn't trigger the ping limiter, ever
535 foreach( $this->getGroups() as $group ) {
536 if( array_search( $group, $wgRateLimitsExcludedGroups ) !== false )
537 return false;
540 global $wgMemc, $wgDBname, $wgRateLimitLog;
541 $fname = 'User::pingLimiter';
542 wfProfileIn( $fname );
544 $limits = $wgRateLimits[$action];
545 $keys = array();
546 $id = $this->getId();
547 $ip = wfGetIP();
549 if( isset( $limits['anon'] ) && $id == 0 ) {
550 $keys["$wgDBname:limiter:$action:anon"] = $limits['anon'];
553 if( isset( $limits['user'] ) && $id != 0 ) {
554 $keys["$wgDBname:limiter:$action:user:$id"] = $limits['user'];
556 if( $this->isNewbie() ) {
557 if( isset( $limits['newbie'] ) && $id != 0 ) {
558 $keys["$wgDBname:limiter:$action:user:$id"] = $limits['newbie'];
560 if( isset( $limits['ip'] ) ) {
561 $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip'];
563 if( isset( $limits['subnet'] ) && preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
564 $subnet = $matches[1];
565 $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
569 $triggered = false;
570 foreach( $keys as $key => $limit ) {
571 list( $max, $period ) = $limit;
572 $summary = "(limit $max in {$period}s)";
573 $count = $wgMemc->get( $key );
574 if( $count ) {
575 if( $count > $max ) {
576 wfDebug( "$fname: tripped! $key at $count $summary\n" );
577 if( $wgRateLimitLog ) {
578 @error_log( wfTimestamp( TS_MW ) . ' ' . $wgDBname . ': ' . $this->getName() . " tripped $key at $count $summary\n", 3, $wgRateLimitLog );
580 $triggered = true;
581 } else {
582 wfDebug( "$fname: ok. $key at $count $summary\n" );
584 } else {
585 wfDebug( "$fname: adding record for $key $summary\n" );
586 $wgMemc->add( $key, 1, intval( $period ) );
588 $wgMemc->incr( $key );
591 wfProfileOut( $fname );
592 return $triggered;
596 * Check if user is blocked
597 * @return bool True if blocked, false otherwise
599 function isBlocked( $bFromSlave = true ) { // hacked from false due to horrible probs on site
600 wfDebug( "User::isBlocked: enter\n" );
601 $this->getBlockedStatus( $bFromSlave );
602 return $this->mBlockedby !== 0;
606 * Check if user is blocked from editing a particular article
608 function isBlockedFrom( $title, $bFromSlave = false ) {
609 global $wgBlockAllowsUTEdit;
610 $fname = 'User::isBlockedFrom';
611 wfProfileIn( $fname );
612 wfDebug( "$fname: enter\n" );
614 if ( $wgBlockAllowsUTEdit && $title->getText() === $this->getName() &&
615 $title->getNamespace() == NS_USER_TALK )
617 $blocked = false;
618 wfDebug( "$fname: self-talk page, ignoring any blocks\n" );
619 } else {
620 wfDebug( "$fname: asking isBlocked()\n" );
621 $blocked = $this->isBlocked( $bFromSlave );
623 wfProfileOut( $fname );
624 return $blocked;
628 * Get name of blocker
629 * @return string name of blocker
631 function blockedBy() {
632 $this->getBlockedStatus();
633 return $this->mBlockedby;
637 * Get blocking reason
638 * @return string Blocking reason
640 function blockedFor() {
641 $this->getBlockedStatus();
642 return $this->mBlockreason;
646 * Initialise php session
648 function SetupSession() {
649 global $wgSessionsInMemcached, $wgCookiePath, $wgCookieDomain;
650 if( $wgSessionsInMemcached ) {
651 require_once( 'MemcachedSessions.php' );
652 } elseif( 'files' != ini_get( 'session.save_handler' ) ) {
653 # If it's left on 'user' or another setting from another
654 # application, it will end up failing. Try to recover.
655 ini_set ( 'session.save_handler', 'files' );
657 session_set_cookie_params( 0, $wgCookiePath, $wgCookieDomain );
658 session_cache_limiter( 'private, must-revalidate' );
659 @session_start();
663 * Create a new user object using data from session
664 * @static
666 function loadFromSession() {
667 global $wgMemc, $wgDBname, $wgCookiePrefix;
669 if ( isset( $_SESSION['wsUserID'] ) ) {
670 if ( 0 != $_SESSION['wsUserID'] ) {
671 $sId = $_SESSION['wsUserID'];
672 } else {
673 return new User();
675 } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserID"] ) ) {
676 $sId = intval( $_COOKIE["{$wgCookiePrefix}UserID"] );
677 $_SESSION['wsUserID'] = $sId;
678 } else {
679 return new User();
681 if ( isset( $_SESSION['wsUserName'] ) ) {
682 $sName = $_SESSION['wsUserName'];
683 } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserName"] ) ) {
684 $sName = $_COOKIE["{$wgCookiePrefix}UserName"];
685 $_SESSION['wsUserName'] = $sName;
686 } else {
687 return new User();
690 $passwordCorrect = FALSE;
691 $user = $wgMemc->get( $key = "$wgDBname:user:id:$sId" );
692 if( !is_object( $user ) || $user->mVersion < MW_USER_VERSION ) {
693 # Expire old serialized objects; they may be corrupt.
694 $user = false;
696 if($makenew = !$user) {
697 wfDebug( "User::loadFromSession() unable to load from memcached\n" );
698 $user = new User();
699 $user->mId = $sId;
700 $user->loadFromDatabase();
701 } else {
702 wfDebug( "User::loadFromSession() got from cache!\n" );
705 if ( isset( $_SESSION['wsToken'] ) ) {
706 $passwordCorrect = $_SESSION['wsToken'] == $user->mToken;
707 } else if ( isset( $_COOKIE["{$wgCookiePrefix}Token"] ) ) {
708 $passwordCorrect = $user->mToken == $_COOKIE["{$wgCookiePrefix}Token"];
709 } else {
710 return new User(); # Can't log in from session
713 if ( ( $sName == $user->mName ) && $passwordCorrect ) {
714 if($makenew) {
715 if($wgMemc->set( $key, $user ))
716 wfDebug( "User::loadFromSession() successfully saved user\n" );
717 else
718 wfDebug( "User::loadFromSession() unable to save to memcached\n" );
720 return $user;
722 return new User(); # Can't log in from session
726 * Load a user from the database
728 function loadFromDatabase() {
729 $fname = "User::loadFromDatabase";
731 # Counter-intuitive, breaks various things, use User::setLoaded() if you want to suppress
732 # loading in a command line script, don't assume all command line scripts need it like this
733 #if ( $this->mDataLoaded || $wgCommandLineMode ) {
734 if ( $this->mDataLoaded ) {
735 return;
738 # Paranoia
739 $this->mId = intval( $this->mId );
741 /** Anonymous user */
742 if( !$this->mId ) {
743 /** Get rights */
744 $this->mRights = $this->getGroupPermissions( array( '*' ) );
745 $this->mDataLoaded = true;
746 return;
747 } # the following stuff is for non-anonymous users only
749 $dbr =& wfGetDB( DB_SLAVE );
750 $s = $dbr->selectRow( 'user', array( 'user_name','user_password','user_newpassword','user_email',
751 'user_email_authenticated',
752 'user_real_name','user_options','user_touched', 'user_token', 'user_registration' ),
753 array( 'user_id' => $this->mId ), $fname );
755 if ( $s !== false ) {
756 $this->mName = $s->user_name;
757 $this->mEmail = $s->user_email;
758 $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $s->user_email_authenticated );
759 $this->mRealName = $s->user_real_name;
760 $this->mPassword = $s->user_password;
761 $this->mNewpassword = $s->user_newpassword;
762 $this->decodeOptions( $s->user_options );
763 $this->mTouched = wfTimestamp(TS_MW,$s->user_touched);
764 $this->mToken = $s->user_token;
765 $this->mRegistration = wfTimestampOrNull( TS_MW, $s->user_registration );
767 $res = $dbr->select( 'user_groups',
768 array( 'ug_group' ),
769 array( 'ug_user' => $this->mId ),
770 $fname );
771 $this->mGroups = array();
772 while( $row = $dbr->fetchObject( $res ) ) {
773 $this->mGroups[] = $row->ug_group;
775 $implicitGroups = array( '*', 'user' );
777 global $wgAutoConfirmAge;
778 $accountAge = time() - wfTimestampOrNull( TS_UNIX, $this->mRegistration );
779 if( $accountAge >= $wgAutoConfirmAge ) {
780 $implicitGroups[] = 'autoconfirmed';
783 $effectiveGroups = array_merge( $implicitGroups, $this->mGroups );
784 $this->mRights = $this->getGroupPermissions( $effectiveGroups );
787 $this->mDataLoaded = true;
790 function getID() { return $this->mId; }
791 function setID( $v ) {
792 $this->mId = $v;
793 $this->mDataLoaded = false;
796 function getName() {
797 $this->loadFromDatabase();
798 if ( $this->mName === false ) {
799 $this->mName = wfGetIP();
801 return $this->mName;
804 function setName( $str ) {
805 $this->loadFromDatabase();
806 $this->mName = $str;
811 * Return the title dbkey form of the name, for eg user pages.
812 * @return string
813 * @public
815 function getTitleKey() {
816 return str_replace( ' ', '_', $this->getName() );
819 function getNewtalk() {
820 $this->loadFromDatabase();
822 # Load the newtalk status if it is unloaded (mNewtalk=-1)
823 if( $this->mNewtalk === -1 ) {
824 $this->mNewtalk = false; # reset talk page status
826 # Check memcached separately for anons, who have no
827 # entire User object stored in there.
828 if( !$this->mId ) {
829 global $wgDBname, $wgMemc;
830 $key = "$wgDBname:newtalk:ip:" . $this->getName();
831 $newtalk = $wgMemc->get( $key );
832 if( is_integer( $newtalk ) ) {
833 $this->mNewtalk = (bool)$newtalk;
834 } else {
835 $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName() );
836 $wgMemc->set( $key, $this->mNewtalk, time() ); // + 1800 );
838 } else {
839 $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
843 return (bool)$this->mNewtalk;
847 * Return the talk page(s) this user has new messages on.
849 function getNewMessageLinks() {
850 global $wgDBname;
851 $talks = array();
852 if (!wfRunHooks('UserRetrieveNewTalks', array(&$this, &$talks)))
853 return $talks;
855 if (!$this->getNewtalk())
856 return array();
857 $up = $this->getUserPage();
858 $utp = $up->getTalkPage();
859 return array(array("wiki" => $wgDBname, "link" => $utp->getLocalURL()));
864 * Perform a user_newtalk check on current slaves; if the memcached data
865 * is funky we don't want newtalk state to get stuck on save, as that's
866 * damn annoying.
868 * @param string $field
869 * @param mixed $id
870 * @return bool
871 * @private
873 function checkNewtalk( $field, $id ) {
874 $fname = 'User::checkNewtalk';
875 $dbr =& wfGetDB( DB_SLAVE );
876 $ok = $dbr->selectField( 'user_newtalk', $field,
877 array( $field => $id ), $fname );
878 return $ok !== false;
882 * Add or update the
883 * @param string $field
884 * @param mixed $id
885 * @private
887 function updateNewtalk( $field, $id ) {
888 $fname = 'User::updateNewtalk';
889 if( $this->checkNewtalk( $field, $id ) ) {
890 wfDebug( "$fname already set ($field, $id), ignoring\n" );
891 return false;
893 $dbw =& wfGetDB( DB_MASTER );
894 $dbw->insert( 'user_newtalk',
895 array( $field => $id ),
896 $fname,
897 'IGNORE' );
898 wfDebug( "$fname: set on ($field, $id)\n" );
899 return true;
903 * Clear the new messages flag for the given user
904 * @param string $field
905 * @param mixed $id
906 * @private
908 function deleteNewtalk( $field, $id ) {
909 $fname = 'User::deleteNewtalk';
910 if( !$this->checkNewtalk( $field, $id ) ) {
911 wfDebug( "$fname: already gone ($field, $id), ignoring\n" );
912 return false;
914 $dbw =& wfGetDB( DB_MASTER );
915 $dbw->delete( 'user_newtalk',
916 array( $field => $id ),
917 $fname );
918 wfDebug( "$fname: killed on ($field, $id)\n" );
919 return true;
923 * Update the 'You have new messages!' status.
924 * @param bool $val
926 function setNewtalk( $val ) {
927 if( wfReadOnly() ) {
928 return;
931 $this->loadFromDatabase();
932 $this->mNewtalk = $val;
934 $fname = 'User::setNewtalk';
936 if( $this->isAnon() ) {
937 $field = 'user_ip';
938 $id = $this->getName();
939 } else {
940 $field = 'user_id';
941 $id = $this->getId();
944 if( $val ) {
945 $changed = $this->updateNewtalk( $field, $id );
946 } else {
947 $changed = $this->deleteNewtalk( $field, $id );
950 if( $changed ) {
951 if( $this->isAnon() ) {
952 // Anons have a separate memcached space, since
953 // user records aren't kept for them.
954 global $wgDBname, $wgMemc;
955 $key = "$wgDBname:newtalk:ip:$val";
956 $wgMemc->set( $key, $val ? 1 : 0 );
957 } else {
958 if( $val ) {
959 // Make sure the user page is watched, so a notification
960 // will be sent out if enabled.
961 $this->addWatch( $this->getTalkPage() );
964 $this->invalidateCache();
965 $this->saveSettings();
969 function invalidateCache() {
970 global $wgClockSkewFudge;
971 $this->loadFromDatabase();
972 $this->mTouched = wfTimestamp(TS_MW, time() + $wgClockSkewFudge );
973 # Don't forget to save the options after this or
974 # it won't take effect!
977 function validateCache( $timestamp ) {
978 $this->loadFromDatabase();
979 return ($timestamp >= $this->mTouched);
983 * Encrypt a password.
984 * It can eventuall salt a password @see User::addSalt()
985 * @param string $p clear Password.
986 * @return string Encrypted password.
988 function encryptPassword( $p ) {
989 return wfEncryptPassword( $this->mId, $p );
992 # Set the password and reset the random token
993 function setPassword( $str ) {
994 $this->loadFromDatabase();
995 $this->setToken();
996 $this->mPassword = $this->encryptPassword( $str );
997 $this->mNewpassword = '';
1000 # Set the random token (used for persistent authentication)
1001 function setToken( $token = false ) {
1002 global $wgSecretKey, $wgProxyKey, $wgDBname;
1003 if ( !$token ) {
1004 if ( $wgSecretKey ) {
1005 $key = $wgSecretKey;
1006 } elseif ( $wgProxyKey ) {
1007 $key = $wgProxyKey;
1008 } else {
1009 $key = microtime();
1011 $this->mToken = md5( $key . mt_rand( 0, 0x7fffffff ) . $wgDBname . $this->mId );
1012 } else {
1013 $this->mToken = $token;
1018 function setCookiePassword( $str ) {
1019 $this->loadFromDatabase();
1020 $this->mCookiePassword = md5( $str );
1023 function setNewpassword( $str ) {
1024 $this->loadFromDatabase();
1025 $this->mNewpassword = $this->encryptPassword( $str );
1028 function getEmail() {
1029 $this->loadFromDatabase();
1030 return $this->mEmail;
1033 function getEmailAuthenticationTimestamp() {
1034 $this->loadFromDatabase();
1035 return $this->mEmailAuthenticated;
1038 function setEmail( $str ) {
1039 $this->loadFromDatabase();
1040 $this->mEmail = $str;
1043 function getRealName() {
1044 $this->loadFromDatabase();
1045 return $this->mRealName;
1048 function setRealName( $str ) {
1049 $this->loadFromDatabase();
1050 $this->mRealName = $str;
1054 * @param string $oname The option to check
1055 * @return string
1057 function getOption( $oname ) {
1058 $this->loadFromDatabase();
1059 if ( array_key_exists( $oname, $this->mOptions ) ) {
1060 return trim( $this->mOptions[$oname] );
1061 } else {
1062 return '';
1067 * @param string $oname The option to check
1068 * @return bool False if the option is not selected, true if it is
1070 function getBoolOption( $oname ) {
1071 return (bool)$this->getOption( $oname );
1075 * Get an option as an integer value from the source string.
1076 * @param string $oname The option to check
1077 * @param int $default Optional value to return if option is unset/blank.
1078 * @return int
1080 function getIntOption( $oname, $default=0 ) {
1081 $val = $this->getOption( $oname );
1082 if( $val == '' ) {
1083 $val = $default;
1085 return intval( $val );
1088 function setOption( $oname, $val ) {
1089 $this->loadFromDatabase();
1090 if ( $oname == 'skin' ) {
1091 # Clear cached skin, so the new one displays immediately in Special:Preferences
1092 unset( $this->mSkin );
1094 // Filter out any newlines that may have passed through input validation.
1095 // Newlines are used to separate items in the options blob.
1096 $val = str_replace( "\r\n", "\n", $val );
1097 $val = str_replace( "\r", "\n", $val );
1098 $val = str_replace( "\n", " ", $val );
1099 $this->mOptions[$oname] = $val;
1100 $this->invalidateCache();
1103 function getRights() {
1104 $this->loadFromDatabase();
1105 return $this->mRights;
1109 * Get the list of explicit group memberships this user has.
1110 * The implicit * and user groups are not included.
1111 * @return array of strings
1113 function getGroups() {
1114 $this->loadFromDatabase();
1115 return $this->mGroups;
1119 * Get the list of implicit group memberships this user has.
1120 * This includes all explicit groups, plus 'user' if logged in
1121 * and '*' for all accounts.
1122 * @return array of strings
1124 function getEffectiveGroups() {
1125 $base = array( '*' );
1126 if( $this->isLoggedIn() ) {
1127 $base[] = 'user';
1129 return array_merge( $base, $this->getGroups() );
1133 * Add the user to the given group.
1134 * This takes immediate effect.
1135 * @string $group
1137 function addGroup( $group ) {
1138 $dbw =& wfGetDB( DB_MASTER );
1139 $dbw->insert( 'user_groups',
1140 array(
1141 'ug_user' => $this->getID(),
1142 'ug_group' => $group,
1144 'User::addGroup',
1145 array( 'IGNORE' ) );
1147 $this->mGroups = array_merge( $this->mGroups, array( $group ) );
1148 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups() );
1150 $this->invalidateCache();
1151 $this->saveSettings();
1155 * Remove the user from the given group.
1156 * This takes immediate effect.
1157 * @string $group
1159 function removeGroup( $group ) {
1160 $dbw =& wfGetDB( DB_MASTER );
1161 $dbw->delete( 'user_groups',
1162 array(
1163 'ug_user' => $this->getID(),
1164 'ug_group' => $group,
1166 'User::removeGroup' );
1168 $this->mGroups = array_diff( $this->mGroups, array( $group ) );
1169 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups() );
1171 $this->invalidateCache();
1172 $this->saveSettings();
1177 * A more legible check for non-anonymousness.
1178 * Returns true if the user is not an anonymous visitor.
1180 * @return bool
1182 function isLoggedIn() {
1183 return( $this->getID() != 0 );
1187 * A more legible check for anonymousness.
1188 * Returns true if the user is an anonymous visitor.
1190 * @return bool
1192 function isAnon() {
1193 return !$this->isLoggedIn();
1197 * Deprecated in 1.6, die in 1.7, to be removed in 1.8
1198 * @deprecated
1200 function isSysop() {
1201 wfDebugDieBacktrace( "Call to deprecated (v1.7) User::isSysop() method\n" );
1202 #return $this->isAllowed( 'protect' );
1206 * Deprecated in 1.6, die in 1.7, to be removed in 1.8
1207 * @deprecated
1209 function isDeveloper() {
1210 wfDebugDieBacktrace( "Call to deprecated (v1.7) User::isDeveloper() method\n" );
1211 #return $this->isAllowed( 'siteadmin' );
1215 * Deprecated in 1.6, die in 1.7, to be removed in 1.8
1216 * @deprecated
1218 function isBureaucrat() {
1219 wfDebugDieBacktrace( "Call to deprecated (v1.7) User::isBureaucrat() method\n" );
1220 #return $this->isAllowed( 'makesysop' );
1224 * Whether the user is a bot
1225 * @todo need to be migrated to the new user level management sytem
1227 function isBot() {
1228 $this->loadFromDatabase();
1229 return in_array( 'bot', $this->mRights );
1233 * Check if user is allowed to access a feature / make an action
1234 * @param string $action Action to be checked (see $wgAvailableRights in Defines.php for possible actions).
1235 * @return boolean True: action is allowed, False: action should not be allowed
1237 function isAllowed($action='') {
1238 if ( $action === '' )
1239 // In the spirit of DWIM
1240 return true;
1242 $this->loadFromDatabase();
1243 return in_array( $action , $this->mRights );
1247 * Load a skin if it doesn't exist or return it
1248 * @todo FIXME : need to check the old failback system [AV]
1250 function &getSkin() {
1251 global $IP, $wgRequest;
1252 if ( ! isset( $this->mSkin ) ) {
1253 $fname = 'User::getSkin';
1254 wfProfileIn( $fname );
1256 # get the user skin
1257 $userSkin = $this->getOption( 'skin' );
1258 $userSkin = $wgRequest->getVal('useskin', $userSkin);
1260 $this->mSkin =& Skin::newFromKey( $userSkin );
1261 wfProfileOut( $fname );
1263 return $this->mSkin;
1266 /**#@+
1267 * @param string $title Article title to look at
1271 * Check watched status of an article
1272 * @return bool True if article is watched
1274 function isWatched( $title ) {
1275 $wl = WatchedItem::fromUserTitle( $this, $title );
1276 return $wl->isWatched();
1280 * Watch an article
1282 function addWatch( $title ) {
1283 $wl = WatchedItem::fromUserTitle( $this, $title );
1284 $wl->addWatch();
1285 $this->invalidateCache();
1289 * Stop watching an article
1291 function removeWatch( $title ) {
1292 $wl = WatchedItem::fromUserTitle( $this, $title );
1293 $wl->removeWatch();
1294 $this->invalidateCache();
1298 * Clear the user's notification timestamp for the given title.
1299 * If e-notif e-mails are on, they will receive notification mails on
1300 * the next change of the page if it's watched etc.
1302 function clearNotification( &$title ) {
1303 global $wgUser, $wgUseEnotif;
1306 if ($title->getNamespace() == NS_USER_TALK &&
1307 $title->getText() == $this->getName() ) {
1308 if (!wfRunHooks('UserClearNewTalkNotification', array(&$this)))
1309 return;
1310 $this->setNewtalk( false );
1313 if( !$wgUseEnotif ) {
1314 return;
1317 if( $this->isAnon() ) {
1318 // Nothing else to do...
1319 return;
1322 // Only update the timestamp if the page is being watched.
1323 // The query to find out if it is watched is cached both in memcached and per-invocation,
1324 // and when it does have to be executed, it can be on a slave
1325 // If this is the user's newtalk page, we always update the timestamp
1326 if ($title->getNamespace() == NS_USER_TALK &&
1327 $title->getText() == $wgUser->getName())
1329 $watched = true;
1330 } elseif ( $this->getID() == $wgUser->getID() ) {
1331 $watched = $title->userIsWatching();
1332 } else {
1333 $watched = true;
1336 // If the page is watched by the user (or may be watched), update the timestamp on any
1337 // any matching rows
1338 if ( $watched ) {
1339 $dbw =& wfGetDB( DB_MASTER );
1340 $success = $dbw->update( 'watchlist',
1341 array( /* SET */
1342 'wl_notificationtimestamp' => NULL
1343 ), array( /* WHERE */
1344 'wl_title' => $title->getDBkey(),
1345 'wl_namespace' => $title->getNamespace(),
1346 'wl_user' => $this->getID()
1347 ), 'User::clearLastVisited'
1352 /**#@-*/
1355 * Resets all of the given user's page-change notification timestamps.
1356 * If e-notif e-mails are on, they will receive notification mails on
1357 * the next change of any watched page.
1359 * @param int $currentUser user ID number
1360 * @public
1362 function clearAllNotifications( $currentUser ) {
1363 global $wgUseEnotif;
1364 if ( !$wgUseEnotif ) {
1365 $this->setNewtalk( false );
1366 return;
1368 if( $currentUser != 0 ) {
1370 $dbw =& wfGetDB( DB_MASTER );
1371 $success = $dbw->update( 'watchlist',
1372 array( /* SET */
1373 'wl_notificationtimestamp' => 0
1374 ), array( /* WHERE */
1375 'wl_user' => $currentUser
1376 ), 'UserMailer::clearAll'
1379 # we also need to clear here the "you have new message" notification for the own user_talk page
1380 # This is cleared one page view later in Article::viewUpdates();
1385 * @private
1386 * @return string Encoding options
1388 function encodeOptions() {
1389 $a = array();
1390 foreach ( $this->mOptions as $oname => $oval ) {
1391 array_push( $a, $oname.'='.$oval );
1393 $s = implode( "\n", $a );
1394 return $s;
1398 * @private
1400 function decodeOptions( $str ) {
1401 $a = explode( "\n", $str );
1402 foreach ( $a as $s ) {
1403 if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
1404 $this->mOptions[$m[1]] = $m[2];
1409 function setCookies() {
1410 global $wgCookieExpiration, $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookiePrefix;
1411 if ( 0 == $this->mId ) return;
1412 $this->loadFromDatabase();
1413 $exp = time() + $wgCookieExpiration;
1415 $_SESSION['wsUserID'] = $this->mId;
1416 setcookie( $wgCookiePrefix.'UserID', $this->mId, $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1418 $_SESSION['wsUserName'] = $this->getName();
1419 setcookie( $wgCookiePrefix.'UserName', $this->getName(), $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1421 $_SESSION['wsToken'] = $this->mToken;
1422 if ( 1 == $this->getOption( 'rememberpassword' ) ) {
1423 setcookie( $wgCookiePrefix.'Token', $this->mToken, $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1424 } else {
1425 setcookie( $wgCookiePrefix.'Token', '', time() - 3600 );
1430 * Logout user
1431 * It will clean the session cookie
1433 function logout() {
1434 global $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookiePrefix;
1435 $this->loadDefaults();
1436 $this->setLoaded( true );
1438 $_SESSION['wsUserID'] = 0;
1440 setcookie( $wgCookiePrefix.'UserID', '', time() - 3600, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1441 setcookie( $wgCookiePrefix.'Token', '', time() - 3600, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1443 # Remember when user logged out, to prevent seeing cached pages
1444 setcookie( $wgCookiePrefix.'LoggedOut', wfTimestampNow(), time() + 86400, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1448 * Save object settings into database
1450 function saveSettings() {
1451 global $wgMemc, $wgDBname;
1452 $fname = 'User::saveSettings';
1454 if ( wfReadOnly() ) { return; }
1455 if ( 0 == $this->mId ) { return; }
1457 $dbw =& wfGetDB( DB_MASTER );
1458 $dbw->update( 'user',
1459 array( /* SET */
1460 'user_name' => $this->mName,
1461 'user_password' => $this->mPassword,
1462 'user_newpassword' => $this->mNewpassword,
1463 'user_real_name' => $this->mRealName,
1464 'user_email' => $this->mEmail,
1465 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
1466 'user_options' => $this->encodeOptions(),
1467 'user_touched' => $dbw->timestamp($this->mTouched),
1468 'user_token' => $this->mToken
1469 ), array( /* WHERE */
1470 'user_id' => $this->mId
1471 ), $fname
1473 $wgMemc->delete( "$wgDBname:user:id:$this->mId" );
1478 * Checks if a user with the given name exists, returns the ID
1480 function idForName() {
1481 $fname = 'User::idForName';
1483 $gotid = 0;
1484 $s = trim( $this->getName() );
1485 if ( 0 == strcmp( '', $s ) ) return 0;
1487 $dbr =& wfGetDB( DB_SLAVE );
1488 $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), $fname );
1489 if ( $id === false ) {
1490 $id = 0;
1492 return $id;
1496 * Add user object to the database
1498 function addToDatabase() {
1499 $fname = 'User::addToDatabase';
1500 $dbw =& wfGetDB( DB_MASTER );
1501 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
1502 $dbw->insert( 'user',
1503 array(
1504 'user_id' => $seqVal,
1505 'user_name' => $this->mName,
1506 'user_password' => $this->mPassword,
1507 'user_newpassword' => $this->mNewpassword,
1508 'user_email' => $this->mEmail,
1509 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
1510 'user_real_name' => $this->mRealName,
1511 'user_options' => $this->encodeOptions(),
1512 'user_token' => $this->mToken,
1513 'user_registration' => $dbw->timestamp( $this->mRegistration ),
1514 ), $fname
1516 $this->mId = $dbw->insertId();
1519 function spreadBlock() {
1520 # If the (non-anonymous) user is blocked, this function will block any IP address
1521 # that they successfully log on from.
1522 $fname = 'User::spreadBlock';
1524 wfDebug( "User:spreadBlock()\n" );
1525 if ( $this->mId == 0 ) {
1526 return;
1529 $userblock = Block::newFromDB( '', $this->mId );
1530 if ( !$userblock->isValid() ) {
1531 return;
1534 # Check if this IP address is already blocked
1535 $ipblock = Block::newFromDB( wfGetIP() );
1536 if ( $ipblock->isValid() ) {
1537 # If the user is already blocked. Then check if the autoblock would
1538 # excede the user block. If it would excede, then do nothing, else
1539 # prolong block time
1540 if ($userblock->mExpiry &&
1541 ($userblock->mExpiry < Block::getAutoblockExpiry($ipblock->mTimestamp))) {
1542 return;
1544 # Just update the timestamp
1545 $ipblock->updateTimestamp();
1546 return;
1549 # Make a new block object with the desired properties
1550 wfDebug( "Autoblocking {$this->mName}@" . wfGetIP() . "\n" );
1551 $ipblock->mAddress = wfGetIP();
1552 $ipblock->mUser = 0;
1553 $ipblock->mBy = $userblock->mBy;
1554 $ipblock->mReason = wfMsg( 'autoblocker', $this->getName(), $userblock->mReason );
1555 $ipblock->mTimestamp = wfTimestampNow();
1556 $ipblock->mAuto = 1;
1557 # If the user is already blocked with an expiry date, we don't
1558 # want to pile on top of that!
1559 if($userblock->mExpiry) {
1560 $ipblock->mExpiry = min ( $userblock->mExpiry, Block::getAutoblockExpiry( $ipblock->mTimestamp ));
1561 } else {
1562 $ipblock->mExpiry = Block::getAutoblockExpiry( $ipblock->mTimestamp );
1565 # Insert it
1566 $ipblock->insert();
1571 * Generate a string which will be different for any combination of
1572 * user options which would produce different parser output.
1573 * This will be used as part of the hash key for the parser cache,
1574 * so users will the same options can share the same cached data
1575 * safely.
1577 * Extensions which require it should install 'PageRenderingHash' hook,
1578 * which will give them a chance to modify this key based on their own
1579 * settings.
1581 * @return string
1583 function getPageRenderingHash() {
1584 global $wgContLang;
1585 if( $this->mHash ){
1586 return $this->mHash;
1589 // stubthreshold is only included below for completeness,
1590 // it will always be 0 when this function is called by parsercache.
1592 $confstr = $this->getOption( 'math' );
1593 $confstr .= '!' . $this->getOption( 'stubthreshold' );
1594 $confstr .= '!' . $this->getOption( 'date' );
1595 $confstr .= '!' . ($this->getOption( 'numberheadings' ) ? '1' : '');
1596 $confstr .= '!' . $this->getOption( 'language' );
1597 $confstr .= '!' . $this->getOption( 'thumbsize' );
1598 // add in language specific options, if any
1599 $extra = $wgContLang->getExtraHashOptions();
1600 $confstr .= $extra;
1602 // Give a chance for extensions to modify the hash, if they have
1603 // extra options or other effects on the parser cache.
1604 wfRunHooks( 'PageRenderingHash', array( &$confstr ) );
1606 $this->mHash = $confstr;
1607 return $confstr;
1610 function isAllowedToCreateAccount() {
1611 return $this->isAllowed( 'createaccount' ) && !$this->isBlocked();
1615 * Set mDataLoaded, return previous value
1616 * Use this to prevent DB access in command-line scripts or similar situations
1618 function setLoaded( $loaded ) {
1619 return wfSetVar( $this->mDataLoaded, $loaded );
1623 * Get this user's personal page title.
1625 * @return Title
1626 * @public
1628 function getUserPage() {
1629 return Title::makeTitle( NS_USER, $this->getName() );
1633 * Get this user's talk page title.
1635 * @return Title
1636 * @public
1638 function getTalkPage() {
1639 $title = $this->getUserPage();
1640 return $title->getTalkPage();
1644 * @static
1646 function getMaxID() {
1647 static $res; // cache
1649 if ( isset( $res ) )
1650 return $res;
1651 else {
1652 $dbr =& wfGetDB( DB_SLAVE );
1653 return $res = $dbr->selectField( 'user', 'max(user_id)', false, 'User::getMaxID' );
1658 * Determine whether the user is a newbie. Newbies are either
1659 * anonymous IPs, or the most recently created accounts.
1660 * @return bool True if it is a newbie.
1662 function isNewbie() {
1663 return !$this->isAllowed( 'autoconfirmed' );
1667 * Check to see if the given clear-text password is one of the accepted passwords
1668 * @param string $password User password.
1669 * @return bool True if the given password is correct otherwise False.
1671 function checkPassword( $password ) {
1672 global $wgAuth, $wgMinimalPasswordLength;
1673 $this->loadFromDatabase();
1675 // Even though we stop people from creating passwords that
1676 // are shorter than this, doesn't mean people wont be able
1677 // to. Certain authentication plugins do NOT want to save
1678 // domain passwords in a mysql database, so we should
1679 // check this (incase $wgAuth->strict() is false).
1680 if( strlen( $password ) < $wgMinimalPasswordLength ) {
1681 return false;
1684 if( $wgAuth->authenticate( $this->getName(), $password ) ) {
1685 return true;
1686 } elseif( $wgAuth->strict() ) {
1687 /* Auth plugin doesn't allow local authentication */
1688 return false;
1690 $ep = $this->encryptPassword( $password );
1691 if ( 0 == strcmp( $ep, $this->mPassword ) ) {
1692 return true;
1693 } elseif ( ($this->mNewpassword != '') && (0 == strcmp( $ep, $this->mNewpassword )) ) {
1694 return true;
1695 } elseif ( function_exists( 'iconv' ) ) {
1696 # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
1697 # Check for this with iconv
1698 $cp1252hash = $this->encryptPassword( iconv( 'UTF-8', 'WINDOWS-1252', $password ) );
1699 if ( 0 == strcmp( $cp1252hash, $this->mPassword ) ) {
1700 return true;
1703 return false;
1707 * Initialize (if necessary) and return a session token value
1708 * which can be used in edit forms to show that the user's
1709 * login credentials aren't being hijacked with a foreign form
1710 * submission.
1712 * @param mixed $salt - Optional function-specific data for hash.
1713 * Use a string or an array of strings.
1714 * @return string
1715 * @public
1717 function editToken( $salt = '' ) {
1718 if( !isset( $_SESSION['wsEditToken'] ) ) {
1719 $token = $this->generateToken();
1720 $_SESSION['wsEditToken'] = $token;
1721 } else {
1722 $token = $_SESSION['wsEditToken'];
1724 if( is_array( $salt ) ) {
1725 $salt = implode( '|', $salt );
1727 return md5( $token . $salt );
1731 * Generate a hex-y looking random token for various uses.
1732 * Could be made more cryptographically sure if someone cares.
1733 * @return string
1735 function generateToken( $salt = '' ) {
1736 $token = dechex( mt_rand() ) . dechex( mt_rand() );
1737 return md5( $token . $salt );
1741 * Check given value against the token value stored in the session.
1742 * A match should confirm that the form was submitted from the
1743 * user's own login session, not a form submission from a third-party
1744 * site.
1746 * @param string $val - the input value to compare
1747 * @param string $salt - Optional function-specific data for hash
1748 * @return bool
1749 * @public
1751 function matchEditToken( $val, $salt = '' ) {
1752 global $wgMemc;
1753 $sessionToken = $this->editToken( $salt );
1754 if ( $val != $sessionToken ) {
1755 wfDebug( "User::matchEditToken: broken session data\n" );
1757 return $val == $sessionToken;
1761 * Generate a new e-mail confirmation token and send a confirmation
1762 * mail to the user's given address.
1764 * @return mixed True on success, a WikiError object on failure.
1766 function sendConfirmationMail() {
1767 global $wgContLang;
1768 $url = $this->confirmationTokenUrl( $expiration );
1769 return $this->sendMail( wfMsg( 'confirmemail_subject' ),
1770 wfMsg( 'confirmemail_body',
1771 wfGetIP(),
1772 $this->getName(),
1773 $url,
1774 $wgContLang->timeanddate( $expiration, false ) ) );
1778 * Send an e-mail to this user's account. Does not check for
1779 * confirmed status or validity.
1781 * @param string $subject
1782 * @param string $body
1783 * @param strong $from Optional from address; default $wgPasswordSender will be used otherwise.
1784 * @return mixed True on success, a WikiError object on failure.
1786 function sendMail( $subject, $body, $from = null ) {
1787 if( is_null( $from ) ) {
1788 global $wgPasswordSender;
1789 $from = $wgPasswordSender;
1792 require_once( 'UserMailer.php' );
1793 $to = new MailAddress( $this );
1794 $sender = new MailAddress( $from );
1795 $error = userMailer( $to, $sender, $subject, $body );
1797 if( $error == '' ) {
1798 return true;
1799 } else {
1800 return new WikiError( $error );
1805 * Generate, store, and return a new e-mail confirmation code.
1806 * A hash (unsalted since it's used as a key) is stored.
1807 * @param &$expiration mixed output: accepts the expiration time
1808 * @return string
1809 * @private
1811 function confirmationToken( &$expiration ) {
1812 $fname = 'User::confirmationToken';
1814 $now = time();
1815 $expires = $now + 7 * 24 * 60 * 60;
1816 $expiration = wfTimestamp( TS_MW, $expires );
1818 $token = $this->generateToken( $this->mId . $this->mEmail . $expires );
1819 $hash = md5( $token );
1821 $dbw =& wfGetDB( DB_MASTER );
1822 $dbw->update( 'user',
1823 array( 'user_email_token' => $hash,
1824 'user_email_token_expires' => $dbw->timestamp( $expires ) ),
1825 array( 'user_id' => $this->mId ),
1826 $fname );
1828 return $token;
1832 * Generate and store a new e-mail confirmation token, and return
1833 * the URL the user can use to confirm.
1834 * @param &$expiration mixed output: accepts the expiration time
1835 * @return string
1836 * @private
1838 function confirmationTokenUrl( &$expiration ) {
1839 $token = $this->confirmationToken( $expiration );
1840 $title = Title::makeTitle( NS_SPECIAL, 'Confirmemail/' . $token );
1841 return $title->getFullUrl();
1845 * Mark the e-mail address confirmed and save.
1847 function confirmEmail() {
1848 $this->loadFromDatabase();
1849 $this->mEmailAuthenticated = wfTimestampNow();
1850 $this->saveSettings();
1851 return true;
1855 * Is this user allowed to send e-mails within limits of current
1856 * site configuration?
1857 * @return bool
1859 function canSendEmail() {
1860 return $this->isEmailConfirmed();
1864 * Is this user allowed to receive e-mails within limits of current
1865 * site configuration?
1866 * @return bool
1868 function canReceiveEmail() {
1869 return $this->canSendEmail() && !$this->getOption( 'disablemail' );
1873 * Is this user's e-mail address valid-looking and confirmed within
1874 * limits of the current site configuration?
1876 * If $wgEmailAuthentication is on, this may require the user to have
1877 * confirmed their address by returning a code or using a password
1878 * sent to the address from the wiki.
1880 * @return bool
1882 function isEmailConfirmed() {
1883 global $wgEmailAuthentication;
1884 $this->loadFromDatabase();
1885 $confirmed = true;
1886 if( wfRunHooks( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) {
1887 if( $this->isAnon() )
1888 return false;
1889 if( !$this->isValidEmailAddr( $this->mEmail ) )
1890 return false;
1891 if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() )
1892 return false;
1893 return true;
1894 } else {
1895 return $confirmed;
1900 * @param array $groups list of groups
1901 * @return array list of permission key names for given groups combined
1902 * @static
1904 function getGroupPermissions( $groups ) {
1905 global $wgGroupPermissions;
1906 $rights = array();
1907 foreach( $groups as $group ) {
1908 if( isset( $wgGroupPermissions[$group] ) ) {
1909 $rights = array_merge( $rights,
1910 array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
1913 return $rights;
1917 * @param string $group key name
1918 * @return string localized descriptive name for group, if provided
1919 * @static
1921 function getGroupName( $group ) {
1922 $key = "group-$group";
1923 $name = wfMsg( $key );
1924 if( $name == '' || $name == "&lt;$key&gt;" ) {
1925 return $group;
1926 } else {
1927 return $name;
1932 * @param string $group key name
1933 * @return string localized descriptive name for member of a group, if provided
1934 * @static
1936 function getGroupMember( $group ) {
1937 $key = "group-$group-member";
1938 $name = wfMsg( $key );
1939 if( $name == '' || $name == "&lt;$key&gt;" ) {
1940 return $group;
1941 } else {
1942 return $name;
1948 * Return the set of defined explicit groups.
1949 * The * and 'user' groups are not included.
1950 * @return array
1951 * @static
1953 function getAllGroups() {
1954 global $wgGroupPermissions;
1955 return array_diff(
1956 array_keys( $wgGroupPermissions ),
1957 array( '*', 'user', 'autoconfirmed' ) );
1961 * Get the title of a page describing a particular group
1963 * @param $group Name of the group
1964 * @return mixed
1966 function getGroupPage( $group ) {
1967 $page = wfMsgForContent( 'grouppage-' . $group );
1968 if( !wfEmptyMsg( 'grouppage-' . $group, $page ) ) {
1969 $title = Title::newFromText( $page );
1970 if( is_object( $title ) )
1971 return $title;
1973 return false;