*cough* Er, make it work? :) [dropped the action=submit] part
[mediawiki.git] / includes / User.php
blobf695d1d2c923b22aa1774e6f158d2568c5bcf5a6
1 <?php
2 /**
3 * See user.txt
5 * @package MediaWiki
6 */
8 # Number of characters in user_token field
9 define( 'USER_TOKEN_LENGTH', 32 );
11 # Serialized record version
12 define( 'MW_USER_VERSION', 3 );
14 /**
16 * @package MediaWiki
18 class User {
20 * When adding a new private variable, dont forget to add it to __sleep()
22 /**@{{
23 * @private
25 var $mBlockedby; //!<
26 var $mBlockreason; //!<
27 var $mDataLoaded; //!<
28 var $mEmail; //!<
29 var $mEmailAuthenticated; //!<
30 var $mGroups; //!<
31 var $mHash; //!<
32 var $mId; //!<
33 var $mName; //!<
34 var $mNewpassword; //!<
35 var $mNewtalk; //!<
36 var $mOptions; //!<
37 var $mPassword; //!<
38 var $mRealName; //!<
39 var $mRegistration; //!<
40 var $mRights; //!<
41 var $mSkin; //!<
42 var $mToken; //!<
43 var $mTouched; //!<
44 var $mVersion; //!< serialized version
45 /**@}} */
47 /** Constructor using User:loadDefaults() */
48 function User() {
49 $this->loadDefaults();
50 $this->mVersion = MW_USER_VERSION;
53 /**
54 * Static factory method
55 * @param string $name Username, validated by Title:newFromText()
56 * @param bool $validate Validate username
57 * @return User
58 * @static
60 function newFromName( $name, $validate = true ) {
61 # Force usernames to capital
62 global $wgContLang;
63 $name = $wgContLang->ucfirst( $name );
65 # Clean up name according to title rules
66 $t = Title::newFromText( $name );
67 if( is_null( $t ) ) {
68 return null;
71 # Reject various classes of invalid names
72 $canonicalName = $t->getText();
73 global $wgAuth;
74 $canonicalName = $wgAuth->getCanonicalName( $t->getText() );
76 if( $validate && !User::isValidUserName( $canonicalName ) ) {
77 return null;
80 $u = new User();
81 $u->setName( $canonicalName );
82 $u->setId( $u->idFromName( $canonicalName ) );
83 return $u;
86 /**
87 * Factory method to fetch whichever use has a given email confirmation code.
88 * This code is generated when an account is created or its e-mail address
89 * has changed.
91 * If the code is invalid or has expired, returns NULL.
93 * @param string $code
94 * @return User
95 * @static
97 function newFromConfirmationCode( $code ) {
98 $dbr =& wfGetDB( DB_SLAVE );
99 $name = $dbr->selectField( 'user', 'user_name', array(
100 'user_email_token' => md5( $code ),
101 'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ),
102 ) );
103 if( is_string( $name ) ) {
104 return User::newFromName( $name );
105 } else {
106 return null;
111 * Serialze sleep function, for better cache efficiency and avoidance of
112 * silly "incomplete type" errors when skins are cached. The array should
113 * contain names of private variables (see at top of User.php).
115 function __sleep() {
116 return array(
117 'mBlockedby',
118 'mBlockreason',
119 'mDataLoaded',
120 'mEmail',
121 'mEmailAuthenticated',
122 'mGroups',
123 'mHash',
124 'mId',
125 'mName',
126 'mNewpassword',
127 'mNewtalk',
128 'mOptions',
129 'mPassword',
130 'mRealName',
131 'mRegistration',
132 'mRights',
133 'mToken',
134 'mTouched',
135 'mVersion',
140 * Get username given an id.
141 * @param integer $id Database user id
142 * @return string Nickname of a user
143 * @static
145 function whoIs( $id ) {
146 $dbr =& wfGetDB( DB_SLAVE );
147 return $dbr->selectField( 'user', 'user_name', array( 'user_id' => $id ), 'User::whoIs' );
151 * Get real username given an id.
152 * @param integer $id Database user id
153 * @return string Realname of a user
154 * @static
156 function whoIsReal( $id ) {
157 $dbr =& wfGetDB( DB_SLAVE );
158 return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ), 'User::whoIsReal' );
162 * Get database id given a user name
163 * @param string $name Nickname of a user
164 * @return integer|null Database user id (null: if non existent
165 * @static
167 function idFromName( $name ) {
168 $fname = "User::idFromName";
170 $nt = Title::newFromText( $name );
171 if( is_null( $nt ) ) {
172 # Illegal name
173 return null;
175 $dbr =& wfGetDB( DB_SLAVE );
176 $s = $dbr->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), $fname );
178 if ( $s === false ) {
179 return 0;
180 } else {
181 return $s->user_id;
186 * Does the string match an anonymous IPv4 address?
188 * This function exists for username validation, in order to reject
189 * usernames which are similar in form to IP addresses. Strings such
190 * as 300.300.300.300 will return true because it looks like an IP
191 * address, despite not being strictly valid.
193 * 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 inDnsBlacklist( $ip, $base ) {
484 $fname = 'User::inDnsBlacklist';
485 wfProfileIn( $fname );
487 $found = false;
488 $host = '';
490 if ( preg_match( '/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $ip, $m ) ) {
491 # Make hostname
492 for ( $i=4; $i>=1; $i-- ) {
493 $host .= $m[$i] . '.';
495 $host .= $base;
497 # Send query
498 $ipList = gethostbynamel( $host );
500 if ( $ipList ) {
501 wfDebug( "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" );
502 $found = true;
503 } else {
504 wfDebug( "Requested $host, not found in $base.\n" );
508 wfProfileOut( $fname );
509 return $found;
513 * Primitive rate limits: enforce maximum actions per time period
514 * to put a brake on flooding.
516 * Note: when using a shared cache like memcached, IP-address
517 * last-hit counters will be shared across wikis.
519 * @return bool true if a rate limiter was tripped
520 * @public
522 function pingLimiter( $action='edit' ) {
523 global $wgRateLimits, $wgRateLimitsExcludedGroups;
524 if( !isset( $wgRateLimits[$action] ) ) {
525 return false;
528 # Some groups shouldn't trigger the ping limiter, ever
529 foreach( $this->getGroups() as $group ) {
530 if( array_search( $group, $wgRateLimitsExcludedGroups ) !== false )
531 return false;
534 global $wgMemc, $wgDBname, $wgRateLimitLog;
535 $fname = 'User::pingLimiter';
536 wfProfileIn( $fname );
538 $limits = $wgRateLimits[$action];
539 $keys = array();
540 $id = $this->getId();
541 $ip = wfGetIP();
543 if( isset( $limits['anon'] ) && $id == 0 ) {
544 $keys["$wgDBname:limiter:$action:anon"] = $limits['anon'];
547 if( isset( $limits['user'] ) && $id != 0 ) {
548 $keys["$wgDBname:limiter:$action:user:$id"] = $limits['user'];
550 if( $this->isNewbie() ) {
551 if( isset( $limits['newbie'] ) && $id != 0 ) {
552 $keys["$wgDBname:limiter:$action:user:$id"] = $limits['newbie'];
554 if( isset( $limits['ip'] ) ) {
555 $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip'];
557 if( isset( $limits['subnet'] ) && preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
558 $subnet = $matches[1];
559 $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
563 $triggered = false;
564 foreach( $keys as $key => $limit ) {
565 list( $max, $period ) = $limit;
566 $summary = "(limit $max in {$period}s)";
567 $count = $wgMemc->get( $key );
568 if( $count ) {
569 if( $count > $max ) {
570 wfDebug( "$fname: tripped! $key at $count $summary\n" );
571 if( $wgRateLimitLog ) {
572 @error_log( wfTimestamp( TS_MW ) . ' ' . $wgDBname . ': ' . $this->getName() . " tripped $key at $count $summary\n", 3, $wgRateLimitLog );
574 $triggered = true;
575 } else {
576 wfDebug( "$fname: ok. $key at $count $summary\n" );
578 } else {
579 wfDebug( "$fname: adding record for $key $summary\n" );
580 $wgMemc->add( $key, 1, intval( $period ) );
582 $wgMemc->incr( $key );
585 wfProfileOut( $fname );
586 return $triggered;
590 * Check if user is blocked
591 * @return bool True if blocked, false otherwise
593 function isBlocked( $bFromSlave = true ) { // hacked from false due to horrible probs on site
594 wfDebug( "User::isBlocked: enter\n" );
595 $this->getBlockedStatus( $bFromSlave );
596 return $this->mBlockedby !== 0;
600 * Check if user is blocked from editing a particular article
602 function isBlockedFrom( $title, $bFromSlave = false ) {
603 global $wgBlockAllowsUTEdit;
604 $fname = 'User::isBlockedFrom';
605 wfProfileIn( $fname );
606 wfDebug( "$fname: enter\n" );
608 if ( $wgBlockAllowsUTEdit && $title->getText() === $this->getName() &&
609 $title->getNamespace() == NS_USER_TALK )
611 $blocked = false;
612 wfDebug( "$fname: self-talk page, ignoring any blocks\n" );
613 } else {
614 wfDebug( "$fname: asking isBlocked()\n" );
615 $blocked = $this->isBlocked( $bFromSlave );
617 wfProfileOut( $fname );
618 return $blocked;
622 * Get name of blocker
623 * @return string name of blocker
625 function blockedBy() {
626 $this->getBlockedStatus();
627 return $this->mBlockedby;
631 * Get blocking reason
632 * @return string Blocking reason
634 function blockedFor() {
635 $this->getBlockedStatus();
636 return $this->mBlockreason;
640 * Initialise php session
642 function SetupSession() {
643 global $wgSessionsInMemcached, $wgCookiePath, $wgCookieDomain;
644 if( $wgSessionsInMemcached ) {
645 require_once( 'MemcachedSessions.php' );
646 } elseif( 'files' != ini_get( 'session.save_handler' ) ) {
647 # If it's left on 'user' or another setting from another
648 # application, it will end up failing. Try to recover.
649 ini_set ( 'session.save_handler', 'files' );
651 session_set_cookie_params( 0, $wgCookiePath, $wgCookieDomain );
652 session_cache_limiter( 'private, must-revalidate' );
653 @session_start();
657 * Create a new user object using data from session
658 * @static
660 function loadFromSession() {
661 global $wgMemc, $wgDBname, $wgCookiePrefix;
663 if ( isset( $_SESSION['wsUserID'] ) ) {
664 if ( 0 != $_SESSION['wsUserID'] ) {
665 $sId = $_SESSION['wsUserID'];
666 } else {
667 return new User();
669 } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserID"] ) ) {
670 $sId = intval( $_COOKIE["{$wgCookiePrefix}UserID"] );
671 $_SESSION['wsUserID'] = $sId;
672 } else {
673 return new User();
675 if ( isset( $_SESSION['wsUserName'] ) ) {
676 $sName = $_SESSION['wsUserName'];
677 } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserName"] ) ) {
678 $sName = $_COOKIE["{$wgCookiePrefix}UserName"];
679 $_SESSION['wsUserName'] = $sName;
680 } else {
681 return new User();
684 $passwordCorrect = FALSE;
685 $user = $wgMemc->get( $key = "$wgDBname:user:id:$sId" );
686 if( !is_object( $user ) || $user->mVersion < MW_USER_VERSION ) {
687 # Expire old serialized objects; they may be corrupt.
688 $user = false;
690 if($makenew = !$user) {
691 wfDebug( "User::loadFromSession() unable to load from memcached\n" );
692 $user = new User();
693 $user->mId = $sId;
694 $user->loadFromDatabase();
695 } else {
696 wfDebug( "User::loadFromSession() got from cache!\n" );
699 if ( isset( $_SESSION['wsToken'] ) ) {
700 $passwordCorrect = $_SESSION['wsToken'] == $user->mToken;
701 } else if ( isset( $_COOKIE["{$wgCookiePrefix}Token"] ) ) {
702 $passwordCorrect = $user->mToken == $_COOKIE["{$wgCookiePrefix}Token"];
703 } else {
704 return new User(); # Can't log in from session
707 if ( ( $sName == $user->mName ) && $passwordCorrect ) {
708 if($makenew) {
709 if($wgMemc->set( $key, $user ))
710 wfDebug( "User::loadFromSession() successfully saved user\n" );
711 else
712 wfDebug( "User::loadFromSession() unable to save to memcached\n" );
714 return $user;
716 return new User(); # Can't log in from session
720 * Load a user from the database
722 function loadFromDatabase() {
723 $fname = "User::loadFromDatabase";
725 # Counter-intuitive, breaks various things, use User::setLoaded() if you want to suppress
726 # loading in a command line script, don't assume all command line scripts need it like this
727 #if ( $this->mDataLoaded || $wgCommandLineMode ) {
728 if ( $this->mDataLoaded ) {
729 return;
732 # Paranoia
733 $this->mId = intval( $this->mId );
735 /** Anonymous user */
736 if( !$this->mId ) {
737 /** Get rights */
738 $this->mRights = $this->getGroupPermissions( array( '*' ) );
739 $this->mDataLoaded = true;
740 return;
741 } # the following stuff is for non-anonymous users only
743 $dbr =& wfGetDB( DB_SLAVE );
744 $s = $dbr->selectRow( 'user', array( 'user_name','user_password','user_newpassword','user_email',
745 'user_email_authenticated',
746 'user_real_name','user_options','user_touched', 'user_token', 'user_registration' ),
747 array( 'user_id' => $this->mId ), $fname );
749 if ( $s !== false ) {
750 $this->mName = $s->user_name;
751 $this->mEmail = $s->user_email;
752 $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $s->user_email_authenticated );
753 $this->mRealName = $s->user_real_name;
754 $this->mPassword = $s->user_password;
755 $this->mNewpassword = $s->user_newpassword;
756 $this->decodeOptions( $s->user_options );
757 $this->mTouched = wfTimestamp(TS_MW,$s->user_touched);
758 $this->mToken = $s->user_token;
759 $this->mRegistration = wfTimestampOrNull( TS_MW, $s->user_registration );
761 $res = $dbr->select( 'user_groups',
762 array( 'ug_group' ),
763 array( 'ug_user' => $this->mId ),
764 $fname );
765 $this->mGroups = array();
766 while( $row = $dbr->fetchObject( $res ) ) {
767 $this->mGroups[] = $row->ug_group;
769 $implicitGroups = array( '*', 'user' );
771 global $wgAutoConfirmAge;
772 $accountAge = time() - wfTimestampOrNull( TS_UNIX, $this->mRegistration );
773 if( $accountAge >= $wgAutoConfirmAge ) {
774 $implicitGroups[] = 'autoconfirmed';
777 $effectiveGroups = array_merge( $implicitGroups, $this->mGroups );
778 $this->mRights = $this->getGroupPermissions( $effectiveGroups );
781 $this->mDataLoaded = true;
784 function getID() { return $this->mId; }
785 function setID( $v ) {
786 $this->mId = $v;
787 $this->mDataLoaded = false;
790 function getName() {
791 $this->loadFromDatabase();
792 if ( $this->mName === false ) {
793 $this->mName = wfGetIP();
795 return $this->mName;
798 function setName( $str ) {
799 $this->loadFromDatabase();
800 $this->mName = $str;
805 * Return the title dbkey form of the name, for eg user pages.
806 * @return string
807 * @public
809 function getTitleKey() {
810 return str_replace( ' ', '_', $this->getName() );
813 function getNewtalk() {
814 $this->loadFromDatabase();
816 # Load the newtalk status if it is unloaded (mNewtalk=-1)
817 if( $this->mNewtalk === -1 ) {
818 $this->mNewtalk = false; # reset talk page status
820 # Check memcached separately for anons, who have no
821 # entire User object stored in there.
822 if( !$this->mId ) {
823 global $wgDBname, $wgMemc;
824 $key = "$wgDBname:newtalk:ip:" . $this->getName();
825 $newtalk = $wgMemc->get( $key );
826 if( is_integer( $newtalk ) ) {
827 $this->mNewtalk = (bool)$newtalk;
828 } else {
829 $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName() );
830 $wgMemc->set( $key, $this->mNewtalk, time() ); // + 1800 );
832 } else {
833 $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
837 return (bool)$this->mNewtalk;
841 * Return the talk page(s) this user has new messages on.
843 function getNewMessageLinks() {
844 global $wgDBname;
845 $talks = array();
846 if (!wfRunHooks('UserRetrieveNewTalks', array(&$this, &$talks)))
847 return $talks;
849 if (!$this->getNewtalk())
850 return array();
851 $up = $this->getUserPage();
852 $utp = $up->getTalkPage();
853 return array(array("wiki" => $wgDBname, "link" => $utp->getLocalURL()));
858 * Perform a user_newtalk check on current slaves; if the memcached data
859 * is funky we don't want newtalk state to get stuck on save, as that's
860 * damn annoying.
862 * @param string $field
863 * @param mixed $id
864 * @return bool
865 * @private
867 function checkNewtalk( $field, $id ) {
868 $fname = 'User::checkNewtalk';
869 $dbr =& wfGetDB( DB_SLAVE );
870 $ok = $dbr->selectField( 'user_newtalk', $field,
871 array( $field => $id ), $fname );
872 return $ok !== false;
876 * Add or update the
877 * @param string $field
878 * @param mixed $id
879 * @private
881 function updateNewtalk( $field, $id ) {
882 $fname = 'User::updateNewtalk';
883 if( $this->checkNewtalk( $field, $id ) ) {
884 wfDebug( "$fname already set ($field, $id), ignoring\n" );
885 return false;
887 $dbw =& wfGetDB( DB_MASTER );
888 $dbw->insert( 'user_newtalk',
889 array( $field => $id ),
890 $fname,
891 'IGNORE' );
892 wfDebug( "$fname: set on ($field, $id)\n" );
893 return true;
897 * Clear the new messages flag for the given user
898 * @param string $field
899 * @param mixed $id
900 * @private
902 function deleteNewtalk( $field, $id ) {
903 $fname = 'User::deleteNewtalk';
904 if( !$this->checkNewtalk( $field, $id ) ) {
905 wfDebug( "$fname: already gone ($field, $id), ignoring\n" );
906 return false;
908 $dbw =& wfGetDB( DB_MASTER );
909 $dbw->delete( 'user_newtalk',
910 array( $field => $id ),
911 $fname );
912 wfDebug( "$fname: killed on ($field, $id)\n" );
913 return true;
917 * Update the 'You have new messages!' status.
918 * @param bool $val
920 function setNewtalk( $val ) {
921 if( wfReadOnly() ) {
922 return;
925 $this->loadFromDatabase();
926 $this->mNewtalk = $val;
928 $fname = 'User::setNewtalk';
930 if( $this->isAnon() ) {
931 $field = 'user_ip';
932 $id = $this->getName();
933 } else {
934 $field = 'user_id';
935 $id = $this->getId();
938 if( $val ) {
939 $changed = $this->updateNewtalk( $field, $id );
940 } else {
941 $changed = $this->deleteNewtalk( $field, $id );
944 if( $changed ) {
945 if( $this->isAnon() ) {
946 // Anons have a separate memcached space, since
947 // user records aren't kept for them.
948 global $wgDBname, $wgMemc;
949 $key = "$wgDBname:newtalk:ip:$val";
950 $wgMemc->set( $key, $val ? 1 : 0 );
951 } else {
952 if( $val ) {
953 // Make sure the user page is watched, so a notification
954 // will be sent out if enabled.
955 $this->addWatch( $this->getTalkPage() );
958 $this->invalidateCache();
959 $this->saveSettings();
963 function invalidateCache() {
964 global $wgClockSkewFudge;
965 $this->loadFromDatabase();
966 $this->mTouched = wfTimestamp(TS_MW, time() + $wgClockSkewFudge );
967 # Don't forget to save the options after this or
968 # it won't take effect!
971 function validateCache( $timestamp ) {
972 $this->loadFromDatabase();
973 return ($timestamp >= $this->mTouched);
977 * Encrypt a password.
978 * It can eventuall salt a password @see User::addSalt()
979 * @param string $p clear Password.
980 * @return string Encrypted password.
982 function encryptPassword( $p ) {
983 return wfEncryptPassword( $this->mId, $p );
986 # Set the password and reset the random token
987 function setPassword( $str ) {
988 $this->loadFromDatabase();
989 $this->setToken();
990 $this->mPassword = $this->encryptPassword( $str );
991 $this->mNewpassword = '';
994 # Set the random token (used for persistent authentication)
995 function setToken( $token = false ) {
996 global $wgSecretKey, $wgProxyKey, $wgDBname;
997 if ( !$token ) {
998 if ( $wgSecretKey ) {
999 $key = $wgSecretKey;
1000 } elseif ( $wgProxyKey ) {
1001 $key = $wgProxyKey;
1002 } else {
1003 $key = microtime();
1005 $this->mToken = md5( $key . mt_rand( 0, 0x7fffffff ) . $wgDBname . $this->mId );
1006 } else {
1007 $this->mToken = $token;
1012 function setCookiePassword( $str ) {
1013 $this->loadFromDatabase();
1014 $this->mCookiePassword = md5( $str );
1017 function setNewpassword( $str ) {
1018 $this->loadFromDatabase();
1019 $this->mNewpassword = $this->encryptPassword( $str );
1022 function getEmail() {
1023 $this->loadFromDatabase();
1024 return $this->mEmail;
1027 function getEmailAuthenticationTimestamp() {
1028 $this->loadFromDatabase();
1029 return $this->mEmailAuthenticated;
1032 function setEmail( $str ) {
1033 $this->loadFromDatabase();
1034 $this->mEmail = $str;
1037 function getRealName() {
1038 $this->loadFromDatabase();
1039 return $this->mRealName;
1042 function setRealName( $str ) {
1043 $this->loadFromDatabase();
1044 $this->mRealName = $str;
1048 * @param string $oname The option to check
1049 * @return string
1051 function getOption( $oname ) {
1052 $this->loadFromDatabase();
1053 if ( array_key_exists( $oname, $this->mOptions ) ) {
1054 return trim( $this->mOptions[$oname] );
1055 } else {
1056 return '';
1061 * @param string $oname The option to check
1062 * @return bool False if the option is not selected, true if it is
1064 function getBoolOption( $oname ) {
1065 return (bool)$this->getOption( $oname );
1069 * Get an option as an integer value from the source string.
1070 * @param string $oname The option to check
1071 * @param int $default Optional value to return if option is unset/blank.
1072 * @return int
1074 function getIntOption( $oname, $default=0 ) {
1075 $val = $this->getOption( $oname );
1076 if( $val == '' ) {
1077 $val = $default;
1079 return intval( $val );
1082 function setOption( $oname, $val ) {
1083 $this->loadFromDatabase();
1084 if ( $oname == 'skin' ) {
1085 # Clear cached skin, so the new one displays immediately in Special:Preferences
1086 unset( $this->mSkin );
1088 // Filter out any newlines that may have passed through input validation.
1089 // Newlines are used to separate items in the options blob.
1090 $val = str_replace( "\r\n", "\n", $val );
1091 $val = str_replace( "\r", "\n", $val );
1092 $val = str_replace( "\n", " ", $val );
1093 $this->mOptions[$oname] = $val;
1094 $this->invalidateCache();
1097 function getRights() {
1098 $this->loadFromDatabase();
1099 return $this->mRights;
1103 * Get the list of explicit group memberships this user has.
1104 * The implicit * and user groups are not included.
1105 * @return array of strings
1107 function getGroups() {
1108 $this->loadFromDatabase();
1109 return $this->mGroups;
1113 * Get the list of implicit group memberships this user has.
1114 * This includes all explicit groups, plus 'user' if logged in
1115 * and '*' for all accounts.
1116 * @return array of strings
1118 function getEffectiveGroups() {
1119 $base = array( '*' );
1120 if( $this->isLoggedIn() ) {
1121 $base[] = 'user';
1123 return array_merge( $base, $this->getGroups() );
1127 * Add the user to the given group.
1128 * This takes immediate effect.
1129 * @string $group
1131 function addGroup( $group ) {
1132 $dbw =& wfGetDB( DB_MASTER );
1133 $dbw->insert( 'user_groups',
1134 array(
1135 'ug_user' => $this->getID(),
1136 'ug_group' => $group,
1138 'User::addGroup',
1139 array( 'IGNORE' ) );
1141 $this->mGroups = array_merge( $this->mGroups, array( $group ) );
1142 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups() );
1144 $this->invalidateCache();
1145 $this->saveSettings();
1149 * Remove the user from the given group.
1150 * This takes immediate effect.
1151 * @string $group
1153 function removeGroup( $group ) {
1154 $dbw =& wfGetDB( DB_MASTER );
1155 $dbw->delete( 'user_groups',
1156 array(
1157 'ug_user' => $this->getID(),
1158 'ug_group' => $group,
1160 'User::removeGroup' );
1162 $this->mGroups = array_diff( $this->mGroups, array( $group ) );
1163 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups() );
1165 $this->invalidateCache();
1166 $this->saveSettings();
1171 * A more legible check for non-anonymousness.
1172 * Returns true if the user is not an anonymous visitor.
1174 * @return bool
1176 function isLoggedIn() {
1177 return( $this->getID() != 0 );
1181 * A more legible check for anonymousness.
1182 * Returns true if the user is an anonymous visitor.
1184 * @return bool
1186 function isAnon() {
1187 return !$this->isLoggedIn();
1191 * Deprecated in 1.6, die in 1.7, to be removed in 1.8
1192 * @deprecated
1194 function isSysop() {
1195 throw new MWException( "Call to deprecated (v1.7) User::isSysop() method\n" );
1196 #return $this->isAllowed( 'protect' );
1200 * Deprecated in 1.6, die in 1.7, to be removed in 1.8
1201 * @deprecated
1203 function isDeveloper() {
1204 throw new MWException( "Call to deprecated (v1.7) User::isDeveloper() method\n" );
1205 #return $this->isAllowed( 'siteadmin' );
1209 * Deprecated in 1.6, die in 1.7, to be removed in 1.8
1210 * @deprecated
1212 function isBureaucrat() {
1213 throw new MWException( "Call to deprecated (v1.7) User::isBureaucrat() method\n" );
1214 #return $this->isAllowed( 'makesysop' );
1218 * Whether the user is a bot
1219 * @todo need to be migrated to the new user level management sytem
1221 function isBot() {
1222 $this->loadFromDatabase();
1223 return in_array( 'bot', $this->mRights );
1227 * Check if user is allowed to access a feature / make an action
1228 * @param string $action Action to be checked (see $wgAvailableRights in Defines.php for possible actions).
1229 * @return boolean True: action is allowed, False: action should not be allowed
1231 function isAllowed($action='') {
1232 if ( $action === '' )
1233 // In the spirit of DWIM
1234 return true;
1236 $this->loadFromDatabase();
1237 return in_array( $action , $this->mRights );
1241 * Load a skin if it doesn't exist or return it
1242 * @todo FIXME : need to check the old failback system [AV]
1244 function &getSkin() {
1245 global $IP, $wgRequest;
1246 if ( ! isset( $this->mSkin ) ) {
1247 $fname = 'User::getSkin';
1248 wfProfileIn( $fname );
1250 # get the user skin
1251 $userSkin = $this->getOption( 'skin' );
1252 $userSkin = $wgRequest->getVal('useskin', $userSkin);
1254 $this->mSkin =& Skin::newFromKey( $userSkin );
1255 wfProfileOut( $fname );
1257 return $this->mSkin;
1260 /**#@+
1261 * @param string $title Article title to look at
1265 * Check watched status of an article
1266 * @return bool True if article is watched
1268 function isWatched( $title ) {
1269 $wl = WatchedItem::fromUserTitle( $this, $title );
1270 return $wl->isWatched();
1274 * Watch an article
1276 function addWatch( $title ) {
1277 $wl = WatchedItem::fromUserTitle( $this, $title );
1278 $wl->addWatch();
1279 $this->invalidateCache();
1283 * Stop watching an article
1285 function removeWatch( $title ) {
1286 $wl = WatchedItem::fromUserTitle( $this, $title );
1287 $wl->removeWatch();
1288 $this->invalidateCache();
1292 * Clear the user's notification timestamp for the given title.
1293 * If e-notif e-mails are on, they will receive notification mails on
1294 * the next change of the page if it's watched etc.
1296 function clearNotification( &$title ) {
1297 global $wgUser, $wgUseEnotif;
1300 if ($title->getNamespace() == NS_USER_TALK &&
1301 $title->getText() == $this->getName() ) {
1302 if (!wfRunHooks('UserClearNewTalkNotification', array(&$this)))
1303 return;
1304 $this->setNewtalk( false );
1307 if( !$wgUseEnotif ) {
1308 return;
1311 if( $this->isAnon() ) {
1312 // Nothing else to do...
1313 return;
1316 // Only update the timestamp if the page is being watched.
1317 // The query to find out if it is watched is cached both in memcached and per-invocation,
1318 // and when it does have to be executed, it can be on a slave
1319 // If this is the user's newtalk page, we always update the timestamp
1320 if ($title->getNamespace() == NS_USER_TALK &&
1321 $title->getText() == $wgUser->getName())
1323 $watched = true;
1324 } elseif ( $this->getID() == $wgUser->getID() ) {
1325 $watched = $title->userIsWatching();
1326 } else {
1327 $watched = true;
1330 // If the page is watched by the user (or may be watched), update the timestamp on any
1331 // any matching rows
1332 if ( $watched ) {
1333 $dbw =& wfGetDB( DB_MASTER );
1334 $success = $dbw->update( 'watchlist',
1335 array( /* SET */
1336 'wl_notificationtimestamp' => NULL
1337 ), array( /* WHERE */
1338 'wl_title' => $title->getDBkey(),
1339 'wl_namespace' => $title->getNamespace(),
1340 'wl_user' => $this->getID()
1341 ), 'User::clearLastVisited'
1346 /**#@-*/
1349 * Resets all of the given user's page-change notification timestamps.
1350 * If e-notif e-mails are on, they will receive notification mails on
1351 * the next change of any watched page.
1353 * @param int $currentUser user ID number
1354 * @public
1356 function clearAllNotifications( $currentUser ) {
1357 global $wgUseEnotif;
1358 if ( !$wgUseEnotif ) {
1359 $this->setNewtalk( false );
1360 return;
1362 if( $currentUser != 0 ) {
1364 $dbw =& wfGetDB( DB_MASTER );
1365 $success = $dbw->update( 'watchlist',
1366 array( /* SET */
1367 'wl_notificationtimestamp' => 0
1368 ), array( /* WHERE */
1369 'wl_user' => $currentUser
1370 ), 'UserMailer::clearAll'
1373 # we also need to clear here the "you have new message" notification for the own user_talk page
1374 # This is cleared one page view later in Article::viewUpdates();
1379 * @private
1380 * @return string Encoding options
1382 function encodeOptions() {
1383 $a = array();
1384 foreach ( $this->mOptions as $oname => $oval ) {
1385 array_push( $a, $oname.'='.$oval );
1387 $s = implode( "\n", $a );
1388 return $s;
1392 * @private
1394 function decodeOptions( $str ) {
1395 $a = explode( "\n", $str );
1396 foreach ( $a as $s ) {
1397 if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
1398 $this->mOptions[$m[1]] = $m[2];
1403 function setCookies() {
1404 global $wgCookieExpiration, $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookiePrefix;
1405 if ( 0 == $this->mId ) return;
1406 $this->loadFromDatabase();
1407 $exp = time() + $wgCookieExpiration;
1409 $_SESSION['wsUserID'] = $this->mId;
1410 setcookie( $wgCookiePrefix.'UserID', $this->mId, $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1412 $_SESSION['wsUserName'] = $this->getName();
1413 setcookie( $wgCookiePrefix.'UserName', $this->getName(), $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1415 $_SESSION['wsToken'] = $this->mToken;
1416 if ( 1 == $this->getOption( 'rememberpassword' ) ) {
1417 setcookie( $wgCookiePrefix.'Token', $this->mToken, $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1418 } else {
1419 setcookie( $wgCookiePrefix.'Token', '', time() - 3600 );
1424 * Logout user
1425 * It will clean the session cookie
1427 function logout() {
1428 global $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookiePrefix;
1429 $this->loadDefaults();
1430 $this->setLoaded( true );
1432 $_SESSION['wsUserID'] = 0;
1434 setcookie( $wgCookiePrefix.'UserID', '', time() - 3600, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1435 setcookie( $wgCookiePrefix.'Token', '', time() - 3600, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1437 # Remember when user logged out, to prevent seeing cached pages
1438 setcookie( $wgCookiePrefix.'LoggedOut', wfTimestampNow(), time() + 86400, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1442 * Save object settings into database
1444 function saveSettings() {
1445 global $wgMemc, $wgDBname;
1446 $fname = 'User::saveSettings';
1448 if ( wfReadOnly() ) { return; }
1449 if ( 0 == $this->mId ) { return; }
1451 $dbw =& wfGetDB( DB_MASTER );
1452 $dbw->update( 'user',
1453 array( /* SET */
1454 'user_name' => $this->mName,
1455 'user_password' => $this->mPassword,
1456 'user_newpassword' => $this->mNewpassword,
1457 'user_real_name' => $this->mRealName,
1458 'user_email' => $this->mEmail,
1459 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
1460 'user_options' => $this->encodeOptions(),
1461 'user_touched' => $dbw->timestamp($this->mTouched),
1462 'user_token' => $this->mToken
1463 ), array( /* WHERE */
1464 'user_id' => $this->mId
1465 ), $fname
1467 $wgMemc->delete( "$wgDBname:user:id:$this->mId" );
1472 * Checks if a user with the given name exists, returns the ID
1474 function idForName() {
1475 $fname = 'User::idForName';
1477 $gotid = 0;
1478 $s = trim( $this->getName() );
1479 if ( 0 == strcmp( '', $s ) ) return 0;
1481 $dbr =& wfGetDB( DB_SLAVE );
1482 $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), $fname );
1483 if ( $id === false ) {
1484 $id = 0;
1486 return $id;
1490 * Add user object to the database
1492 function addToDatabase() {
1493 $fname = 'User::addToDatabase';
1494 $dbw =& wfGetDB( DB_MASTER );
1495 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
1496 $dbw->insert( 'user',
1497 array(
1498 'user_id' => $seqVal,
1499 'user_name' => $this->mName,
1500 'user_password' => $this->mPassword,
1501 'user_newpassword' => $this->mNewpassword,
1502 'user_email' => $this->mEmail,
1503 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
1504 'user_real_name' => $this->mRealName,
1505 'user_options' => $this->encodeOptions(),
1506 'user_token' => $this->mToken,
1507 'user_registration' => $dbw->timestamp( $this->mRegistration ),
1508 ), $fname
1510 $this->mId = $dbw->insertId();
1513 function spreadBlock() {
1514 # If the (non-anonymous) user is blocked, this function will block any IP address
1515 # that they successfully log on from.
1516 $fname = 'User::spreadBlock';
1518 wfDebug( "User:spreadBlock()\n" );
1519 if ( $this->mId == 0 ) {
1520 return;
1523 $userblock = Block::newFromDB( '', $this->mId );
1524 if ( !$userblock->isValid() ) {
1525 return;
1528 # Check if this IP address is already blocked
1529 $ipblock = Block::newFromDB( wfGetIP() );
1530 if ( $ipblock->isValid() ) {
1531 # If the user is already blocked. Then check if the autoblock would
1532 # excede the user block. If it would excede, then do nothing, else
1533 # prolong block time
1534 if ($userblock->mExpiry &&
1535 ($userblock->mExpiry < Block::getAutoblockExpiry($ipblock->mTimestamp))) {
1536 return;
1538 # Just update the timestamp
1539 $ipblock->updateTimestamp();
1540 return;
1543 # Make a new block object with the desired properties
1544 wfDebug( "Autoblocking {$this->mName}@" . wfGetIP() . "\n" );
1545 $ipblock->mAddress = wfGetIP();
1546 $ipblock->mUser = 0;
1547 $ipblock->mBy = $userblock->mBy;
1548 $ipblock->mReason = wfMsg( 'autoblocker', $this->getName(), $userblock->mReason );
1549 $ipblock->mTimestamp = wfTimestampNow();
1550 $ipblock->mAuto = 1;
1551 # If the user is already blocked with an expiry date, we don't
1552 # want to pile on top of that!
1553 if($userblock->mExpiry) {
1554 $ipblock->mExpiry = min ( $userblock->mExpiry, Block::getAutoblockExpiry( $ipblock->mTimestamp ));
1555 } else {
1556 $ipblock->mExpiry = Block::getAutoblockExpiry( $ipblock->mTimestamp );
1559 # Insert it
1560 $ipblock->insert();
1565 * Generate a string which will be different for any combination of
1566 * user options which would produce different parser output.
1567 * This will be used as part of the hash key for the parser cache,
1568 * so users will the same options can share the same cached data
1569 * safely.
1571 * Extensions which require it should install 'PageRenderingHash' hook,
1572 * which will give them a chance to modify this key based on their own
1573 * settings.
1575 * @return string
1577 function getPageRenderingHash() {
1578 global $wgContLang;
1579 if( $this->mHash ){
1580 return $this->mHash;
1583 // stubthreshold is only included below for completeness,
1584 // it will always be 0 when this function is called by parsercache.
1586 $confstr = $this->getOption( 'math' );
1587 $confstr .= '!' . $this->getOption( 'stubthreshold' );
1588 $confstr .= '!' . $this->getOption( 'date' );
1589 $confstr .= '!' . ($this->getOption( 'numberheadings' ) ? '1' : '');
1590 $confstr .= '!' . $this->getOption( 'language' );
1591 $confstr .= '!' . $this->getOption( 'thumbsize' );
1592 // add in language specific options, if any
1593 $extra = $wgContLang->getExtraHashOptions();
1594 $confstr .= $extra;
1596 // Give a chance for extensions to modify the hash, if they have
1597 // extra options or other effects on the parser cache.
1598 wfRunHooks( 'PageRenderingHash', array( &$confstr ) );
1600 $this->mHash = $confstr;
1601 return $confstr;
1604 function isAllowedToCreateAccount() {
1605 return $this->isAllowed( 'createaccount' ) && !$this->isBlocked();
1609 * Set mDataLoaded, return previous value
1610 * Use this to prevent DB access in command-line scripts or similar situations
1612 function setLoaded( $loaded ) {
1613 return wfSetVar( $this->mDataLoaded, $loaded );
1617 * Get this user's personal page title.
1619 * @return Title
1620 * @public
1622 function getUserPage() {
1623 return Title::makeTitle( NS_USER, $this->getName() );
1627 * Get this user's talk page title.
1629 * @return Title
1630 * @public
1632 function getTalkPage() {
1633 $title = $this->getUserPage();
1634 return $title->getTalkPage();
1638 * @static
1640 function getMaxID() {
1641 static $res; // cache
1643 if ( isset( $res ) )
1644 return $res;
1645 else {
1646 $dbr =& wfGetDB( DB_SLAVE );
1647 return $res = $dbr->selectField( 'user', 'max(user_id)', false, 'User::getMaxID' );
1652 * Determine whether the user is a newbie. Newbies are either
1653 * anonymous IPs, or the most recently created accounts.
1654 * @return bool True if it is a newbie.
1656 function isNewbie() {
1657 return !$this->isAllowed( 'autoconfirmed' );
1661 * Check to see if the given clear-text password is one of the accepted passwords
1662 * @param string $password User password.
1663 * @return bool True if the given password is correct otherwise False.
1665 function checkPassword( $password ) {
1666 global $wgAuth, $wgMinimalPasswordLength;
1667 $this->loadFromDatabase();
1669 // Even though we stop people from creating passwords that
1670 // are shorter than this, doesn't mean people wont be able
1671 // to. Certain authentication plugins do NOT want to save
1672 // domain passwords in a mysql database, so we should
1673 // check this (incase $wgAuth->strict() is false).
1674 if( strlen( $password ) < $wgMinimalPasswordLength ) {
1675 return false;
1678 if( $wgAuth->authenticate( $this->getName(), $password ) ) {
1679 return true;
1680 } elseif( $wgAuth->strict() ) {
1681 /* Auth plugin doesn't allow local authentication */
1682 return false;
1684 $ep = $this->encryptPassword( $password );
1685 if ( 0 == strcmp( $ep, $this->mPassword ) ) {
1686 return true;
1687 } elseif ( ($this->mNewpassword != '') && (0 == strcmp( $ep, $this->mNewpassword )) ) {
1688 return true;
1689 } elseif ( function_exists( 'iconv' ) ) {
1690 # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
1691 # Check for this with iconv
1692 $cp1252hash = $this->encryptPassword( iconv( 'UTF-8', 'WINDOWS-1252', $password ) );
1693 if ( 0 == strcmp( $cp1252hash, $this->mPassword ) ) {
1694 return true;
1697 return false;
1701 * Initialize (if necessary) and return a session token value
1702 * which can be used in edit forms to show that the user's
1703 * login credentials aren't being hijacked with a foreign form
1704 * submission.
1706 * @param mixed $salt - Optional function-specific data for hash.
1707 * Use a string or an array of strings.
1708 * @return string
1709 * @public
1711 function editToken( $salt = '' ) {
1712 if( !isset( $_SESSION['wsEditToken'] ) ) {
1713 $token = $this->generateToken();
1714 $_SESSION['wsEditToken'] = $token;
1715 } else {
1716 $token = $_SESSION['wsEditToken'];
1718 if( is_array( $salt ) ) {
1719 $salt = implode( '|', $salt );
1721 return md5( $token . $salt );
1725 * Generate a hex-y looking random token for various uses.
1726 * Could be made more cryptographically sure if someone cares.
1727 * @return string
1729 function generateToken( $salt = '' ) {
1730 $token = dechex( mt_rand() ) . dechex( mt_rand() );
1731 return md5( $token . $salt );
1735 * Check given value against the token value stored in the session.
1736 * A match should confirm that the form was submitted from the
1737 * user's own login session, not a form submission from a third-party
1738 * site.
1740 * @param string $val - the input value to compare
1741 * @param string $salt - Optional function-specific data for hash
1742 * @return bool
1743 * @public
1745 function matchEditToken( $val, $salt = '' ) {
1746 global $wgMemc;
1747 $sessionToken = $this->editToken( $salt );
1748 if ( $val != $sessionToken ) {
1749 wfDebug( "User::matchEditToken: broken session data\n" );
1751 return $val == $sessionToken;
1755 * Generate a new e-mail confirmation token and send a confirmation
1756 * mail to the user's given address.
1758 * @return mixed True on success, a WikiError object on failure.
1760 function sendConfirmationMail() {
1761 global $wgContLang;
1762 $url = $this->confirmationTokenUrl( $expiration );
1763 return $this->sendMail( wfMsg( 'confirmemail_subject' ),
1764 wfMsg( 'confirmemail_body',
1765 wfGetIP(),
1766 $this->getName(),
1767 $url,
1768 $wgContLang->timeanddate( $expiration, false ) ) );
1772 * Send an e-mail to this user's account. Does not check for
1773 * confirmed status or validity.
1775 * @param string $subject
1776 * @param string $body
1777 * @param strong $from Optional from address; default $wgPasswordSender will be used otherwise.
1778 * @return mixed True on success, a WikiError object on failure.
1780 function sendMail( $subject, $body, $from = null ) {
1781 if( is_null( $from ) ) {
1782 global $wgPasswordSender;
1783 $from = $wgPasswordSender;
1786 require_once( 'UserMailer.php' );
1787 $to = new MailAddress( $this );
1788 $sender = new MailAddress( $from );
1789 $error = userMailer( $to, $sender, $subject, $body );
1791 if( $error == '' ) {
1792 return true;
1793 } else {
1794 return new WikiError( $error );
1799 * Generate, store, and return a new e-mail confirmation code.
1800 * A hash (unsalted since it's used as a key) is stored.
1801 * @param &$expiration mixed output: accepts the expiration time
1802 * @return string
1803 * @private
1805 function confirmationToken( &$expiration ) {
1806 $fname = 'User::confirmationToken';
1808 $now = time();
1809 $expires = $now + 7 * 24 * 60 * 60;
1810 $expiration = wfTimestamp( TS_MW, $expires );
1812 $token = $this->generateToken( $this->mId . $this->mEmail . $expires );
1813 $hash = md5( $token );
1815 $dbw =& wfGetDB( DB_MASTER );
1816 $dbw->update( 'user',
1817 array( 'user_email_token' => $hash,
1818 'user_email_token_expires' => $dbw->timestamp( $expires ) ),
1819 array( 'user_id' => $this->mId ),
1820 $fname );
1822 return $token;
1826 * Generate and store a new e-mail confirmation token, and return
1827 * the URL the user can use to confirm.
1828 * @param &$expiration mixed output: accepts the expiration time
1829 * @return string
1830 * @private
1832 function confirmationTokenUrl( &$expiration ) {
1833 $token = $this->confirmationToken( $expiration );
1834 $title = Title::makeTitle( NS_SPECIAL, 'Confirmemail/' . $token );
1835 return $title->getFullUrl();
1839 * Mark the e-mail address confirmed and save.
1841 function confirmEmail() {
1842 $this->loadFromDatabase();
1843 $this->mEmailAuthenticated = wfTimestampNow();
1844 $this->saveSettings();
1845 return true;
1849 * Is this user allowed to send e-mails within limits of current
1850 * site configuration?
1851 * @return bool
1853 function canSendEmail() {
1854 return $this->isEmailConfirmed();
1858 * Is this user allowed to receive e-mails within limits of current
1859 * site configuration?
1860 * @return bool
1862 function canReceiveEmail() {
1863 return $this->canSendEmail() && !$this->getOption( 'disablemail' );
1867 * Is this user's e-mail address valid-looking and confirmed within
1868 * limits of the current site configuration?
1870 * If $wgEmailAuthentication is on, this may require the user to have
1871 * confirmed their address by returning a code or using a password
1872 * sent to the address from the wiki.
1874 * @return bool
1876 function isEmailConfirmed() {
1877 global $wgEmailAuthentication;
1878 $this->loadFromDatabase();
1879 $confirmed = true;
1880 if( wfRunHooks( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) {
1881 if( $this->isAnon() )
1882 return false;
1883 if( !$this->isValidEmailAddr( $this->mEmail ) )
1884 return false;
1885 if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() )
1886 return false;
1887 return true;
1888 } else {
1889 return $confirmed;
1894 * @param array $groups list of groups
1895 * @return array list of permission key names for given groups combined
1896 * @static
1898 function getGroupPermissions( $groups ) {
1899 global $wgGroupPermissions;
1900 $rights = array();
1901 foreach( $groups as $group ) {
1902 if( isset( $wgGroupPermissions[$group] ) ) {
1903 $rights = array_merge( $rights,
1904 array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
1907 return $rights;
1911 * @param string $group key name
1912 * @return string localized descriptive name for group, if provided
1913 * @static
1915 function getGroupName( $group ) {
1916 $key = "group-$group";
1917 $name = wfMsg( $key );
1918 if( $name == '' || $name == "&lt;$key&gt;" ) {
1919 return $group;
1920 } else {
1921 return $name;
1926 * @param string $group key name
1927 * @return string localized descriptive name for member of a group, if provided
1928 * @static
1930 function getGroupMember( $group ) {
1931 $key = "group-$group-member";
1932 $name = wfMsg( $key );
1933 if( $name == '' || $name == "&lt;$key&gt;" ) {
1934 return $group;
1935 } else {
1936 return $name;
1942 * Return the set of defined explicit groups.
1943 * The * and 'user' groups are not included.
1944 * @return array
1945 * @static
1947 function getAllGroups() {
1948 global $wgGroupPermissions;
1949 return array_diff(
1950 array_keys( $wgGroupPermissions ),
1951 array( '*', 'user', 'autoconfirmed' ) );
1955 * Get the title of a page describing a particular group
1957 * @param $group Name of the group
1958 * @return mixed
1960 function getGroupPage( $group ) {
1961 $page = wfMsgForContent( 'grouppage-' . $group );
1962 if( !wfEmptyMsg( 'grouppage-' . $group, $page ) ) {
1963 $title = Title::newFromText( $page );
1964 if( is_object( $title ) )
1965 return $title;
1967 return false;