disable ipv6 cruft
[mediawiki.git] / includes / User.php
blob56d4cccd9d5b5f13eb69903ce784966f56a2478d
1 <?php
2 /**
3 * See user.txt
5 */
7 # Number of characters in user_token field
8 define( 'USER_TOKEN_LENGTH', 32 );
10 # Serialized record version
11 define( 'MW_USER_VERSION', 5 );
13 # Some punctuation to prevent editing from broken text-mangling proxies.
14 # FIXME: this is embedded unescaped into HTML attributes in various
15 # places, so we can't safely include ' or " even though we really should.
16 define( 'EDIT_TOKEN_SUFFIX', '\\' );
18 /**
19 * Thrown by User::setPassword() on error
21 class PasswordError extends MWException {
22 // NOP
25 /**
28 class User {
30 /**
31 * A list of default user toggles, i.e. boolean user preferences that are
32 * displayed by Special:Preferences as checkboxes. This list can be
33 * extended via the UserToggles hook or $wgContLang->getExtraUserToggles().
35 static public $mToggles = array(
36 'highlightbroken',
37 'justify',
38 'hideminor',
39 'extendwatchlist',
40 'usenewrc',
41 'numberheadings',
42 'showtoolbar',
43 'editondblclick',
44 'editsection',
45 'editsectiononrightclick',
46 'showtoc',
47 'rememberpassword',
48 'editwidth',
49 'watchcreations',
50 'watchdefault',
51 'watchmoves',
52 'watchdeletion',
53 'minordefault',
54 'previewontop',
55 'previewonfirst',
56 'nocache',
57 'enotifwatchlistpages',
58 'enotifusertalkpages',
59 'enotifminoredits',
60 'enotifrevealaddr',
61 'shownumberswatching',
62 'fancysig',
63 'externaleditor',
64 'externaldiff',
65 'showjumplinks',
66 'uselivepreview',
67 'forceeditsummary',
68 'watchlisthideown',
69 'watchlisthidebots',
70 'watchlisthideminor',
71 'ccmeonemails',
72 'diffonly',
75 /**
76 * List of member variables which are saved to the shared cache (memcached).
77 * Any operation which changes the corresponding database fields must
78 * call a cache-clearing function.
80 static $mCacheVars = array(
81 # user table
82 'mId',
83 'mName',
84 'mRealName',
85 'mPassword',
86 'mNewpassword',
87 'mNewpassTime',
88 'mEmail',
89 'mOptions',
90 'mTouched',
91 'mToken',
92 'mEmailAuthenticated',
93 'mEmailToken',
94 'mEmailTokenExpires',
95 'mRegistration',
96 'mEditCount',
97 # user_group table
98 'mGroups',
102 * The cache variable declarations
104 var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime,
105 $mEmail, $mOptions, $mTouched, $mToken, $mEmailAuthenticated,
106 $mEmailToken, $mEmailTokenExpires, $mRegistration, $mGroups;
109 * Whether the cache variables have been loaded
111 var $mDataLoaded;
114 * Initialisation data source if mDataLoaded==false. May be one of:
115 * defaults anonymous user initialised from class defaults
116 * name initialise from mName
117 * id initialise from mId
118 * session log in from cookies or session if possible
120 * Use the User::newFrom*() family of functions to set this.
122 var $mFrom;
125 * Lazy-initialised variables, invalidated with clearInstanceCache
127 var $mNewtalk, $mDatePreference, $mBlockedby, $mHash, $mSkin, $mRights,
128 $mBlockreason, $mBlock, $mEffectiveGroups;
130 /**
131 * Lightweight constructor for anonymous user
132 * Use the User::newFrom* factory functions for other kinds of users
134 function User() {
135 $this->clearInstanceCache( 'defaults' );
139 * Load the user table data for this object from the source given by mFrom
141 function load() {
142 if ( $this->mDataLoaded ) {
143 return;
145 wfProfileIn( __METHOD__ );
147 # Set it now to avoid infinite recursion in accessors
148 $this->mDataLoaded = true;
150 switch ( $this->mFrom ) {
151 case 'defaults':
152 $this->loadDefaults();
153 break;
154 case 'name':
155 $this->mId = self::idFromName( $this->mName );
156 if ( !$this->mId ) {
157 # Nonexistent user placeholder object
158 $this->loadDefaults( $this->mName );
159 } else {
160 $this->loadFromId();
162 break;
163 case 'id':
164 $this->loadFromId();
165 break;
166 case 'session':
167 $this->loadFromSession();
168 break;
169 default:
170 throw new MWException( "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" );
172 wfProfileOut( __METHOD__ );
176 * Load user table data given mId
177 * @return false if the ID does not exist, true otherwise
178 * @private
180 function loadFromId() {
181 global $wgMemc;
182 if ( $this->mId == 0 ) {
183 $this->loadDefaults();
184 return false;
187 # Try cache
188 $key = wfMemcKey( 'user', 'id', $this->mId );
189 $data = $wgMemc->get( $key );
190 if ( !is_array( $data ) || $data['mVersion'] < MW_USER_VERSION ) {
191 # Object is expired, load from DB
192 $data = false;
195 if ( !$data ) {
196 wfDebug( "Cache miss for user {$this->mId}\n" );
197 # Load from DB
198 if ( !$this->loadFromDatabase() ) {
199 # Can't load from ID, user is anonymous
200 return false;
203 # Save to cache
204 $data = array();
205 foreach ( self::$mCacheVars as $name ) {
206 $data[$name] = $this->$name;
208 $data['mVersion'] = MW_USER_VERSION;
209 $wgMemc->set( $key, $data );
210 } else {
211 wfDebug( "Got user {$this->mId} from cache\n" );
212 # Restore from cache
213 foreach ( self::$mCacheVars as $name ) {
214 $this->$name = $data[$name];
217 return true;
221 * Static factory method for creation from username.
223 * This is slightly less efficient than newFromId(), so use newFromId() if
224 * you have both an ID and a name handy.
226 * @param string $name Username, validated by Title:newFromText()
227 * @param mixed $validate Validate username. Takes the same parameters as
228 * User::getCanonicalName(), except that true is accepted as an alias
229 * for 'valid', for BC.
231 * @return User object, or null if the username is invalid. If the username
232 * is not present in the database, the result will be a user object with
233 * a name, zero user ID and default settings.
234 * @static
236 static function newFromName( $name, $validate = 'valid' ) {
237 if ( $validate === true ) {
238 $validate = 'valid';
240 $name = self::getCanonicalName( $name, $validate );
241 if ( $name === false ) {
242 return null;
243 } else {
244 # Create unloaded user object
245 $u = new User;
246 $u->mName = $name;
247 $u->mFrom = 'name';
248 return $u;
252 static function newFromId( $id ) {
253 $u = new User;
254 $u->mId = $id;
255 $u->mFrom = 'id';
256 return $u;
260 * Factory method to fetch whichever user has a given email confirmation code.
261 * This code is generated when an account is created or its e-mail address
262 * has changed.
264 * If the code is invalid or has expired, returns NULL.
266 * @param string $code
267 * @return User
268 * @static
270 static function newFromConfirmationCode( $code ) {
271 $dbr = wfGetDB( DB_SLAVE );
272 $id = $dbr->selectField( 'user', 'user_id', array(
273 'user_email_token' => md5( $code ),
274 'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ),
275 ) );
276 if( $id !== false ) {
277 return User::newFromId( $id );
278 } else {
279 return null;
284 * Create a new user object using data from session or cookies. If the
285 * login credentials are invalid, the result is an anonymous user.
287 * @return User
288 * @static
290 static function newFromSession() {
291 $user = new User;
292 $user->mFrom = 'session';
293 return $user;
297 * Get username given an id.
298 * @param integer $id Database user id
299 * @return string Nickname of a user
300 * @static
302 static function whoIs( $id ) {
303 $dbr = wfGetDB( DB_SLAVE );
304 return $dbr->selectField( 'user', 'user_name', array( 'user_id' => $id ), 'User::whoIs' );
308 * Get real username given an id.
309 * @param integer $id Database user id
310 * @return string Realname of a user
311 * @static
313 static function whoIsReal( $id ) {
314 $dbr = wfGetDB( DB_SLAVE );
315 return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ), 'User::whoIsReal' );
319 * Get database id given a user name
320 * @param string $name Nickname of a user
321 * @return integer|null Database user id (null: if non existent
322 * @static
324 static function idFromName( $name ) {
325 $nt = Title::newFromText( $name );
326 if( is_null( $nt ) ) {
327 # Illegal name
328 return null;
330 $dbr = wfGetDB( DB_SLAVE );
331 $s = $dbr->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), __METHOD__ );
333 if ( $s === false ) {
334 return 0;
335 } else {
336 return $s->user_id;
341 * Does the string match an anonymous IPv4 address?
343 * This function exists for username validation, in order to reject
344 * usernames which are similar in form to IP addresses. Strings such
345 * as 300.300.300.300 will return true because it looks like an IP
346 * address, despite not being strictly valid.
348 * We match \d{1,3}\.\d{1,3}\.\d{1,3}\.xxx as an anonymous IP
349 * address because the usemod software would "cloak" anonymous IP
350 * addresses like this, if we allowed accounts like this to be created
351 * new users could get the old edits of these anonymous users.
353 * @static
354 * @param string $name Nickname of a user
355 * @return bool
357 static function isIP( $name ) {
358 return preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/',$name) || User::isIPv6($name);
359 /*return preg_match("/^
360 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
361 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
362 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
363 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))
364 $/x", $name);*/
368 * Check if $name is an IPv6 IP.
370 static function isIPv6($name) {
372 * if it has any non-valid characters, it can't be a valid IPv6
373 * address.
375 if (preg_match("/[^:a-fA-F0-9]/", $name))
376 return false;
378 $parts = explode(":", $name);
379 if (count($parts) < 3)
380 return false;
381 foreach ($parts as $part) {
382 if (!preg_match("/^[0-9a-fA-F]{0,4}$/", $part))
383 return false;
385 return true;
389 * Is the input a valid username?
391 * Checks if the input is a valid username, we don't want an empty string,
392 * an IP address, anything that containins slashes (would mess up subpages),
393 * is longer than the maximum allowed username size or doesn't begin with
394 * a capital letter.
396 * @param string $name
397 * @return bool
398 * @static
400 static function isValidUserName( $name ) {
401 global $wgContLang, $wgMaxNameChars;
403 if ( $name == ''
404 || User::isIP( $name )
405 || strpos( $name, '/' ) !== false
406 || strlen( $name ) > $wgMaxNameChars
407 || $name != $wgContLang->ucfirst( $name ) )
408 return false;
410 // Ensure that the name can't be misresolved as a different title,
411 // such as with extra namespace keys at the start.
412 $parsed = Title::newFromText( $name );
413 if( is_null( $parsed )
414 || $parsed->getNamespace()
415 || strcmp( $name, $parsed->getPrefixedText() ) )
416 return false;
418 // Check an additional blacklist of troublemaker characters.
419 // Should these be merged into the title char list?
420 $unicodeBlacklist = '/[' .
421 '\x{0080}-\x{009f}' . # iso-8859-1 control chars
422 '\x{00a0}' . # non-breaking space
423 '\x{2000}-\x{200f}' . # various whitespace
424 '\x{2028}-\x{202f}' . # breaks and control chars
425 '\x{3000}' . # ideographic space
426 '\x{e000}-\x{f8ff}' . # private use
427 ']/u';
428 if( preg_match( $unicodeBlacklist, $name ) ) {
429 return false;
432 return true;
436 * Usernames which fail to pass this function will be blocked
437 * from user login and new account registrations, but may be used
438 * internally by batch processes.
440 * If an account already exists in this form, login will be blocked
441 * by a failure to pass this function.
443 * @param string $name
444 * @return bool
446 static function isUsableName( $name ) {
447 global $wgReservedUsernames;
448 return
449 // Must be a usable username, obviously ;)
450 self::isValidUserName( $name ) &&
452 // Certain names may be reserved for batch processes.
453 !in_array( $name, $wgReservedUsernames );
457 * Usernames which fail to pass this function will be blocked
458 * from new account registrations, but may be used internally
459 * either by batch processes or by user accounts which have
460 * already been created.
462 * Additional character blacklisting may be added here
463 * rather than in isValidUserName() to avoid disrupting
464 * existing accounts.
466 * @param string $name
467 * @return bool
469 static function isCreatableName( $name ) {
470 return
471 self::isUsableName( $name ) &&
473 // Registration-time character blacklisting...
474 strpos( $name, '@' ) === false;
478 * Is the input a valid password?
480 * @param string $password
481 * @return bool
482 * @static
484 static function isValidPassword( $password ) {
485 global $wgMinimalPasswordLength;
487 $result = null;
488 if( !wfRunHooks( 'isValidPassword', array( $password, &$result ) ) ) return $result;
489 if ($result === false) return false;
490 return (strlen( $password ) >= $wgMinimalPasswordLength);
494 * Does the string match roughly an email address ?
496 * There used to be a regular expression here, it got removed because it
497 * rejected valid addresses. Actually just check if there is '@' somewhere
498 * in the given address.
500 * @todo Check for RFC 2822 compilance (bug 959)
502 * @param string $addr email address
503 * @static
504 * @return bool
506 static function isValidEmailAddr ( $addr ) {
507 return ( trim( $addr ) != '' ) &&
508 (false !== strpos( $addr, '@' ) );
512 * Given unvalidated user input, return a canonical username, or false if
513 * the username is invalid.
514 * @param string $name
515 * @param mixed $validate Type of validation to use:
516 * false No validation
517 * 'valid' Valid for batch processes
518 * 'usable' Valid for batch processes and login
519 * 'creatable' Valid for batch processes, login and account creation
521 static function getCanonicalName( $name, $validate = 'valid' ) {
522 # Force usernames to capital
523 global $wgContLang;
524 $name = $wgContLang->ucfirst( $name );
526 # Clean up name according to title rules
527 $t = Title::newFromText( $name );
528 if( is_null( $t ) ) {
529 return false;
532 # Reject various classes of invalid names
533 $name = $t->getText();
534 global $wgAuth;
535 $name = $wgAuth->getCanonicalName( $t->getText() );
537 switch ( $validate ) {
538 case false:
539 break;
540 case 'valid':
541 if ( !User::isValidUserName( $name ) ) {
542 $name = false;
544 break;
545 case 'usable':
546 if ( !User::isUsableName( $name ) ) {
547 $name = false;
549 break;
550 case 'creatable':
551 if ( !User::isCreatableName( $name ) ) {
552 $name = false;
554 break;
555 default:
556 throw new MWException( 'Invalid parameter value for $validate in '.__METHOD__ );
558 return $name;
562 * Count the number of edits of a user
564 * It should not be static and some day should be merged as proper member function / deprecated -- domas
566 * @param int $uid The user ID to check
567 * @return int
568 * @static
570 static function edits( $uid ) {
571 wfProfileIn( __METHOD__ );
572 $dbr = wfGetDB( DB_SLAVE );
573 // check if the user_editcount field has been initialized
574 $field = $dbr->selectField(
575 'user', 'user_editcount',
576 array( 'user_id' => $uid ),
577 __METHOD__
580 if( $field === null ) { // it has not been initialized. do so.
581 $dbw = wfGetDb( DB_MASTER );
582 $count = $dbr->selectField(
583 'revision', 'count(*)',
584 array( 'rev_user' => $uid ),
585 __METHOD__
587 $dbw->update(
588 'user',
589 array( 'user_editcount' => $count ),
590 array( 'user_id' => $uid ),
591 __METHOD__
593 } else {
594 $count = $field;
596 wfProfileOut( __METHOD__ );
597 return $count;
601 * Return a random password. Sourced from mt_rand, so it's not particularly secure.
602 * @todo hash random numbers to improve security, like generateToken()
604 * @return string
605 * @static
607 static function randomPassword() {
608 global $wgMinimalPasswordLength;
609 $pwchars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz';
610 $l = strlen( $pwchars ) - 1;
612 $pwlength = max( 7, $wgMinimalPasswordLength );
613 $digit = mt_rand(0, $pwlength - 1);
614 $np = '';
615 for ( $i = 0; $i < $pwlength; $i++ ) {
616 $np .= $i == $digit ? chr( mt_rand(48, 57) ) : $pwchars{ mt_rand(0, $l)};
618 return $np;
622 * Set cached properties to default. Note: this no longer clears
623 * uncached lazy-initialised properties. The constructor does that instead.
625 * @private
627 function loadDefaults( $name = false ) {
628 wfProfileIn( __METHOD__ );
630 global $wgCookiePrefix;
632 $this->mId = 0;
633 $this->mName = $name;
634 $this->mRealName = '';
635 $this->mPassword = $this->mNewpassword = '';
636 $this->mNewpassTime = null;
637 $this->mEmail = '';
638 $this->mOptions = null; # Defer init
640 if ( isset( $_COOKIE[$wgCookiePrefix.'LoggedOut'] ) ) {
641 $this->mTouched = wfTimestamp( TS_MW, $_COOKIE[$wgCookiePrefix.'LoggedOut'] );
642 } else {
643 $this->mTouched = '0'; # Allow any pages to be cached
646 $this->setToken(); # Random
647 $this->mEmailAuthenticated = null;
648 $this->mEmailToken = '';
649 $this->mEmailTokenExpires = null;
650 $this->mRegistration = wfTimestamp( TS_MW );
651 $this->mGroups = array();
653 wfProfileOut( __METHOD__ );
657 * Initialise php session
658 * @deprecated use wfSetupSession()
660 function SetupSession() {
661 wfSetupSession();
665 * Load user data from the session or login cookie. If there are no valid
666 * credentials, initialises the user as an anon.
667 * @return true if the user is logged in, false otherwise
669 * @private
671 function loadFromSession() {
672 global $wgMemc, $wgCookiePrefix;
674 if ( isset( $_SESSION['wsUserID'] ) ) {
675 if ( 0 != $_SESSION['wsUserID'] ) {
676 $sId = $_SESSION['wsUserID'];
677 } else {
678 $this->loadDefaults();
679 return false;
681 } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserID"] ) ) {
682 $sId = intval( $_COOKIE["{$wgCookiePrefix}UserID"] );
683 $_SESSION['wsUserID'] = $sId;
684 } else {
685 $this->loadDefaults();
686 return false;
688 if ( isset( $_SESSION['wsUserName'] ) ) {
689 $sName = $_SESSION['wsUserName'];
690 } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserName"] ) ) {
691 $sName = $_COOKIE["{$wgCookiePrefix}UserName"];
692 $_SESSION['wsUserName'] = $sName;
693 } else {
694 $this->loadDefaults();
695 return false;
698 $passwordCorrect = FALSE;
699 $this->mId = $sId;
700 if ( !$this->loadFromId() ) {
701 # Not a valid ID, loadFromId has switched the object to anon for us
702 return false;
705 if ( isset( $_SESSION['wsToken'] ) ) {
706 $passwordCorrect = $_SESSION['wsToken'] == $this->mToken;
707 $from = 'session';
708 } else if ( isset( $_COOKIE["{$wgCookiePrefix}Token"] ) ) {
709 $passwordCorrect = $this->mToken == $_COOKIE["{$wgCookiePrefix}Token"];
710 $from = 'cookie';
711 } else {
712 # No session or persistent login cookie
713 $this->loadDefaults();
714 return false;
717 if ( ( $sName == $this->mName ) && $passwordCorrect ) {
718 wfDebug( "Logged in from $from\n" );
719 return true;
720 } else {
721 # Invalid credentials
722 wfDebug( "Can't log in from $from, invalid credentials\n" );
723 $this->loadDefaults();
724 return false;
729 * Load user and user_group data from the database
730 * $this->mId must be set, this is how the user is identified.
732 * @return true if the user exists, false if the user is anonymous
733 * @private
735 function loadFromDatabase() {
736 # Paranoia
737 $this->mId = intval( $this->mId );
739 /** Anonymous user */
740 if( !$this->mId ) {
741 $this->loadDefaults();
742 return false;
745 $dbr = wfGetDB( DB_MASTER );
746 $s = $dbr->selectRow( 'user', '*', array( 'user_id' => $this->mId ), __METHOD__ );
748 if ( $s !== false ) {
749 # Initialise user table data
750 $this->mName = $s->user_name;
751 $this->mRealName = $s->user_real_name;
752 $this->mPassword = $s->user_password;
753 $this->mNewpassword = $s->user_newpassword;
754 $this->mNewpassTime = wfTimestampOrNull( TS_MW, $s->user_newpass_time );
755 $this->mEmail = $s->user_email;
756 $this->decodeOptions( $s->user_options );
757 $this->mTouched = wfTimestamp(TS_MW,$s->user_touched);
758 $this->mToken = $s->user_token;
759 $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $s->user_email_authenticated );
760 $this->mEmailToken = $s->user_email_token;
761 $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $s->user_email_token_expires );
762 $this->mRegistration = wfTimestampOrNull( TS_MW, $s->user_registration );
763 $this->mEditCount = $s->user_editcount;
764 $this->getEditCount(); // revalidation for nulls
766 # Load group data
767 $res = $dbr->select( 'user_groups',
768 array( 'ug_group' ),
769 array( 'ug_user' => $this->mId ),
770 __METHOD__ );
771 $this->mGroups = array();
772 while( $row = $dbr->fetchObject( $res ) ) {
773 $this->mGroups[] = $row->ug_group;
775 return true;
776 } else {
777 # Invalid user_id
778 $this->mId = 0;
779 $this->loadDefaults();
780 return false;
785 * Clear various cached data stored in this object.
786 * @param string $reloadFrom Reload user and user_groups table data from a
787 * given source. May be "name", "id", "defaults", "session" or false for
788 * no reload.
790 function clearInstanceCache( $reloadFrom = false ) {
791 $this->mNewtalk = -1;
792 $this->mDatePreference = null;
793 $this->mBlockedby = -1; # Unset
794 $this->mHash = false;
795 $this->mSkin = null;
796 $this->mRights = null;
797 $this->mEffectiveGroups = null;
799 if ( $reloadFrom ) {
800 $this->mDataLoaded = false;
801 $this->mFrom = $reloadFrom;
806 * Combine the language default options with any site-specific options
807 * and add the default language variants.
808 * Not really private cause it's called by Language class
809 * @return array
810 * @static
811 * @private
813 static function getDefaultOptions() {
814 global $wgNamespacesToBeSearchedDefault;
816 * Site defaults will override the global/language defaults
818 global $wgDefaultUserOptions, $wgContLang;
819 $defOpt = $wgDefaultUserOptions + $wgContLang->getDefaultUserOptionOverrides();
822 * default language setting
824 $variant = $wgContLang->getPreferredVariant( false );
825 $defOpt['variant'] = $variant;
826 $defOpt['language'] = $variant;
828 foreach( $wgNamespacesToBeSearchedDefault as $nsnum => $val ) {
829 $defOpt['searchNs'.$nsnum] = $val;
831 return $defOpt;
835 * Get a given default option value.
837 * @param string $opt
838 * @return string
839 * @static
840 * @public
842 function getDefaultOption( $opt ) {
843 $defOpts = User::getDefaultOptions();
844 if( isset( $defOpts[$opt] ) ) {
845 return $defOpts[$opt];
846 } else {
847 return '';
852 * Get a list of user toggle names
853 * @return array
855 static function getToggles() {
856 global $wgContLang;
857 $extraToggles = array();
858 wfRunHooks( 'UserToggles', array( &$extraToggles ) );
859 return array_merge( self::$mToggles, $extraToggles, $wgContLang->getExtraUserToggles() );
864 * Get blocking information
865 * @private
866 * @param bool $bFromSlave Specify whether to check slave or master. To improve performance,
867 * non-critical checks are done against slaves. Check when actually saving should be done against
868 * master.
870 function getBlockedStatus( $bFromSlave = true ) {
871 global $wgEnableSorbs, $wgProxyWhitelist;
873 if ( -1 != $this->mBlockedby ) {
874 wfDebug( "User::getBlockedStatus: already loaded.\n" );
875 return;
878 wfProfileIn( __METHOD__ );
879 wfDebug( __METHOD__.": checking...\n" );
881 $this->mBlockedby = 0;
882 $this->mHideName = 0;
883 $ip = wfGetIP();
885 if ($this->isAllowed( 'ipblock-exempt' ) ) {
886 # Exempt from all types of IP-block
887 $ip = '';
890 # User/IP blocking
891 $this->mBlock = new Block();
892 $this->mBlock->fromMaster( !$bFromSlave );
893 if ( $this->mBlock->load( $ip , $this->mId ) ) {
894 wfDebug( __METHOD__.": Found block.\n" );
895 $this->mBlockedby = $this->mBlock->mBy;
896 $this->mBlockreason = $this->mBlock->mReason;
897 $this->mHideName = $this->mBlock->mHideName;
898 if ( $this->isLoggedIn() ) {
899 $this->spreadBlock();
901 } else {
902 $this->mBlock = null;
903 wfDebug( __METHOD__.": No block.\n" );
906 # Proxy blocking
907 if ( !$this->isAllowed('proxyunbannable') && !in_array( $ip, $wgProxyWhitelist ) ) {
909 # Local list
910 if ( wfIsLocallyBlockedProxy( $ip ) ) {
911 $this->mBlockedby = wfMsg( 'proxyblocker' );
912 $this->mBlockreason = wfMsg( 'proxyblockreason' );
915 # DNSBL
916 if ( !$this->mBlockedby && $wgEnableSorbs && !$this->getID() ) {
917 if ( $this->inSorbsBlacklist( $ip ) ) {
918 $this->mBlockedby = wfMsg( 'sorbs' );
919 $this->mBlockreason = wfMsg( 'sorbsreason' );
924 # Extensions
925 wfRunHooks( 'GetBlockedStatus', array( &$this ) );
927 wfProfileOut( __METHOD__ );
930 function inSorbsBlacklist( $ip ) {
931 global $wgEnableSorbs, $wgSorbsUrl;
933 return $wgEnableSorbs &&
934 $this->inDnsBlacklist( $ip, $wgSorbsUrl );
937 function inDnsBlacklist( $ip, $base ) {
938 wfProfileIn( __METHOD__ );
940 $found = false;
941 $host = '';
943 $m = array();
944 if ( preg_match( '/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $ip, $m ) ) {
945 # Make hostname
946 for ( $i=4; $i>=1; $i-- ) {
947 $host .= $m[$i] . '.';
949 $host .= $base;
951 # Send query
952 $ipList = gethostbynamel( $host );
954 if ( $ipList ) {
955 wfDebug( "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" );
956 $found = true;
957 } else {
958 wfDebug( "Requested $host, not found in $base.\n" );
962 wfProfileOut( __METHOD__ );
963 return $found;
967 * Is this user subject to rate limiting?
969 * @return bool
971 public function isPingLimitable() {
972 global $wgRateLimitsExcludedGroups;
973 return array_intersect($this->getEffectiveGroups(), $wgRateLimitsExcludedGroups) != array();
977 * Primitive rate limits: enforce maximum actions per time period
978 * to put a brake on flooding.
980 * Note: when using a shared cache like memcached, IP-address
981 * last-hit counters will be shared across wikis.
983 * @return bool true if a rate limiter was tripped
984 * @public
986 function pingLimiter( $action='edit' ) {
988 # Call the 'PingLimiter' hook
989 $result = false;
990 if( !wfRunHooks( 'PingLimiter', array( &$this, $action, $result ) ) ) {
991 return $result;
994 global $wgRateLimits, $wgRateLimitsExcludedGroups;
995 if( !isset( $wgRateLimits[$action] ) ) {
996 return false;
999 # Some groups shouldn't trigger the ping limiter, ever
1000 if( !$this->isPingLimitable() )
1001 return false;
1003 global $wgMemc, $wgRateLimitLog;
1004 wfProfileIn( __METHOD__ );
1006 $limits = $wgRateLimits[$action];
1007 $keys = array();
1008 $id = $this->getId();
1009 $ip = wfGetIP();
1011 if( isset( $limits['anon'] ) && $id == 0 ) {
1012 $keys[wfMemcKey( 'limiter', $action, 'anon' )] = $limits['anon'];
1015 if( isset( $limits['user'] ) && $id != 0 ) {
1016 $keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $limits['user'];
1018 if( $this->isNewbie() ) {
1019 if( isset( $limits['newbie'] ) && $id != 0 ) {
1020 $keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $limits['newbie'];
1022 if( isset( $limits['ip'] ) ) {
1023 $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip'];
1025 $matches = array();
1026 if( isset( $limits['subnet'] ) && preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
1027 $subnet = $matches[1];
1028 $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
1032 $triggered = false;
1033 foreach( $keys as $key => $limit ) {
1034 list( $max, $period ) = $limit;
1035 $summary = "(limit $max in {$period}s)";
1036 $count = $wgMemc->get( $key );
1037 if( $count ) {
1038 if( $count > $max ) {
1039 wfDebug( __METHOD__.": tripped! $key at $count $summary\n" );
1040 if( $wgRateLimitLog ) {
1041 @error_log( wfTimestamp( TS_MW ) . ' ' . wfWikiID() . ': ' . $this->getName() . " tripped $key at $count $summary\n", 3, $wgRateLimitLog );
1043 $triggered = true;
1044 } else {
1045 wfDebug( __METHOD__.": ok. $key at $count $summary\n" );
1047 } else {
1048 wfDebug( __METHOD__.": adding record for $key $summary\n" );
1049 $wgMemc->add( $key, 1, intval( $period ) );
1051 $wgMemc->incr( $key );
1054 wfProfileOut( __METHOD__ );
1055 return $triggered;
1059 * Check if user is blocked
1060 * @return bool True if blocked, false otherwise
1062 function isBlocked( $bFromSlave = true ) { // hacked from false due to horrible probs on site
1063 wfDebug( "User::isBlocked: enter\n" );
1064 $this->getBlockedStatus( $bFromSlave );
1065 return $this->mBlockedby !== 0;
1069 * Check if user is blocked from editing a particular article
1071 function isBlockedFrom( $title, $bFromSlave = false ) {
1072 global $wgBlockAllowsUTEdit;
1073 wfProfileIn( __METHOD__ );
1074 wfDebug( __METHOD__.": enter\n" );
1076 wfDebug( __METHOD__.": asking isBlocked()\n" );
1077 $blocked = $this->isBlocked( $bFromSlave );
1078 # If a user's name is suppressed, they cannot make edits anywhere
1079 if ( !$this->mHideName && $wgBlockAllowsUTEdit && $title->getText() === $this->getName() &&
1080 $title->getNamespace() == NS_USER_TALK ) {
1081 $blocked = false;
1082 wfDebug( __METHOD__.": self-talk page, ignoring any blocks\n" );
1084 wfProfileOut( __METHOD__ );
1085 return $blocked;
1089 * Get name of blocker
1090 * @return string name of blocker
1092 function blockedBy() {
1093 $this->getBlockedStatus();
1094 return $this->mBlockedby;
1098 * Get blocking reason
1099 * @return string Blocking reason
1101 function blockedFor() {
1102 $this->getBlockedStatus();
1103 return $this->mBlockreason;
1107 * Get the user ID. Returns 0 if the user is anonymous or nonexistent.
1109 function getID() {
1110 $this->load();
1111 return $this->mId;
1115 * Set the user and reload all fields according to that ID
1116 * @deprecated use User::newFromId()
1118 function setID( $v ) {
1119 $this->mId = $v;
1120 $this->clearInstanceCache( 'id' );
1124 * Get the user name, or the IP for anons
1126 function getName() {
1127 if ( !$this->mDataLoaded && $this->mFrom == 'name' ) {
1128 # Special case optimisation
1129 return $this->mName;
1130 } else {
1131 $this->load();
1132 if ( $this->mName === false ) {
1133 $this->mName = wfGetIP();
1135 # Clean up IPs
1136 return IP::sanitizeIP($this->mName);
1141 * Set the user name.
1143 * This does not reload fields from the database according to the given
1144 * name. Rather, it is used to create a temporary "nonexistent user" for
1145 * later addition to the database. It can also be used to set the IP
1146 * address for an anonymous user to something other than the current
1147 * remote IP.
1149 * User::newFromName() has rougly the same function, when the named user
1150 * does not exist.
1152 function setName( $str ) {
1153 $this->load();
1154 $this->mName = $str;
1158 * Return the title dbkey form of the name, for eg user pages.
1159 * @return string
1160 * @public
1162 function getTitleKey() {
1163 return str_replace( ' ', '_', $this->getName() );
1166 function getNewtalk() {
1167 $this->load();
1169 # Load the newtalk status if it is unloaded (mNewtalk=-1)
1170 if( $this->mNewtalk === -1 ) {
1171 $this->mNewtalk = false; # reset talk page status
1173 # Check memcached separately for anons, who have no
1174 # entire User object stored in there.
1175 if( !$this->mId ) {
1176 global $wgMemc;
1177 $key = wfMemcKey( 'newtalk', 'ip', $this->getName() );
1178 $newtalk = $wgMemc->get( $key );
1179 if( $newtalk != "" ) {
1180 $this->mNewtalk = (bool)$newtalk;
1181 } else {
1182 $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName() );
1183 $wgMemc->set( $key, (int)$this->mNewtalk, time() + 1800 );
1185 } else {
1186 $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
1190 return (bool)$this->mNewtalk;
1194 * Return the talk page(s) this user has new messages on.
1196 function getNewMessageLinks() {
1197 $talks = array();
1198 if (!wfRunHooks('UserRetrieveNewTalks', array(&$this, &$talks)))
1199 return $talks;
1201 if (!$this->getNewtalk())
1202 return array();
1203 $up = $this->getUserPage();
1204 $utp = $up->getTalkPage();
1205 return array(array("wiki" => wfWikiID(), "link" => $utp->getLocalURL()));
1210 * Perform a user_newtalk check on current slaves; if the memcached data
1211 * is funky we don't want newtalk state to get stuck on save, as that's
1212 * damn annoying.
1214 * @param string $field
1215 * @param mixed $id
1216 * @return bool
1217 * @private
1219 function checkNewtalk( $field, $id ) {
1220 $dbr = wfGetDB( DB_SLAVE );
1221 $ok = $dbr->selectField( 'user_newtalk', $field,
1222 array( $field => $id ), __METHOD__ );
1223 return $ok !== false;
1227 * Add or update the
1228 * @param string $field
1229 * @param mixed $id
1230 * @private
1232 function updateNewtalk( $field, $id ) {
1233 if( $this->checkNewtalk( $field, $id ) ) {
1234 wfDebug( __METHOD__." already set ($field, $id), ignoring\n" );
1235 return false;
1237 $dbw = wfGetDB( DB_MASTER );
1238 $dbw->insert( 'user_newtalk',
1239 array( $field => $id ),
1240 __METHOD__,
1241 'IGNORE' );
1242 wfDebug( __METHOD__.": set on ($field, $id)\n" );
1243 return true;
1247 * Clear the new messages flag for the given user
1248 * @param string $field
1249 * @param mixed $id
1250 * @private
1252 function deleteNewtalk( $field, $id ) {
1253 if( !$this->checkNewtalk( $field, $id ) ) {
1254 wfDebug( __METHOD__.": already gone ($field, $id), ignoring\n" );
1255 return false;
1257 $dbw = wfGetDB( DB_MASTER );
1258 $dbw->delete( 'user_newtalk',
1259 array( $field => $id ),
1260 __METHOD__ );
1261 wfDebug( __METHOD__.": killed on ($field, $id)\n" );
1262 return true;
1266 * Update the 'You have new messages!' status.
1267 * @param bool $val
1269 function setNewtalk( $val ) {
1270 if( wfReadOnly() ) {
1271 return;
1274 $this->load();
1275 $this->mNewtalk = $val;
1277 if( $this->isAnon() ) {
1278 $field = 'user_ip';
1279 $id = $this->getName();
1280 } else {
1281 $field = 'user_id';
1282 $id = $this->getId();
1285 if( $val ) {
1286 $changed = $this->updateNewtalk( $field, $id );
1287 } else {
1288 $changed = $this->deleteNewtalk( $field, $id );
1291 if( $changed ) {
1292 if( $this->isAnon() ) {
1293 // Anons have a separate memcached space, since
1294 // user records aren't kept for them.
1295 global $wgMemc;
1296 $key = wfMemcKey( 'newtalk', 'ip', $val );
1297 $wgMemc->set( $key, $val ? 1 : 0 );
1298 } else {
1299 if( $val ) {
1300 // Make sure the user page is watched, so a notification
1301 // will be sent out if enabled.
1302 $this->addWatch( $this->getTalkPage() );
1305 $this->invalidateCache();
1310 * Generate a current or new-future timestamp to be stored in the
1311 * user_touched field when we update things.
1313 private static function newTouchedTimestamp() {
1314 global $wgClockSkewFudge;
1315 return wfTimestamp( TS_MW, time() + $wgClockSkewFudge );
1319 * Clear user data from memcached.
1320 * Use after applying fun updates to the database; caller's
1321 * responsibility to update user_touched if appropriate.
1323 * Called implicitly from invalidateCache() and saveSettings().
1325 private function clearSharedCache() {
1326 if( $this->mId ) {
1327 global $wgMemc;
1328 $wgMemc->delete( wfMemcKey( 'user', 'id', $this->mId ) );
1333 * Immediately touch the user data cache for this account.
1334 * Updates user_touched field, and removes account data from memcached
1335 * for reload on the next hit.
1337 function invalidateCache() {
1338 $this->load();
1339 if( $this->mId ) {
1340 $this->mTouched = self::newTouchedTimestamp();
1342 $dbw = wfGetDB( DB_MASTER );
1343 $dbw->update( 'user',
1344 array( 'user_touched' => $dbw->timestamp( $this->mTouched ) ),
1345 array( 'user_id' => $this->mId ),
1346 __METHOD__ );
1348 $this->clearSharedCache();
1352 function validateCache( $timestamp ) {
1353 $this->load();
1354 return ($timestamp >= $this->mTouched);
1358 * Encrypt a password.
1359 * It can eventuall salt a password @see User::addSalt()
1360 * @param string $p clear Password.
1361 * @return string Encrypted password.
1363 function encryptPassword( $p ) {
1364 $this->load();
1365 return wfEncryptPassword( $this->mId, $p );
1369 * Set the password and reset the random token
1370 * Calls through to authentication plugin if necessary;
1371 * will have no effect if the auth plugin refuses to
1372 * pass the change through or if the legal password
1373 * checks fail.
1375 * As a special case, setting the password to null
1376 * wipes it, so the account cannot be logged in until
1377 * a new password is set, for instance via e-mail.
1379 * @param string $str
1380 * @throws PasswordError on failure
1382 function setPassword( $str ) {
1383 global $wgAuth;
1385 if( $str !== null ) {
1386 if( !$wgAuth->allowPasswordChange() ) {
1387 throw new PasswordError( wfMsg( 'password-change-forbidden' ) );
1390 if( !$this->isValidPassword( $str ) ) {
1391 global $wgMinimalPasswordLength;
1392 throw new PasswordError( wfMsg( 'passwordtooshort',
1393 $wgMinimalPasswordLength ) );
1397 if( !$wgAuth->setPassword( $this, $str ) ) {
1398 throw new PasswordError( wfMsg( 'externaldberror' ) );
1401 $this->setInternalPassword( $str );
1403 return true;
1407 * Set the password and reset the random token no matter
1408 * what.
1410 * @param string $str
1412 function setInternalPassword( $str ) {
1413 $this->load();
1414 $this->setToken();
1416 if( $str === null ) {
1417 // Save an invalid hash...
1418 $this->mPassword = '';
1419 } else {
1420 $this->mPassword = $this->encryptPassword( $str );
1422 $this->mNewpassword = '';
1423 $this->mNewpassTime = null;
1426 * Set the random token (used for persistent authentication)
1427 * Called from loadDefaults() among other places.
1428 * @private
1430 function setToken( $token = false ) {
1431 global $wgSecretKey, $wgProxyKey;
1432 $this->load();
1433 if ( !$token ) {
1434 if ( $wgSecretKey ) {
1435 $key = $wgSecretKey;
1436 } elseif ( $wgProxyKey ) {
1437 $key = $wgProxyKey;
1438 } else {
1439 $key = microtime();
1441 $this->mToken = md5( $key . mt_rand( 0, 0x7fffffff ) . wfWikiID() . $this->mId );
1442 } else {
1443 $this->mToken = $token;
1447 function setCookiePassword( $str ) {
1448 $this->load();
1449 $this->mCookiePassword = md5( $str );
1453 * Set the password for a password reminder or new account email
1454 * Sets the user_newpass_time field if $throttle is true
1456 function setNewpassword( $str, $throttle = true ) {
1457 $this->load();
1458 $this->mNewpassword = $this->encryptPassword( $str );
1459 if ( $throttle ) {
1460 $this->mNewpassTime = wfTimestampNow();
1465 * Returns true if a password reminder email has already been sent within
1466 * the last $wgPasswordReminderResendTime hours
1468 function isPasswordReminderThrottled() {
1469 global $wgPasswordReminderResendTime;
1470 $this->load();
1471 if ( !$this->mNewpassTime || !$wgPasswordReminderResendTime ) {
1472 return false;
1474 $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgPasswordReminderResendTime * 3600;
1475 return time() < $expiry;
1478 function getEmail() {
1479 $this->load();
1480 return $this->mEmail;
1483 function getEmailAuthenticationTimestamp() {
1484 $this->load();
1485 return $this->mEmailAuthenticated;
1488 function setEmail( $str ) {
1489 $this->load();
1490 $this->mEmail = $str;
1493 function getRealName() {
1494 $this->load();
1495 return $this->mRealName;
1498 function setRealName( $str ) {
1499 $this->load();
1500 $this->mRealName = $str;
1504 * @param string $oname The option to check
1505 * @param string $defaultOverride A default value returned if the option does not exist
1506 * @return string
1508 function getOption( $oname, $defaultOverride = '' ) {
1509 $this->load();
1511 if ( is_null( $this->mOptions ) ) {
1512 if($defaultOverride != '') {
1513 return $defaultOverride;
1515 $this->mOptions = User::getDefaultOptions();
1518 if ( array_key_exists( $oname, $this->mOptions ) ) {
1519 return trim( $this->mOptions[$oname] );
1520 } else {
1521 return $defaultOverride;
1526 * Get the user's date preference, including some important migration for
1527 * old user rows.
1529 function getDatePreference() {
1530 if ( is_null( $this->mDatePreference ) ) {
1531 global $wgLang;
1532 $value = $this->getOption( 'date' );
1533 $map = $wgLang->getDatePreferenceMigrationMap();
1534 if ( isset( $map[$value] ) ) {
1535 $value = $map[$value];
1537 $this->mDatePreference = $value;
1539 return $this->mDatePreference;
1543 * @param string $oname The option to check
1544 * @return bool False if the option is not selected, true if it is
1546 function getBoolOption( $oname ) {
1547 return (bool)$this->getOption( $oname );
1551 * Get an option as an integer value from the source string.
1552 * @param string $oname The option to check
1553 * @param int $default Optional value to return if option is unset/blank.
1554 * @return int
1556 function getIntOption( $oname, $default=0 ) {
1557 $val = $this->getOption( $oname );
1558 if( $val == '' ) {
1559 $val = $default;
1561 return intval( $val );
1564 function setOption( $oname, $val ) {
1565 $this->load();
1566 if ( is_null( $this->mOptions ) ) {
1567 $this->mOptions = User::getDefaultOptions();
1569 if ( $oname == 'skin' ) {
1570 # Clear cached skin, so the new one displays immediately in Special:Preferences
1571 unset( $this->mSkin );
1573 // Filter out any newlines that may have passed through input validation.
1574 // Newlines are used to separate items in the options blob.
1575 $val = str_replace( "\r\n", "\n", $val );
1576 $val = str_replace( "\r", "\n", $val );
1577 $val = str_replace( "\n", " ", $val );
1578 $this->mOptions[$oname] = $val;
1581 function getRights() {
1582 if ( is_null( $this->mRights ) ) {
1583 $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() );
1585 return $this->mRights;
1589 * Get the list of explicit group memberships this user has.
1590 * The implicit * and user groups are not included.
1591 * @return array of strings
1593 function getGroups() {
1594 $this->load();
1595 return $this->mGroups;
1599 * Get the list of implicit group memberships this user has.
1600 * This includes all explicit groups, plus 'user' if logged in
1601 * and '*' for all accounts.
1602 * @param boolean $recache Don't use the cache
1603 * @return array of strings
1605 function getEffectiveGroups( $recache = false ) {
1606 if ( $recache || is_null( $this->mEffectiveGroups ) ) {
1607 $this->load();
1608 $this->mEffectiveGroups = $this->mGroups;
1609 $this->mEffectiveGroups[] = '*';
1610 if( $this->mId ) {
1611 $this->mEffectiveGroups[] = 'user';
1613 global $wgAutoConfirmAge, $wgAutoConfirmCount;
1615 $accountAge = time() - wfTimestampOrNull( TS_UNIX, $this->mRegistration );
1616 if( $accountAge >= $wgAutoConfirmAge && $this->getEditCount() >= $wgAutoConfirmCount ) {
1617 $this->mEffectiveGroups[] = 'autoconfirmed';
1619 # Implicit group for users whose email addresses are confirmed
1620 global $wgEmailAuthentication;
1621 if( self::isValidEmailAddr( $this->mEmail ) ) {
1622 if( $wgEmailAuthentication ) {
1623 if( $this->mEmailAuthenticated )
1624 $this->mEffectiveGroups[] = 'emailconfirmed';
1625 } else {
1626 $this->mEffectiveGroups[] = 'emailconfirmed';
1631 return $this->mEffectiveGroups;
1634 /* Return the edit count for the user. This is where User::edits should have been */
1635 function getEditCount() {
1636 if ($this->mId) {
1637 if ( !isset( $this->mEditCount ) ) {
1638 /* Populate the count, if it has not been populated yet */
1639 $this->mEditCount = User::edits($this->mId);
1641 return $this->mEditCount;
1642 } else {
1643 /* nil */
1644 return null;
1649 * Add the user to the given group.
1650 * This takes immediate effect.
1651 * @param string $group
1653 function addGroup( $group ) {
1654 $this->load();
1655 $dbw = wfGetDB( DB_MASTER );
1656 if( $this->getId() ) {
1657 $dbw->insert( 'user_groups',
1658 array(
1659 'ug_user' => $this->getID(),
1660 'ug_group' => $group,
1662 'User::addGroup',
1663 array( 'IGNORE' ) );
1666 $this->mGroups[] = $group;
1667 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
1669 $this->invalidateCache();
1673 * Remove the user from the given group.
1674 * This takes immediate effect.
1675 * @param string $group
1677 function removeGroup( $group ) {
1678 $this->load();
1679 $dbw = wfGetDB( DB_MASTER );
1680 $dbw->delete( 'user_groups',
1681 array(
1682 'ug_user' => $this->getID(),
1683 'ug_group' => $group,
1685 'User::removeGroup' );
1687 $this->mGroups = array_diff( $this->mGroups, array( $group ) );
1688 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
1690 $this->invalidateCache();
1695 * A more legible check for non-anonymousness.
1696 * Returns true if the user is not an anonymous visitor.
1698 * @return bool
1700 function isLoggedIn() {
1701 return( $this->getID() != 0 );
1705 * A more legible check for anonymousness.
1706 * Returns true if the user is an anonymous visitor.
1708 * @return bool
1710 function isAnon() {
1711 return !$this->isLoggedIn();
1715 * Whether the user is a bot
1716 * @deprecated
1718 function isBot() {
1719 return $this->isAllowed( 'bot' );
1723 * Check if user is allowed to access a feature / make an action
1724 * @param string $action Action to be checked
1725 * @return boolean True: action is allowed, False: action should not be allowed
1727 function isAllowed($action='') {
1728 if ( $action === '' )
1729 // In the spirit of DWIM
1730 return true;
1732 return in_array( $action, $this->getRights() );
1736 * Load a skin if it doesn't exist or return it
1737 * @todo FIXME : need to check the old failback system [AV]
1739 function &getSkin() {
1740 global $wgRequest;
1741 if ( ! isset( $this->mSkin ) ) {
1742 wfProfileIn( __METHOD__ );
1744 # get the user skin
1745 $userSkin = $this->getOption( 'skin' );
1746 $userSkin = $wgRequest->getVal('useskin', $userSkin);
1748 $this->mSkin =& Skin::newFromKey( $userSkin );
1749 wfProfileOut( __METHOD__ );
1751 return $this->mSkin;
1754 /**#@+
1755 * @param string $title Article title to look at
1759 * Check watched status of an article
1760 * @return bool True if article is watched
1762 function isWatched( $title ) {
1763 $wl = WatchedItem::fromUserTitle( $this, $title );
1764 return $wl->isWatched();
1768 * Watch an article
1770 function addWatch( $title ) {
1771 $wl = WatchedItem::fromUserTitle( $this, $title );
1772 $wl->addWatch();
1773 $this->invalidateCache();
1777 * Stop watching an article
1779 function removeWatch( $title ) {
1780 $wl = WatchedItem::fromUserTitle( $this, $title );
1781 $wl->removeWatch();
1782 $this->invalidateCache();
1786 * Clear the user's notification timestamp for the given title.
1787 * If e-notif e-mails are on, they will receive notification mails on
1788 * the next change of the page if it's watched etc.
1790 function clearNotification( &$title ) {
1791 global $wgUser, $wgUseEnotif;
1793 # Do nothing if the database is locked to writes
1794 if( wfReadOnly() ) {
1795 return;
1798 if ($title->getNamespace() == NS_USER_TALK &&
1799 $title->getText() == $this->getName() ) {
1800 if (!wfRunHooks('UserClearNewTalkNotification', array(&$this)))
1801 return;
1802 $this->setNewtalk( false );
1805 if( !$wgUseEnotif ) {
1806 return;
1809 if( $this->isAnon() ) {
1810 // Nothing else to do...
1811 return;
1814 // Only update the timestamp if the page is being watched.
1815 // The query to find out if it is watched is cached both in memcached and per-invocation,
1816 // and when it does have to be executed, it can be on a slave
1817 // If this is the user's newtalk page, we always update the timestamp
1818 if ($title->getNamespace() == NS_USER_TALK &&
1819 $title->getText() == $wgUser->getName())
1821 $watched = true;
1822 } elseif ( $this->getID() == $wgUser->getID() ) {
1823 $watched = $title->userIsWatching();
1824 } else {
1825 $watched = true;
1828 // If the page is watched by the user (or may be watched), update the timestamp on any
1829 // any matching rows
1830 if ( $watched ) {
1831 $dbw = wfGetDB( DB_MASTER );
1832 $dbw->update( 'watchlist',
1833 array( /* SET */
1834 'wl_notificationtimestamp' => NULL
1835 ), array( /* WHERE */
1836 'wl_title' => $title->getDBkey(),
1837 'wl_namespace' => $title->getNamespace(),
1838 'wl_user' => $this->getID()
1839 ), 'User::clearLastVisited'
1844 /**#@-*/
1847 * Resets all of the given user's page-change notification timestamps.
1848 * If e-notif e-mails are on, they will receive notification mails on
1849 * the next change of any watched page.
1851 * @param int $currentUser user ID number
1852 * @public
1854 function clearAllNotifications( $currentUser ) {
1855 global $wgUseEnotif;
1856 if ( !$wgUseEnotif ) {
1857 $this->setNewtalk( false );
1858 return;
1860 if( $currentUser != 0 ) {
1862 $dbw = wfGetDB( DB_MASTER );
1863 $dbw->update( 'watchlist',
1864 array( /* SET */
1865 'wl_notificationtimestamp' => NULL
1866 ), array( /* WHERE */
1867 'wl_user' => $currentUser
1868 ), 'UserMailer::clearAll'
1871 # we also need to clear here the "you have new message" notification for the own user_talk page
1872 # This is cleared one page view later in Article::viewUpdates();
1877 * @private
1878 * @return string Encoding options
1880 function encodeOptions() {
1881 $this->load();
1882 if ( is_null( $this->mOptions ) ) {
1883 $this->mOptions = User::getDefaultOptions();
1885 $a = array();
1886 foreach ( $this->mOptions as $oname => $oval ) {
1887 array_push( $a, $oname.'='.$oval );
1889 $s = implode( "\n", $a );
1890 return $s;
1894 * @private
1896 function decodeOptions( $str ) {
1897 $this->mOptions = array();
1898 $a = explode( "\n", $str );
1899 foreach ( $a as $s ) {
1900 $m = array();
1901 if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
1902 $this->mOptions[$m[1]] = $m[2];
1907 function setCookies() {
1908 global $wgCookieExpiration, $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookiePrefix;
1909 $this->load();
1910 if ( 0 == $this->mId ) return;
1911 $exp = time() + $wgCookieExpiration;
1913 $_SESSION['wsUserID'] = $this->mId;
1914 setcookie( $wgCookiePrefix.'UserID', $this->mId, $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1916 $_SESSION['wsUserName'] = $this->getName();
1917 setcookie( $wgCookiePrefix.'UserName', $this->getName(), $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1919 $_SESSION['wsToken'] = $this->mToken;
1920 if ( 1 == $this->getOption( 'rememberpassword' ) ) {
1921 setcookie( $wgCookiePrefix.'Token', $this->mToken, $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1922 } else {
1923 setcookie( $wgCookiePrefix.'Token', '', time() - 3600 );
1928 * Logout user
1929 * Clears the cookies and session, resets the instance cache
1931 function logout() {
1932 global $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookiePrefix;
1933 $this->clearInstanceCache( 'defaults' );
1935 $_SESSION['wsUserID'] = 0;
1937 setcookie( $wgCookiePrefix.'UserID', '', time() - 3600, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1938 setcookie( $wgCookiePrefix.'Token', '', time() - 3600, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1940 # Remember when user logged out, to prevent seeing cached pages
1941 setcookie( $wgCookiePrefix.'LoggedOut', wfTimestampNow(), time() + 86400, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1945 * Save object settings into database
1946 * @todo Only rarely do all these fields need to be set!
1948 function saveSettings() {
1949 $this->load();
1950 if ( wfReadOnly() ) { return; }
1951 if ( 0 == $this->mId ) { return; }
1953 $this->mTouched = self::newTouchedTimestamp();
1955 $dbw = wfGetDB( DB_MASTER );
1956 $dbw->update( 'user',
1957 array( /* SET */
1958 'user_name' => $this->mName,
1959 'user_password' => $this->mPassword,
1960 'user_newpassword' => $this->mNewpassword,
1961 'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ),
1962 'user_real_name' => $this->mRealName,
1963 'user_email' => $this->mEmail,
1964 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
1965 'user_options' => $this->encodeOptions(),
1966 'user_touched' => $dbw->timestamp($this->mTouched),
1967 'user_token' => $this->mToken
1968 ), array( /* WHERE */
1969 'user_id' => $this->mId
1970 ), __METHOD__
1972 $this->clearSharedCache();
1977 * Checks if a user with the given name exists, returns the ID
1979 function idForName() {
1980 $s = trim( $this->getName() );
1981 if ( 0 == strcmp( '', $s ) ) return 0;
1983 $dbr = wfGetDB( DB_SLAVE );
1984 $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), __METHOD__ );
1985 if ( $id === false ) {
1986 $id = 0;
1988 return $id;
1992 * Add a user to the database, return the user object
1994 * @param string $name The user's name
1995 * @param array $params Associative array of non-default parameters to save to the database:
1996 * password The user's password. Password logins will be disabled if this is omitted.
1997 * newpassword A temporary password mailed to the user
1998 * email The user's email address
1999 * email_authenticated The email authentication timestamp
2000 * real_name The user's real name
2001 * options An associative array of non-default options
2002 * token Random authentication token. Do not set.
2003 * registration Registration timestamp. Do not set.
2005 * @return User object, or null if the username already exists
2007 static function createNew( $name, $params = array() ) {
2008 $user = new User;
2009 $user->load();
2010 if ( isset( $params['options'] ) ) {
2011 $user->mOptions = $params['options'] + $user->mOptions;
2012 unset( $params['options'] );
2014 $dbw = wfGetDB( DB_MASTER );
2015 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
2016 $fields = array(
2017 'user_id' => $seqVal,
2018 'user_name' => $name,
2019 'user_password' => $user->mPassword,
2020 'user_newpassword' => $user->mNewpassword,
2021 'user_newpass_time' => $dbw->timestamp( $user->mNewpassTime ),
2022 'user_email' => $user->mEmail,
2023 'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ),
2024 'user_real_name' => $user->mRealName,
2025 'user_options' => $user->encodeOptions(),
2026 'user_token' => $user->mToken,
2027 'user_registration' => $dbw->timestamp( $user->mRegistration ),
2028 'user_editcount' => 0,
2030 foreach ( $params as $name => $value ) {
2031 $fields["user_$name"] = $value;
2033 $dbw->insert( 'user', $fields, __METHOD__, array( 'IGNORE' ) );
2034 if ( $dbw->affectedRows() ) {
2035 $newUser = User::newFromId( $dbw->insertId() );
2036 } else {
2037 $newUser = null;
2039 return $newUser;
2043 * Add an existing user object to the database
2045 function addToDatabase() {
2046 $this->load();
2047 $dbw = wfGetDB( DB_MASTER );
2048 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
2049 $dbw->insert( 'user',
2050 array(
2051 'user_id' => $seqVal,
2052 'user_name' => $this->mName,
2053 'user_password' => $this->mPassword,
2054 'user_newpassword' => $this->mNewpassword,
2055 'user_newpass_time' => $dbw->timestamp( $this->mNewpassTime ),
2056 'user_email' => $this->mEmail,
2057 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
2058 'user_real_name' => $this->mRealName,
2059 'user_options' => $this->encodeOptions(),
2060 'user_token' => $this->mToken,
2061 'user_registration' => $dbw->timestamp( $this->mRegistration ),
2062 'user_editcount' => 0,
2063 ), __METHOD__
2065 $this->mId = $dbw->insertId();
2067 # Clear instance cache other than user table data, which is already accurate
2068 $this->clearInstanceCache();
2072 * If the (non-anonymous) user is blocked, this function will block any IP address
2073 * that they successfully log on from.
2075 function spreadBlock() {
2076 wfDebug( __METHOD__."()\n" );
2077 $this->load();
2078 if ( $this->mId == 0 ) {
2079 return;
2082 $userblock = Block::newFromDB( '', $this->mId );
2083 if ( !$userblock ) {
2084 return;
2087 $userblock->doAutoblock( wfGetIp() );
2092 * Generate a string which will be different for any combination of
2093 * user options which would produce different parser output.
2094 * This will be used as part of the hash key for the parser cache,
2095 * so users will the same options can share the same cached data
2096 * safely.
2098 * Extensions which require it should install 'PageRenderingHash' hook,
2099 * which will give them a chance to modify this key based on their own
2100 * settings.
2102 * @return string
2104 function getPageRenderingHash() {
2105 global $wgContLang, $wgUseDynamicDates, $wgLang;
2106 if( $this->mHash ){
2107 return $this->mHash;
2110 // stubthreshold is only included below for completeness,
2111 // it will always be 0 when this function is called by parsercache.
2113 $confstr = $this->getOption( 'math' );
2114 $confstr .= '!' . $this->getOption( 'stubthreshold' );
2115 if ( $wgUseDynamicDates ) {
2116 $confstr .= '!' . $this->getDatePreference();
2118 $confstr .= '!' . ($this->getOption( 'numberheadings' ) ? '1' : '');
2119 $confstr .= '!' . $wgLang->getCode();
2120 $confstr .= '!' . $this->getOption( 'thumbsize' );
2121 // add in language specific options, if any
2122 $extra = $wgContLang->getExtraHashOptions();
2123 $confstr .= $extra;
2125 // Give a chance for extensions to modify the hash, if they have
2126 // extra options or other effects on the parser cache.
2127 wfRunHooks( 'PageRenderingHash', array( &$confstr ) );
2129 $this->mHash = $confstr;
2130 return $confstr;
2133 function isBlockedFromCreateAccount() {
2134 $this->getBlockedStatus();
2135 return $this->mBlock && $this->mBlock->mCreateAccount;
2138 function isAllowedToCreateAccount() {
2139 return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount();
2143 * @deprecated
2145 function setLoaded( $loaded ) {}
2148 * Get this user's personal page title.
2150 * @return Title
2151 * @public
2153 function getUserPage() {
2154 return Title::makeTitle( NS_USER, $this->getName() );
2158 * Get this user's talk page title.
2160 * @return Title
2161 * @public
2163 function getTalkPage() {
2164 $title = $this->getUserPage();
2165 return $title->getTalkPage();
2169 * @static
2171 function getMaxID() {
2172 static $res; // cache
2174 if ( isset( $res ) )
2175 return $res;
2176 else {
2177 $dbr = wfGetDB( DB_SLAVE );
2178 return $res = $dbr->selectField( 'user', 'max(user_id)', false, 'User::getMaxID' );
2183 * Determine whether the user is a newbie. Newbies are either
2184 * anonymous IPs, or the most recently created accounts.
2185 * @return bool True if it is a newbie.
2187 function isNewbie() {
2188 return !$this->isAllowed( 'autoconfirmed' );
2192 * Check to see if the given clear-text password is one of the accepted passwords
2193 * @param string $password User password.
2194 * @return bool True if the given password is correct otherwise False.
2196 function checkPassword( $password ) {
2197 global $wgAuth;
2198 $this->load();
2200 // Even though we stop people from creating passwords that
2201 // are shorter than this, doesn't mean people wont be able
2202 // to. Certain authentication plugins do NOT want to save
2203 // domain passwords in a mysql database, so we should
2204 // check this (incase $wgAuth->strict() is false).
2205 if( !$this->isValidPassword( $password ) ) {
2206 return false;
2209 if( $wgAuth->authenticate( $this->getName(), $password ) ) {
2210 return true;
2211 } elseif( $wgAuth->strict() ) {
2212 /* Auth plugin doesn't allow local authentication */
2213 return false;
2215 $ep = $this->encryptPassword( $password );
2216 if ( 0 == strcmp( $ep, $this->mPassword ) ) {
2217 return true;
2218 } elseif ( function_exists( 'iconv' ) ) {
2219 # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
2220 # Check for this with iconv
2221 $cp1252hash = $this->encryptPassword( iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password ) );
2222 if ( 0 == strcmp( $cp1252hash, $this->mPassword ) ) {
2223 return true;
2226 return false;
2230 * Check if the given clear-text password matches the temporary password
2231 * sent by e-mail for password reset operations.
2232 * @return bool
2234 function checkTemporaryPassword( $plaintext ) {
2235 $hash = $this->encryptPassword( $plaintext );
2236 return $hash === $this->mNewpassword;
2240 * Initialize (if necessary) and return a session token value
2241 * which can be used in edit forms to show that the user's
2242 * login credentials aren't being hijacked with a foreign form
2243 * submission.
2245 * @param mixed $salt - Optional function-specific data for hash.
2246 * Use a string or an array of strings.
2247 * @return string
2248 * @public
2250 function editToken( $salt = '' ) {
2251 if( !isset( $_SESSION['wsEditToken'] ) ) {
2252 $token = $this->generateToken();
2253 $_SESSION['wsEditToken'] = $token;
2254 } else {
2255 $token = $_SESSION['wsEditToken'];
2257 if( is_array( $salt ) ) {
2258 $salt = implode( '|', $salt );
2260 return md5( $token . $salt ) . EDIT_TOKEN_SUFFIX;
2264 * Generate a hex-y looking random token for various uses.
2265 * Could be made more cryptographically sure if someone cares.
2266 * @return string
2268 function generateToken( $salt = '' ) {
2269 $token = dechex( mt_rand() ) . dechex( mt_rand() );
2270 return md5( $token . $salt );
2274 * Check given value against the token value stored in the session.
2275 * A match should confirm that the form was submitted from the
2276 * user's own login session, not a form submission from a third-party
2277 * site.
2279 * @param string $val - the input value to compare
2280 * @param string $salt - Optional function-specific data for hash
2281 * @return bool
2282 * @public
2284 function matchEditToken( $val, $salt = '' ) {
2285 global $wgMemc;
2286 $sessionToken = $this->editToken( $salt );
2287 if ( $val != $sessionToken ) {
2288 wfDebug( "User::matchEditToken: broken session data\n" );
2290 return $val == $sessionToken;
2294 * Generate a new e-mail confirmation token and send a confirmation
2295 * mail to the user's given address.
2297 * @return mixed True on success, a WikiError object on failure.
2299 function sendConfirmationMail() {
2300 global $wgContLang;
2301 $expiration = null; // gets passed-by-ref and defined in next line.
2302 $url = $this->confirmationTokenUrl( $expiration );
2303 return $this->sendMail( wfMsg( 'confirmemail_subject' ),
2304 wfMsg( 'confirmemail_body',
2305 wfGetIP(),
2306 $this->getName(),
2307 $url,
2308 $wgContLang->timeanddate( $expiration, false ) ) );
2312 * Send an e-mail to this user's account. Does not check for
2313 * confirmed status or validity.
2315 * @param string $subject
2316 * @param string $body
2317 * @param strong $from Optional from address; default $wgPasswordSender will be used otherwise.
2318 * @return mixed True on success, a WikiError object on failure.
2320 function sendMail( $subject, $body, $from = null ) {
2321 if( is_null( $from ) ) {
2322 global $wgPasswordSender;
2323 $from = $wgPasswordSender;
2326 require_once( 'UserMailer.php' );
2327 $to = new MailAddress( $this );
2328 $sender = new MailAddress( $from );
2329 $error = userMailer( $to, $sender, $subject, $body );
2331 if( $error == '' ) {
2332 return true;
2333 } else {
2334 return new WikiError( $error );
2339 * Generate, store, and return a new e-mail confirmation code.
2340 * A hash (unsalted since it's used as a key) is stored.
2341 * @param &$expiration mixed output: accepts the expiration time
2342 * @return string
2343 * @private
2345 function confirmationToken( &$expiration ) {
2346 $now = time();
2347 $expires = $now + 7 * 24 * 60 * 60;
2348 $expiration = wfTimestamp( TS_MW, $expires );
2350 $token = $this->generateToken( $this->mId . $this->mEmail . $expires );
2351 $hash = md5( $token );
2353 $dbw = wfGetDB( DB_MASTER );
2354 $dbw->update( 'user',
2355 array( 'user_email_token' => $hash,
2356 'user_email_token_expires' => $dbw->timestamp( $expires ) ),
2357 array( 'user_id' => $this->mId ),
2358 __METHOD__ );
2360 return $token;
2364 * Generate and store a new e-mail confirmation token, and return
2365 * the URL the user can use to confirm.
2366 * @param &$expiration mixed output: accepts the expiration time
2367 * @return string
2368 * @private
2370 function confirmationTokenUrl( &$expiration ) {
2371 $token = $this->confirmationToken( $expiration );
2372 $title = SpecialPage::getTitleFor( 'Confirmemail', $token );
2373 return $title->getFullUrl();
2377 * Mark the e-mail address confirmed and save.
2379 function confirmEmail() {
2380 $this->load();
2381 $this->mEmailAuthenticated = wfTimestampNow();
2382 $this->saveSettings();
2383 return true;
2387 * Is this user allowed to send e-mails within limits of current
2388 * site configuration?
2389 * @return bool
2391 function canSendEmail() {
2392 return $this->isEmailConfirmed();
2396 * Is this user allowed to receive e-mails within limits of current
2397 * site configuration?
2398 * @return bool
2400 function canReceiveEmail() {
2401 return $this->canSendEmail() && !$this->getOption( 'disablemail' );
2405 * Is this user's e-mail address valid-looking and confirmed within
2406 * limits of the current site configuration?
2408 * If $wgEmailAuthentication is on, this may require the user to have
2409 * confirmed their address by returning a code or using a password
2410 * sent to the address from the wiki.
2412 * @return bool
2414 function isEmailConfirmed() {
2415 global $wgEmailAuthentication;
2416 $this->load();
2417 $confirmed = true;
2418 if( wfRunHooks( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) {
2419 if( $this->isAnon() )
2420 return false;
2421 if( !self::isValidEmailAddr( $this->mEmail ) )
2422 return false;
2423 if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() )
2424 return false;
2425 return true;
2426 } else {
2427 return $confirmed;
2432 * Return true if there is an outstanding request for e-mail confirmation.
2433 * @return bool
2435 function isEmailConfirmationPending() {
2436 global $wgEmailAuthentication;
2437 return $wgEmailAuthentication &&
2438 !$this->isEmailConfirmed() &&
2439 $this->mEmailToken &&
2440 $this->mEmailTokenExpires > wfTimestamp();
2444 * @param array $groups list of groups
2445 * @return array list of permission key names for given groups combined
2446 * @static
2448 static function getGroupPermissions( $groups ) {
2449 global $wgGroupPermissions;
2450 $rights = array();
2451 foreach( $groups as $group ) {
2452 if( isset( $wgGroupPermissions[$group] ) ) {
2453 $rights = array_merge( $rights,
2454 array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
2457 return $rights;
2461 * @param string $group key name
2462 * @return string localized descriptive name for group, if provided
2463 * @static
2465 static function getGroupName( $group ) {
2466 MessageCache::loadAllMessages();
2467 $key = "group-$group";
2468 $name = wfMsg( $key );
2469 return $name == '' || wfEmptyMsg( $key, $name )
2470 ? $group
2471 : $name;
2475 * @param string $group key name
2476 * @return string localized descriptive name for member of a group, if provided
2477 * @static
2479 static function getGroupMember( $group ) {
2480 MessageCache::loadAllMessages();
2481 $key = "group-$group-member";
2482 $name = wfMsg( $key );
2483 return $name == '' || wfEmptyMsg( $key, $name )
2484 ? $group
2485 : $name;
2489 * Return the set of defined explicit groups.
2490 * The *, 'user', 'autoconfirmed' and 'emailconfirmed'
2491 * groups are not included, as they are defined
2492 * automatically, not in the database.
2493 * @return array
2494 * @static
2496 static function getAllGroups() {
2497 global $wgGroupPermissions;
2498 return array_diff(
2499 array_keys( $wgGroupPermissions ),
2500 array( '*', 'user', 'autoconfirmed', 'emailconfirmed' ) );
2504 * Get the title of a page describing a particular group
2506 * @param $group Name of the group
2507 * @return mixed
2509 static function getGroupPage( $group ) {
2510 MessageCache::loadAllMessages();
2511 $page = wfMsgForContent( 'grouppage-' . $group );
2512 if( !wfEmptyMsg( 'grouppage-' . $group, $page ) ) {
2513 $title = Title::newFromText( $page );
2514 if( is_object( $title ) )
2515 return $title;
2517 return false;
2521 * Create a link to the group in HTML, if available
2523 * @param $group Name of the group
2524 * @param $text The text of the link
2525 * @return mixed
2527 static function makeGroupLinkHTML( $group, $text = '' ) {
2528 if( $text == '' ) {
2529 $text = self::getGroupName( $group );
2531 $title = self::getGroupPage( $group );
2532 if( $title ) {
2533 global $wgUser;
2534 $sk = $wgUser->getSkin();
2535 return $sk->makeLinkObj( $title, htmlspecialchars( $text ) );
2536 } else {
2537 return $text;
2542 * Create a link to the group in Wikitext, if available
2544 * @param $group Name of the group
2545 * @param $text The text of the link (by default, the name of the group)
2546 * @return mixed
2548 static function makeGroupLinkWiki( $group, $text = '' ) {
2549 if( $text == '' ) {
2550 $text = self::getGroupName( $group );
2552 $title = self::getGroupPage( $group );
2553 if( $title ) {
2554 $page = $title->getPrefixedText();
2555 return "[[$page|$text]]";
2556 } else {
2557 return $text;
2562 * Increment the user's edit-count field.
2563 * Will have no effect for anonymous users.
2565 function incEditCount() {
2566 if( !$this->isAnon() ) {
2567 $dbw = wfGetDB( DB_MASTER );
2568 $dbw->update( 'user',
2569 array( 'user_editcount=user_editcount+1' ),
2570 array( 'user_id' => $this->getId() ),
2571 __METHOD__ );
2573 // Lazy initialization check...
2574 if( $dbw->affectedRows() == 0 ) {
2575 // Pull from a slave to be less cruel to servers
2576 // Accuracy isn't the point anyway here
2577 $dbr = wfGetDB( DB_SLAVE );
2578 $count = $dbr->selectField( 'revision',
2579 'COUNT(rev_user)',
2580 array( 'rev_user' => $this->getId() ),
2581 __METHOD__ );
2583 // Now here's a goddamn hack...
2584 if( $dbr !== $dbw ) {
2585 // If we actually have a slave server, the count is
2586 // at least one behind because the current transaction
2587 // has not been committed and replicated.
2588 $count++;
2589 } else {
2590 // But if DB_SLAVE is selecting the master, then the
2591 // count we just read includes the revision that was
2592 // just added in the working transaction.
2595 $dbw->update( 'user',
2596 array( 'user_editcount' => $count ),
2597 array( 'user_id' => $this->getId() ),
2598 __METHOD__ );
2601 // edit count in user cache too
2602 $this->invalidateCache();