source: trunk/_InstallationResources/User.php @ 1491

Revision 1491, 103.0 KB checked in by Xiping.Wang, 2 years ago (diff)

[trunk] update user.php with new version

Line 
1<?php
2/**
3 * Implements the User class for the %MediaWiki software.
4 * @file
5 */
6
7/**
8 * \int Number of characters in user_token field.
9 * @ingroup Constants
10 */
11define( 'USER_TOKEN_LENGTH', 32 );
12
13/**
14 * \int Serialized record version.
15 * @ingroup Constants
16 */
17define( 'MW_USER_VERSION', 8 );
18
19/**
20 * \string Some punctuation to prevent editing from broken text-mangling proxies.
21 * @ingroup Constants
22 */
23define( 'EDIT_TOKEN_SUFFIX', '+\\' );
24
25/**
26 * Thrown by User::setPassword() on error.
27 * @ingroup Exception
28 */
29class PasswordError extends MWException {
30        // NOP
31}
32
33/**
34 * The User object encapsulates all of the user-specific settings (user_id,
35 * name, rights, password, email address, options, last login time). Client
36 * classes use the getXXX() functions to access these fields. These functions
37 * do all the work of determining whether the user is logged in,
38 * whether the requested option can be satisfied from cookies or
39 * whether a database query is needed. Most of the settings needed
40 * for rendering normal pages are set in the cookie to minimize use
41 * of the database.
42 */
43class User {
44
45        /**
46         * \type{\arrayof{\string}} A list of default user toggles, i.e., boolean user
47         * preferences that are displayed by Special:Preferences as checkboxes.
48         * This list can be extended via the UserToggles hook or by
49         * $wgContLang::getExtraUserToggles().
50         * @showinitializer
51         */
52        public static $mToggles = array(
53                'highlightbroken',
54                'justify',
55                'hideminor',
56                'extendwatchlist',
57                'usenewrc',
58                'numberheadings',
59                'showtoolbar',
60                'editondblclick',
61                'editsection',
62                'editsectiononrightclick',
63                'showtoc',
64                'rememberpassword',
65                'editwidth',
66                'watchcreations',
67                'watchdefault',
68                'watchmoves',
69                'watchdeletion',
70                'minordefault',
71                'previewontop',
72                'previewonfirst',
73                'nocache',
74                'enotifwatchlistpages',
75                'enotifusertalkpages',
76                'enotifminoredits',
77                'enotifrevealaddr',
78                'shownumberswatching',
79                'fancysig',
80                'externaleditor',
81                'externaldiff',
82                'showjumplinks',
83                'uselivepreview',
84                'forceeditsummary',
85                'watchlisthideminor',
86                'watchlisthidebots',
87                'watchlisthideown',
88                'watchlisthideanons',
89                'watchlisthideliu',
90                'ccmeonemails',
91                'diffonly',
92                'showhiddencats',
93                'noconvertlink',
94                'norollbackdiff',
95        );
96
97        /**
98         * \type{\arrayof{\string}} List of member variables which are saved to the
99         * shared cache (memcached). Any operation which changes the
100         * corresponding database fields must call a cache-clearing function.
101         * @showinitializer
102         */
103        static $mCacheVars = array(
104                // user table
105                'mId',
106                'mName',
107                'mRealName',
108                'mPassword',
109                'mNewpassword',
110                'mNewpassTime',
111                'mEmail',
112                'mTouched',
113                'mToken',
114                'mEmailAuthenticated',
115                'mEmailToken',
116                'mEmailTokenExpires',
117                'mRegistration',
118                'mEditCount',
119                // user_group table
120                'mGroups',
121                // user_properties table
122                'mOptionOverrides',
123        );
124
125        /**
126         * \type{\arrayof{\string}} Core rights.
127         * Each of these should have a corresponding message of the form
128         * "right-$right".
129         * @showinitializer
130         */
131        static $mCoreRights = array(
132                'apihighlimits',
133                'autoconfirmed',
134                'autopatrol',
135                'bigdelete',
136                'block',
137                'blockemail',
138                'bot',
139                'browsearchive',
140                'createaccount',
141                'createpage',
142                'createtalk',
143                'delete',
144                'deletedhistory',
145                'deletedtext',
146                'deleterevision',
147                'edit',
148                'editinterface',
149                'editusercssjs',
150                'hideuser',
151                'import',
152                'importupload',
153                'ipblock-exempt',
154                'markbotedits',
155                'minoredit',
156                'move',
157                'movefile',
158                'move-rootuserpages',
159                'move-subpages',
160                'nominornewtalk',
161                'noratelimit',
162                'override-export-depth',
163                'patrol',
164                'protect',
165                'proxyunbannable',
166                'purge',
167                'read',
168                'reupload',
169                'reupload-shared',
170                'rollback',
171                'sendemail',
172                'siteadmin',
173                'suppressionlog',
174                'suppressredirect',
175                'suppressrevision',
176                'trackback',
177                'undelete',
178                'unwatchedpages',
179                'upload',
180                'upload_by_url',
181                'userrights',
182                'userrights-interwiki',
183                'writeapi',
184        );
185        /**
186         * \string Cached results of getAllRights()
187         */
188        static $mAllRights = false;
189
190        /** @name Cache variables */
191        //@{
192        var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime,
193                $mEmail, $mTouched, $mToken, $mEmailAuthenticated,
194                $mEmailToken, $mEmailTokenExpires, $mRegistration, $mGroups, $mOptionOverrides;
195        //@}
196
197        /**
198         * \bool Whether the cache variables have been loaded.
199         */
200        var $mDataLoaded, $mAuthLoaded, $mOptionsLoaded;
201
202        /**
203         * \string Initialization data source if mDataLoaded==false. May be one of:
204         *  - 'defaults'   anonymous user initialised from class defaults
205         *  - 'name'       initialise from mName
206         *  - 'id'         initialise from mId
207         *  - 'session'    log in from cookies or session if possible
208         *
209         * Use the User::newFrom*() family of functions to set this.
210         */
211        var $mFrom;
212
213        /** @name Lazy-initialized variables, invalidated with clearInstanceCache */
214        //@{
215        var $mNewtalk, $mDatePreference, $mBlockedby, $mHash, $mSkin, $mRights,
216                $mBlockreason, $mBlock, $mEffectiveGroups, $mBlockedGlobally,
217                $mLocked, $mHideName, $mOptions;
218        //@}
219
220        static $idCacheByName = array();
221
222        /**
223         * Lightweight constructor for an anonymous user.
224         * Use the User::newFrom* factory functions for other kinds of users.
225         *
226         * @see newFromName()
227         * @see newFromId()
228         * @see newFromConfirmationCode()
229         * @see newFromSession()
230         * @see newFromRow()
231         */
232        function User() {
233                $this->clearInstanceCache( 'defaults' );
234        }
235
236        /**
237         * Load the user table data for this object from the source given by mFrom.
238         */
239        function load() {
240                if ( $this->mDataLoaded ) {
241                        return;
242                }
243                wfProfileIn( __METHOD__ );
244
245                # Set it now to avoid infinite recursion in accessors
246                $this->mDataLoaded = true;
247
248                switch ( $this->mFrom ) {
249                        case 'defaults':
250                                $this->loadDefaults();
251                                break;
252                        case 'name':
253                                $this->mId = self::idFromName( $this->mName );
254                                if ( !$this->mId ) {
255                                        # Nonexistent user placeholder object
256                                        $this->loadDefaults( $this->mName );
257                                } else {
258                                        $this->loadFromId();
259                                }
260                                break;
261                        case 'id':
262                                $this->loadFromId();
263                                break;
264                        case 'session':
265                                $this->loadFromSession();
266                                wfRunHooks( 'UserLoadAfterLoadFromSession', array( $this ) );
267                                break;
268                        default:
269                                throw new MWException( "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" );
270                }
271                wfProfileOut( __METHOD__ );
272        }
273
274        /**
275         * Load user table data, given mId has already been set.
276         * @return \bool false if the ID does not exist, true otherwise
277         * @private
278         */
279        function loadFromId() {
280                global $wgMemc;
281                if ( $this->mId == 0 ) {
282                        $this->loadDefaults();
283                        return false;
284                }
285
286                # Try cache
287                $key = wfMemcKey( 'user', 'id', $this->mId );
288                $data = $wgMemc->get( $key );
289                if ( !is_array( $data ) || $data['mVersion'] < MW_USER_VERSION ) {
290                        # Object is expired, load from DB
291                        $data = false;
292                }
293
294                if ( !$data ) {
295                        wfDebug( "Cache miss for user {$this->mId}\n" );
296                        # Load from DB
297                        if ( !$this->loadFromDatabase() ) {
298                                # Can't load from ID, user is anonymous
299                                return false;
300                        }
301                        $this->saveToCache();
302                } else {
303                        wfDebug( "Got user {$this->mId} from cache\n" );
304                        # Restore from cache
305                        foreach ( self::$mCacheVars as $name ) {
306                                $this->$name = $data[$name];
307                        }
308                }
309                return true;
310        }
311
312        /**
313         * Save user data to the shared cache
314         */
315        function saveToCache() {
316                $this->load();
317                $this->loadGroups();
318                $this->loadOptions();
319                if ( $this->isAnon() ) {
320                        // Anonymous users are uncached
321                        return;
322                }
323                $data = array();
324                foreach ( self::$mCacheVars as $name ) {
325                        $data[$name] = $this->$name;
326                }
327                $data['mVersion'] = MW_USER_VERSION;
328                $key = wfMemcKey( 'user', 'id', $this->mId );
329                global $wgMemc;
330                $wgMemc->set( $key, $data );
331        }
332
333
334        /** @name newFrom*() static factory methods */
335        //@{
336
337        /**
338         * Static factory method for creation from username.
339         *
340         * This is slightly less efficient than newFromId(), so use newFromId() if
341         * you have both an ID and a name handy.
342         *
343         * @param $name \string Username, validated by Title::newFromText()
344         * @param $validate \mixed Validate username. Takes the same parameters as
345         *    User::getCanonicalName(), except that true is accepted as an alias
346         *    for 'valid', for BC.
347         *
348         * @return \type{User} The User object, or false if the username is invalid
349         *    (e.g. if it contains illegal characters or is an IP address). If the
350         *    username is not present in the database, the result will be a user object
351         *    with a name, zero user ID and default settings.
352         */
353        static function newFromName( $name, $validate = 'valid' ) {
354                if ( $validate === true ) {
355                        $validate = 'valid';
356                }
357                $name = self::getCanonicalName( $name, $validate );
358                if ( $name === false ) {
359                        return false;
360                } else {
361                        # Create unloaded user object
362                        $u = new User;
363                        $u->mName = $name;
364                        $u->mFrom = 'name';
365                        return $u;
366                }
367        }
368
369        /**
370         * Static factory method for creation from a given user ID.
371         *
372         * @param $id \int Valid user ID
373         * @return \type{User} The corresponding User object
374         */
375        static function newFromId( $id ) {
376                $u = new User;
377                $u->mId = $id;
378                $u->mFrom = 'id';
379                return $u;
380        }
381
382        /**
383         * Factory method to fetch whichever user has a given email confirmation code.
384         * This code is generated when an account is created or its e-mail address
385         * has changed.
386         *
387         * If the code is invalid or has expired, returns NULL.
388         *
389         * @param $code \string Confirmation code
390         * @return \type{User}
391         */
392        static function newFromConfirmationCode( $code ) {
393                $dbr = wfGetDB( DB_SLAVE );
394                $id = $dbr->selectField( 'user', 'user_id', array(
395                        'user_email_token' => md5( $code ),
396                        'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ),
397                        ) );
398                if( $id !== false ) {
399                        return User::newFromId( $id );
400                } else {
401                        return null;
402                }
403        }
404
405        /**
406         * Create a new user object using data from session or cookies. If the
407         * login credentials are invalid, the result is an anonymous user.
408         *
409         * @return \type{User}
410         */
411        static function newFromSession() {
412                $user = new User;
413                $user->mFrom = 'session';
414                return $user;
415        }
416
417        /**
418         * Create a new user object from a user row.
419         * The row should have all fields from the user table in it.
420         * @param $row array A row from the user table
421         * @return \type{User}
422         */
423        static function newFromRow( $row ) {
424                $user = new User;
425                $user->loadFromRow( $row );
426                return $user;
427        }
428
429        //@}
430
431
432        /**
433         * Get the username corresponding to a given user ID
434         * @param $id \int User ID
435         * @return \string The corresponding username
436         */
437        static function whoIs( $id ) {
438                $dbr = wfGetDB( DB_SLAVE );
439                return $dbr->selectField( 'user', 'user_name', array( 'user_id' => $id ), __METHOD__ );
440        }
441
442        /**
443         * Get the real name of a user given their user ID
444         *
445         * @param $id \int User ID
446         * @return \string The corresponding user's real name
447         */
448        static function whoIsReal( $id ) {
449                $dbr = wfGetDB( DB_SLAVE );
450                return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ), __METHOD__ );
451        }
452
453        /**
454         * Get database id given a user name
455         * @param $name \string Username
456         * @return \types{\int,\null} The corresponding user's ID, or null if user is nonexistent
457         */
458        static function idFromName( $name ) {
459                $nt = Title::makeTitleSafe( NS_USER, $name );
460                if( is_null( $nt ) ) {
461                        # Illegal name
462                        return null;
463                }
464
465                if ( isset( self::$idCacheByName[$name] ) ) {
466                        return self::$idCacheByName[$name];
467                }
468
469                $dbr = wfGetDB( DB_SLAVE );
470                $s = $dbr->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), __METHOD__ );
471
472                if ( $s === false ) {
473                        $result = null;
474                } else {
475                        $result = $s->user_id;
476                }
477
478                self::$idCacheByName[$name] = $result;
479
480                if ( count( self::$idCacheByName ) > 1000 ) {
481                        self::$idCacheByName = array();
482                }
483
484                return $result;
485        }
486
487        /**
488         * Does the string match an anonymous IPv4 address?
489         *
490         * This function exists for username validation, in order to reject
491         * usernames which are similar in form to IP addresses. Strings such
492         * as 300.300.300.300 will return true because it looks like an IP
493         * address, despite not being strictly valid.
494         *
495         * We match \d{1,3}\.\d{1,3}\.\d{1,3}\.xxx as an anonymous IP
496         * address because the usemod software would "cloak" anonymous IP
497         * addresses like this, if we allowed accounts like this to be created
498         * new users could get the old edits of these anonymous users.
499         *
500         * @param $name \string String to match
501         * @return \bool True or false
502         */
503        static function isIP( $name ) {
504                return preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/',$name) || IP::isIPv6($name);
505        }
506
507        /**
508         * Is the input a valid username?
509         *
510         * Checks if the input is a valid username, we don't want an empty string,
511         * an IP address, anything that containins slashes (would mess up subpages),
512         * is longer than the maximum allowed username size or doesn't begin with
513         * a capital letter.
514         *
515         * @param $name \string String to match
516         * @return \bool True or false
517         */
518        static function isValidUserName( $name ) {
519                global $wgContLang, $wgMaxNameChars;
520
521                if ( $name == ''
522                || User::isIP( $name )
523                || strpos( $name, '/' ) !== false
524                || strlen( $name ) > $wgMaxNameChars
525                || $name != $wgContLang->ucfirst( $name ) ) {
526                        wfDebugLog( 'username', __METHOD__ .
527                                ": '$name' invalid due to empty, IP, slash, length, or lowercase" );
528                        return false;
529                }
530
531                // Ensure that the name can't be misresolved as a different title,
532                // such as with extra namespace keys at the start.
533                $parsed = Title::newFromText( $name );
534                if( is_null( $parsed )
535                        || $parsed->getNamespace()
536                        || strcmp( $name, $parsed->getPrefixedText() ) ) {
537                        wfDebugLog( 'username', __METHOD__ .
538                                ": '$name' invalid due to ambiguous prefixes" );
539                        return false;
540                }
541
542                // Check an additional blacklist of troublemaker characters.
543                // Should these be merged into the title char list?
544                $unicodeBlacklist = '/[' .
545                        '\x{0080}-\x{009f}' . # iso-8859-1 control chars
546                        '\x{00a0}' .          # non-breaking space
547                        '\x{2000}-\x{200f}' . # various whitespace
548                        '\x{2028}-\x{202f}' . # breaks and control chars
549                        '\x{3000}' .          # ideographic space
550                        '\x{e000}-\x{f8ff}' . # private use
551                        ']/u';
552                if( preg_match( $unicodeBlacklist, $name ) ) {
553                        wfDebugLog( 'username', __METHOD__ .
554                                ": '$name' invalid due to blacklisted characters" );
555                        return false;
556                }
557
558                return true;
559        }
560
561        /**
562         * Usernames which fail to pass this function will be blocked
563         * from user login and new account registrations, but may be used
564         * internally by batch processes.
565         *
566         * If an account already exists in this form, login will be blocked
567         * by a failure to pass this function.
568         *
569         * @param $name \string String to match
570         * @return \bool True or false
571         */
572        static function isUsableName( $name ) {
573                global $wgReservedUsernames;
574                // Must be a valid username, obviously ;)
575                if ( !self::isValidUserName( $name ) ) {
576                        return false;
577                }
578
579                static $reservedUsernames = false;
580                if ( !$reservedUsernames ) {
581                        $reservedUsernames = $wgReservedUsernames;
582                        wfRunHooks( 'UserGetReservedNames', array( &$reservedUsernames ) );
583                }
584
585                // Certain names may be reserved for batch processes.
586                foreach ( $reservedUsernames as $reserved ) {
587                        if ( substr( $reserved, 0, 4 ) == 'msg:' ) {
588                                $reserved = wfMsgForContent( substr( $reserved, 4 ) );
589                        }
590                        if ( $reserved == $name ) {
591                                return false;
592                        }
593                }
594                return true;
595        }
596
597        /**
598         * Usernames which fail to pass this function will be blocked
599         * from new account registrations, but may be used internally
600         * either by batch processes or by user accounts which have
601         * already been created.
602         *
603         * Additional character blacklisting may be added here
604         * rather than in isValidUserName() to avoid disrupting
605         * existing accounts.
606         *
607         * @param $name \string String to match
608         * @return \bool True or false
609         */
610        static function isCreatableName( $name ) {
611                global $wgInvalidUsernameCharacters;
612                return
613                        self::isUsableName( $name ) &&
614
615                        // Registration-time character blacklisting...
616                        !preg_match( '/[' . preg_quote( $wgInvalidUsernameCharacters, '/' ) . ']/', $name );
617        }
618
619        /**
620         * Is the input a valid password for this user?
621         *
622         * @param $password String Desired password
623         * @return bool True or false
624         */
625        function isValidPassword( $password ) {
626                //simple boolean wrapper for getPasswordValidity
627                return $this->getPasswordValidity( $password ) === true;
628        }
629
630        /**
631         * Given unvalidated password input, return error message on failure.
632         *
633         * @param $password String Desired password
634         * @return mixed: true on success, string of error message on failure
635         */
636        function getPasswordValidity( $password ) {
637                global $wgMinimalPasswordLength, $wgContLang;
638
639                $result = false; //init $result to false for the internal checks
640
641                if( !wfRunHooks( 'isValidPassword', array( $password, &$result, $this ) ) )
642                        return $result;
643
644                if ( $result === false ) {
645                        if( strlen( $password ) < $wgMinimalPasswordLength ) {
646                                return 'passwordtooshort';
647                        } elseif ( $wgContLang->lc( $password ) == $wgContLang->lc( $this->mName ) ) {
648                                return 'password-name-match';
649                        } else {
650                                //it seems weird returning true here, but this is because of the
651                                //initialization of $result to false above. If the hook is never run or it
652                                //doesn't modify $result, then we will likely get down into this if with
653                                //a valid password.
654                                return true;
655                        }
656                } elseif( $result === true ) {
657                        return true;
658                } else {
659                        return $result; //the isValidPassword hook set a string $result and returned true
660                }
661        }
662
663        /**
664         * Does a string look like an e-mail address?
665         *
666         * There used to be a regular expression here, it got removed because it
667         * rejected valid addresses. Actually just check if there is '@' somewhere
668         * in the given address.
669         *
670         * @todo Check for RFC 2822 compilance (bug 959)
671         *
672         * @param $addr \string E-mail address
673         * @return \bool True or false
674         */
675        public static function isValidEmailAddr( $addr ) {
676                $result = null;
677                if( !wfRunHooks( 'isValidEmailAddr', array( $addr, &$result ) ) ) {
678                        return $result;
679                }
680
681                return strpos( $addr, '@' ) !== false;
682        }
683
684        /**
685         * Given unvalidated user input, return a canonical username, or false if
686         * the username is invalid.
687         * @param $name \string User input
688         * @param $validate \types{\string,\bool} Type of validation to use:
689         *                - false        No validation
690         *                - 'valid'      Valid for batch processes
691         *                - 'usable'     Valid for batch processes and login
692         *                - 'creatable'  Valid for batch processes, login and account creation
693         */
694        static function getCanonicalName( $name, $validate = 'valid' ) {
695                # Force usernames to capital
696                global $wgContLang;
697                $name = $wgContLang->ucfirst( $name );
698
699                # Reject names containing '#'; these will be cleaned up
700                # with title normalisation, but then it's too late to
701                # check elsewhere
702                if( strpos( $name, '#' ) !== false )
703                        return false;
704
705                # Clean up name according to title rules
706                $t = ( $validate === 'valid' ) ?
707                        Title::newFromText( $name ) : Title::makeTitle( NS_USER, $name );
708                # Check for invalid titles
709                if( is_null( $t ) ) {
710                        return false;
711                }
712
713                # Reject various classes of invalid names
714                $name = $t->getText();
715                global $wgAuth;
716                $name = $wgAuth->getCanonicalName( $t->getText() );
717
718                switch ( $validate ) {
719                        case false:
720                                break;
721                        case 'valid':
722                                if ( !User::isValidUserName( $name ) ) {
723                                        $name = false;
724                                }
725                                break;
726                        case 'usable':
727                                if ( !User::isUsableName( $name ) ) {
728                                        $name = false;
729                                }
730                                break;
731                        case 'creatable':
732                                if ( !User::isCreatableName( $name ) ) {
733                                        $name = false;
734                                }
735                                break;
736                        default:
737                                throw new MWException( 'Invalid parameter value for $validate in ' . __METHOD__ );
738                }
739                return $name;
740        }
741
742        /**
743         * Count the number of edits of a user
744         * @todo It should not be static and some day should be merged as proper member function / deprecated -- domas
745         *
746         * @param $uid \int User ID to check
747         * @return \int The user's edit count
748         */
749        static function edits( $uid ) {
750                wfProfileIn( __METHOD__ );
751                $dbr = wfGetDB( DB_SLAVE );
752                // check if the user_editcount field has been initialized
753                $field = $dbr->selectField(
754                        'user', 'user_editcount',
755                        array( 'user_id' => $uid ),
756                        __METHOD__
757                );
758
759                if( $field === null ) { // it has not been initialized. do so.
760                        $dbw = wfGetDB( DB_MASTER );
761                        $count = $dbr->selectField(
762                                'revision', 'count(*)',
763                                array( 'rev_user' => $uid ),
764                                __METHOD__
765                        );
766                        $dbw->update(
767                                'user',
768                                array( 'user_editcount' => $count ),
769                                array( 'user_id' => $uid ),
770                                __METHOD__
771                        );
772                } else {
773                        $count = $field;
774                }
775                wfProfileOut( __METHOD__ );
776                return $count;
777        }
778
779        /**
780         * Return a random password. Sourced from mt_rand, so it's not particularly secure.
781         * @todo hash random numbers to improve security, like generateToken()
782         *
783         * @return \string New random password
784         */
785        static function randomPassword() {
786                global $wgMinimalPasswordLength;
787                $pwchars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz';
788                $l = strlen( $pwchars ) - 1;
789
790                $pwlength = max( 7, $wgMinimalPasswordLength );
791                $digit = mt_rand( 0, $pwlength - 1 );
792                $np = '';
793                for ( $i = 0; $i < $pwlength; $i++ ) {
794                        $np .= $i == $digit ? chr( mt_rand( 48, 57 ) ) : $pwchars{ mt_rand( 0, $l ) };
795                }
796                return $np;
797        }
798
799        /**
800         * Set cached properties to default.
801         *
802         * @note This no longer clears uncached lazy-initialised properties;
803         *       the constructor does that instead.
804         * @private
805         */
806        function loadDefaults( $name = false ) {
807                wfProfileIn( __METHOD__ );
808
809                global $wgCookiePrefix;
810
811                $this->mId = 0;
812                $this->mName = $name;
813                $this->mRealName = '';
814                $this->mPassword = $this->mNewpassword = '';
815                $this->mNewpassTime = null;
816                $this->mEmail = '';
817                $this->mOptionOverrides = null;
818                $this->mOptionsLoaded = false;
819
820                if ( isset( $_COOKIE[$wgCookiePrefix.'LoggedOut'] ) ) {
821                        $this->mTouched = wfTimestamp( TS_MW, $_COOKIE[$wgCookiePrefix.'LoggedOut'] );
822                } else {
823                        $this->mTouched = '0'; # Allow any pages to be cached
824                }
825
826                $this->setToken(); # Random
827                $this->mEmailAuthenticated = null;
828                $this->mEmailToken = '';
829                $this->mEmailTokenExpires = null;
830                $this->mRegistration = wfTimestamp( TS_MW );
831                $this->mGroups = array();
832
833                wfRunHooks( 'UserLoadDefaults', array( $this, $name ) );
834
835                wfProfileOut( __METHOD__ );
836        }
837
838        /**
839         * @deprecated Use wfSetupSession().
840         */
841        function SetupSession() {
842                wfDeprecated( __METHOD__ );
843                wfSetupSession();
844        }
845
846        /**
847         * Load user data from the session or login cookie. If there are no valid
848         * credentials, initialises the user as an anonymous user.
849         * @return \bool True if the user is logged in, false otherwise.
850         */
851        private function loadFromSession() {
852                global $wgMemc, $wgCookiePrefix, $wgExternalAuthType, $wgAutocreatePolicy;
853
854                $result = null;
855                wfRunHooks( 'UserLoadFromSession', array( $this, &$result ) );
856                if ( $result !== null ) {
857                        return $result;
858                }
859
860                if ( $wgExternalAuthType && $wgAutocreatePolicy == 'view' ) {
861                        $extUser = ExternalUser::newFromCookie();
862                        if ( $extUser ) {
863                                # TODO: Automatically create the user here (or probably a bit
864                                # lower down, in fact)
865                        }
866                }
867
868                if ( isset( $_COOKIE["{$wgCookiePrefix}UserID"] ) ) {
869                        $sId = intval( $_COOKIE["{$wgCookiePrefix}UserID"] );
870                        if( isset( $_SESSION['wsUserID'] ) && $sId != $_SESSION['wsUserID'] ) {
871                                $this->loadDefaults(); // Possible collision!
872                                wfDebugLog( 'loginSessions', "Session user ID ({$_SESSION['wsUserID']}) and
873                                        cookie user ID ($sId) don't match!" );
874                                return false;
875                        }
876                        $_SESSION['wsUserID'] = $sId;
877                } else if ( isset( $_SESSION['wsUserID'] ) ) {
878                        if ( $_SESSION['wsUserID'] != 0 ) {
879                                $sId = $_SESSION['wsUserID'];
880                        } else {
881                                $this->loadDefaults();
882                                return false;
883                        }
884                } else {
885                        $this->loadDefaults();
886                        return false;
887                }
888
889                if ( isset( $_SESSION['wsUserName'] ) ) {
890                        $sName = $_SESSION['wsUserName'];
891                } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserName"] ) ) {
892                        $sName = $_COOKIE["{$wgCookiePrefix}UserName"];
893                        $_SESSION['wsUserName'] = $sName;
894                } else {
895                        $this->loadDefaults();
896                        return false;
897                }
898
899                $passwordCorrect = FALSE;
900                $this->mId = $sId;
901                if ( !$this->loadFromId() ) {
902                        # Not a valid ID, loadFromId has switched the object to anon for us
903                        return false;
904                }
905
906                global $wgBlockDisablesLogin;
907                if( $wgBlockDisablesLogin && $this->isBlocked() ) {
908                        # User blocked and we've disabled blocked user logins
909                        $this->loadDefaults();
910                        return false;
911                }
912
913                if ( isset( $_SESSION['wsToken'] ) ) {
914                        $passwordCorrect = $_SESSION['wsToken'] == $this->mToken;
915                        $from = 'session';
916                } else if ( isset( $_COOKIE["{$wgCookiePrefix}Token"] ) ) {
917                        $passwordCorrect = $this->mToken == $_COOKIE["{$wgCookiePrefix}Token"];
918                        $from = 'cookie';
919                } else {
920                        # No session or persistent login cookie
921                        $this->loadDefaults();
922                        return false;
923                }
924
925                if ( ( $sName == $this->mName ) && $passwordCorrect ) {
926                        $_SESSION['wsToken'] = $this->mToken;
927                        wfDebug( "Logged in from $from\n" );
928                        return true;
929                } else {
930                        # Invalid credentials
931                        wfDebug( "Can't log in from $from, invalid credentials\n" );
932                        $this->loadDefaults();
933                        return false;
934                }
935        }
936
937        /**
938         * Load user and user_group data from the database.
939         * $this::mId must be set, this is how the user is identified.
940         *
941         * @return \bool True if the user exists, false if the user is anonymous
942         * @private
943         */
944        function loadFromDatabase() {
945                # Paranoia
946                $this->mId = intval( $this->mId );
947
948                /** Anonymous user */
949                if( !$this->mId ) {
950                        $this->loadDefaults();
951                        return false;
952                }
953
954                $dbr = wfGetDB( DB_MASTER );
955                $s = $dbr->selectRow( 'user', '*', array( 'user_id' => $this->mId ), __METHOD__ );
956
957                wfRunHooks( 'UserLoadFromDatabase', array( $this, &$s ) );
958
959                if ( $s !== false ) {
960                        # Initialise user table data
961                        $this->loadFromRow( $s );
962                        $this->mGroups = null; // deferred
963                        $this->getEditCount(); // revalidation for nulls
964                        return true;
965                } else {
966                        # Invalid user_id
967                        $this->mId = 0;
968                        $this->loadDefaults();
969                        return false;
970                }
971        }
972
973        /**
974         * Initialize this object from a row from the user table.
975         *
976         * @param $row \type{\arrayof{\mixed}} Row from the user table to load.
977         */
978        function loadFromRow( $row ) {
979                $this->mDataLoaded = true;
980
981                if ( isset( $row->user_id ) ) {
982                        $this->mId = intval( $row->user_id );
983                }
984                $this->mName = $row->user_name;
985                $this->mRealName = $row->user_real_name;
986                $this->mPassword = $row->user_password;
987                $this->mNewpassword = $row->user_newpassword;
988                $this->mNewpassTime = wfTimestampOrNull( TS_MW, $row->user_newpass_time );
989                $this->mEmail = $row->user_email;
990                $this->decodeOptions( $row->user_options );
991                $this->mTouched = wfTimestamp(TS_MW,$row->user_touched);
992                $this->mToken = $row->user_token;
993                $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated );
994                $this->mEmailToken = $row->user_email_token;
995                $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires );
996                $this->mRegistration = wfTimestampOrNull( TS_MW, $row->user_registration );
997                $this->mEditCount = $row->user_editcount;
998        }
999
1000        /**
1001         * Load the groups from the database if they aren't already loaded.
1002         * @private
1003         */
1004        function loadGroups() {
1005                if ( is_null( $this->mGroups ) ) {
1006                        $dbr = wfGetDB( DB_MASTER );
1007                        $res = $dbr->select( 'user_groups',
1008                                array( 'ug_group' ),
1009                                array( 'ug_user' => $this->mId ),
1010                                __METHOD__ );
1011                        $this->mGroups = array();
1012                        while( $row = $dbr->fetchObject( $res ) ) {
1013                                $this->mGroups[] = $row->ug_group;
1014                        }
1015                }
1016        }
1017
1018        /**
1019         * Clear various cached data stored in this object.
1020         * @param $reloadFrom \string Reload user and user_groups table data from a
1021         *   given source. May be "name", "id", "defaults", "session", or false for
1022         *   no reload.
1023         */
1024        function clearInstanceCache( $reloadFrom = false ) {
1025                $this->mNewtalk = -1;
1026                $this->mDatePreference = null;
1027                $this->mBlockedby = -1; # Unset
1028                $this->mHash = false;
1029                $this->mSkin = null;
1030                $this->mRights = null;
1031                $this->mEffectiveGroups = null;
1032                $this->mOptions = null;
1033
1034                if ( $reloadFrom ) {
1035                        $this->mDataLoaded = false;
1036                        $this->mFrom = $reloadFrom;
1037                }
1038        }
1039
1040        /**
1041         * Combine the language default options with any site-specific options
1042         * and add the default language variants.
1043         *
1044         * @return \type{\arrayof{\string}} Array of options
1045         */
1046        static function getDefaultOptions() {
1047                global $wgNamespacesToBeSearchedDefault;
1048                /**
1049                 * Site defaults will override the global/language defaults
1050                 */
1051                global $wgDefaultUserOptions, $wgContLang, $wgDefaultSkin;
1052                $defOpt = $wgDefaultUserOptions + $wgContLang->getDefaultUserOptionOverrides();
1053
1054                /**
1055                 * default language setting
1056                 */
1057                $variant = $wgContLang->getPreferredVariant( false );
1058                $defOpt['variant'] = $variant;
1059                $defOpt['language'] = $variant;
1060                foreach( SearchEngine::searchableNamespaces() as $nsnum => $nsname ) {
1061                        $defOpt['searchNs'.$nsnum] = !empty( $wgNamespacesToBeSearchedDefault[$nsnum] );
1062                }
1063                $defOpt['skin'] = $wgDefaultSkin;
1064
1065                return $defOpt;
1066        }
1067
1068        /**
1069         * Get a given default option value.
1070         *
1071         * @param $opt \string Name of option to retrieve
1072         * @return \string Default option value
1073         */
1074        public static function getDefaultOption( $opt ) {
1075                $defOpts = self::getDefaultOptions();
1076                if( isset( $defOpts[$opt] ) ) {
1077                        return $defOpts[$opt];
1078                } else {
1079                        return null;
1080                }
1081        }
1082
1083        /**
1084         * Get a list of user toggle names
1085         * @return \type{\arrayof{\string}} Array of user toggle names
1086         */
1087        static function getToggles() {
1088                global $wgContLang, $wgUseRCPatrol;
1089                $extraToggles = array();
1090                wfRunHooks( 'UserToggles', array( &$extraToggles ) );
1091                if( $wgUseRCPatrol ) {
1092                        $extraToggles[] = 'hidepatrolled';
1093                        $extraToggles[] = 'newpageshidepatrolled';
1094                        $extraToggles[] = 'watchlisthidepatrolled';
1095                }
1096                return array_merge( self::$mToggles, $extraToggles, $wgContLang->getExtraUserToggles() );
1097        }
1098
1099
1100        /**
1101         * Get blocking information
1102         * @private
1103         * @param $bFromSlave \bool Whether to check the slave database first. To
1104         *                    improve performance, non-critical checks are done
1105         *                    against slaves. Check when actually saving should be
1106         *                    done against master.
1107         */
1108        function getBlockedStatus( $bFromSlave = true ) {
1109                global $wgProxyWhitelist, $wgUser;
1110
1111                if ( -1 != $this->mBlockedby ) {
1112                        wfDebug( "User::getBlockedStatus: already loaded.\n" );
1113                        return;
1114                }
1115
1116                wfProfileIn( __METHOD__ );
1117                wfDebug( __METHOD__.": checking...\n" );
1118
1119                // Initialize data...
1120                // Otherwise something ends up stomping on $this->mBlockedby when
1121                // things get lazy-loaded later, causing false positive block hits
1122                // due to -1 !== 0. Probably session-related... Nothing should be
1123                // overwriting mBlockedby, surely?
1124                $this->load();
1125
1126                $this->mBlockedby = 0;
1127                $this->mHideName = 0;
1128                $this->mAllowUsertalk = 0;
1129
1130                # Check if we are looking at an IP or a logged-in user
1131                if ( $this->isIP( $this->getName() ) ) {
1132                        $ip = $this->getName();
1133                } else {
1134                        # Check if we are looking at the current user
1135                        # If we don't, and the user is logged in, we don't know about
1136                        # his IP / autoblock status, so ignore autoblock of current user's IP
1137                        if ( $this->getID() != $wgUser->getID() ) {
1138                                $ip = '';
1139                        } else {
1140                                # Get IP of current user
1141                                $ip = wfGetIP();
1142                        }
1143                }
1144
1145                if ( $this->isAllowed( 'ipblock-exempt' ) ) {
1146                        # Exempt from all types of IP-block
1147                        $ip = '';
1148                }
1149
1150                # User/IP blocking
1151                $this->mBlock = new Block();
1152                $this->mBlock->fromMaster( !$bFromSlave );
1153                if ( $this->mBlock->load( $ip , $this->mId ) ) {
1154                        wfDebug( __METHOD__ . ": Found block.\n" );
1155                        $this->mBlockedby = $this->mBlock->mBy;
1156                        if( $this->mBlockedby == "0" )
1157                                $this->mBlockedby = $this->mBlock->mByName;
1158                        $this->mBlockreason = $this->mBlock->mReason;
1159                        $this->mHideName = $this->mBlock->mHideName;
1160                        $this->mAllowUsertalk = $this->mBlock->mAllowUsertalk;
1161                        if ( $this->isLoggedIn() && $wgUser->getID() == $this->getID() ) {
1162                                $this->spreadBlock();
1163                        }
1164                } else {
1165                        // Bug 13611: don't remove mBlock here, to allow account creation blocks to
1166                        // apply to users. Note that the existence of $this->mBlock is not used to
1167                        // check for edit blocks, $this->mBlockedby is instead.
1168                }
1169
1170                # Proxy blocking
1171                if ( !$this->isAllowed( 'proxyunbannable' ) && !in_array( $ip, $wgProxyWhitelist ) ) {
1172                        # Local list
1173                        if ( wfIsLocallyBlockedProxy( $ip ) ) {
1174                                $this->mBlockedby = wfMsg( 'proxyblocker' );
1175                                $this->mBlockreason = wfMsg( 'proxyblockreason' );
1176                        }
1177
1178                        # DNSBL
1179                        if ( !$this->mBlockedby && !$this->getID() ) {
1180                                if ( $this->isDnsBlacklisted( $ip ) ) {
1181                                        $this->mBlockedby = wfMsg( 'sorbs' );
1182                                        $this->mBlockreason = wfMsg( 'sorbsreason' );
1183                                }
1184                        }
1185                }
1186
1187                # Extensions
1188                wfRunHooks( 'GetBlockedStatus', array( &$this ) );
1189
1190                wfProfileOut( __METHOD__ );
1191        }
1192
1193        /**
1194         * Whether the given IP is in a DNS blacklist.
1195         *
1196         * @param $ip \string IP to check
1197         * @param $checkWhitelist Boolean: whether to check the whitelist first
1198         * @return \bool True if blacklisted.
1199         */
1200        function isDnsBlacklisted( $ip, $checkWhitelist = false ) {
1201                global $wgEnableSorbs, $wgEnableDnsBlacklist,
1202                        $wgSorbsUrl, $wgDnsBlacklistUrls, $wgProxyWhitelist;
1203
1204                if ( !$wgEnableDnsBlacklist && !$wgEnableSorbs )
1205                        return false;
1206
1207                if ( $checkWhitelist && in_array( $ip, $wgProxyWhitelist ) )
1208                        return false;
1209
1210                $urls = array_merge( $wgDnsBlacklistUrls, (array)$wgSorbsUrl );
1211                return $this->inDnsBlacklist( $ip, $urls );
1212        }
1213
1214        /**
1215         * Whether the given IP is in a given DNS blacklist.
1216         *
1217         * @param $ip \string IP to check
1218         * @param $bases \string or Array of Strings: URL of the DNS blacklist
1219         * @return \bool True if blacklisted.
1220         */
1221        function inDnsBlacklist( $ip, $bases ) {
1222                wfProfileIn( __METHOD__ );
1223
1224                $found = false;
1225                $host = '';
1226                // FIXME: IPv6 ???  (http://bugs.php.net/bug.php?id=33170)
1227                if( IP::isIPv4( $ip ) ) {
1228                        # Reverse IP, bug 21255
1229                        $ipReversed = implode( '.', array_reverse( explode( '.', $ip ) ) );
1230
1231                        foreach( (array)$bases as $base ) {
1232                                # Make hostname
1233                                $host = "$ipReversed.$base";
1234
1235                                # Send query
1236                                $ipList = gethostbynamel( $host );
1237
1238                                if( $ipList ) {
1239                                        wfDebug( "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" );
1240                                        $found = true;
1241                                        break;
1242                                } else {
1243                                        wfDebug( "Requested $host, not found in $base.\n" );
1244                                }
1245                        }
1246                }
1247
1248                wfProfileOut( __METHOD__ );
1249                return $found;
1250        }
1251
1252        /**
1253         * Is this user subject to rate limiting?
1254         *
1255         * @return \bool True if rate limited
1256         */
1257        public function isPingLimitable() {
1258                global $wgRateLimitsExcludedGroups;
1259                global $wgRateLimitsExcludedIPs;
1260                if( array_intersect( $this->getEffectiveGroups(), $wgRateLimitsExcludedGroups ) ) {
1261                        // Deprecated, but kept for backwards-compatibility config
1262                        return false;
1263                }
1264                if( in_array( wfGetIP(), $wgRateLimitsExcludedIPs ) ) {
1265                        // No other good way currently to disable rate limits
1266                        // for specific IPs. :P
1267                        // But this is a crappy hack and should die.
1268                        return false;
1269                }
1270                return !$this->isAllowed('noratelimit');
1271        }
1272
1273        /**
1274         * Primitive rate limits: enforce maximum actions per time period
1275         * to put a brake on flooding.
1276         *
1277         * @note When using a shared cache like memcached, IP-address
1278         * last-hit counters will be shared across wikis.
1279         *
1280         * @param $action \string Action to enforce; 'edit' if unspecified
1281         * @return \bool True if a rate limiter was tripped
1282         */
1283        function pingLimiter( $action = 'edit' ) {
1284                # Call the 'PingLimiter' hook
1285                $result = false;
1286                if( !wfRunHooks( 'PingLimiter', array( &$this, $action, $result ) ) ) {
1287                        return $result;
1288                }
1289
1290                global $wgRateLimits;
1291                if( !isset( $wgRateLimits[$action] ) ) {
1292                        return false;
1293                }
1294
1295                # Some groups shouldn't trigger the ping limiter, ever
1296                if( !$this->isPingLimitable() )
1297                        return false;
1298
1299                global $wgMemc, $wgRateLimitLog;
1300                wfProfileIn( __METHOD__ );
1301
1302                $limits = $wgRateLimits[$action];
1303                $keys = array();
1304                $id = $this->getId();
1305                $ip = wfGetIP();
1306                $userLimit = false;
1307
1308                if( isset( $limits['anon'] ) && $id == 0 ) {
1309                        $keys[wfMemcKey( 'limiter', $action, 'anon' )] = $limits['anon'];
1310                }
1311
1312                if( isset( $limits['user'] ) && $id != 0 ) {
1313                        $userLimit = $limits['user'];
1314                }
1315                if( $this->isNewbie() ) {
1316                        if( isset( $limits['newbie'] ) && $id != 0 ) {
1317                                $keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $limits['newbie'];
1318                        }
1319                        if( isset( $limits['ip'] ) ) {
1320                                $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip'];
1321                        }
1322                        $matches = array();
1323                        if( isset( $limits['subnet'] ) && preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
1324                                $subnet = $matches[1];
1325                                $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
1326                        }
1327                }
1328                // Check for group-specific permissions
1329                // If more than one group applies, use the group with the highest limit
1330                foreach ( $this->getGroups() as $group ) {
1331                        if ( isset( $limits[$group] ) ) {
1332                                if ( $userLimit === false || $limits[$group] > $userLimit ) {
1333                                        $userLimit = $limits[$group];
1334                                }
1335                        }
1336                }
1337                // Set the user limit key
1338                if ( $userLimit !== false ) {
1339                        wfDebug( __METHOD__ . ": effective user limit: $userLimit\n" );
1340                        $keys[ wfMemcKey( 'limiter', $action, 'user', $id ) ] = $userLimit;
1341                }
1342
1343                $triggered = false;
1344                foreach( $keys as $key => $limit ) {
1345                        list( $max, $period ) = $limit;
1346                        $summary = "(limit $max in {$period}s)";
1347                        $count = $wgMemc->get( $key );
1348                        // Already pinged?
1349                        if( $count ) {
1350                                if( $count > $max ) {
1351                                        wfDebug( __METHOD__ . ": tripped! $key at $count $summary\n" );
1352                                        if( $wgRateLimitLog ) {
1353                                                @error_log( wfTimestamp( TS_MW ) . ' ' . wfWikiID() . ': ' . $this->getName() . " tripped $key at $count $summary\n", 3, $wgRateLimitLog );
1354                                        }
1355                                        $triggered = true;
1356                                } else {
1357                                        wfDebug( __METHOD__ . ": ok. $key at $count $summary\n" );
1358                                }
1359                        } else {
1360                                wfDebug( __METHOD__ . ": adding record for $key $summary\n" );
1361                                $wgMemc->add( $key, 0, intval( $period ) ); // first ping
1362                        }
1363                        $wgMemc->incr( $key );
1364                }
1365
1366                wfProfileOut( __METHOD__ );
1367                return $triggered;
1368        }
1369
1370        /**
1371         * Check if user is blocked
1372         *
1373         * @param $bFromSlave \bool Whether to check the slave database instead of the master
1374         * @return \bool True if blocked, false otherwise
1375         */
1376        function isBlocked( $bFromSlave = true ) { // hacked from false due to horrible probs on site
1377                wfDebug( "User::isBlocked: enter\n" );
1378                $this->getBlockedStatus( $bFromSlave );
1379                return $this->mBlockedby !== 0;
1380        }
1381
1382        /**
1383         * Check if user is blocked from editing a particular article
1384         *
1385         * @param $title      \string Title to check
1386         * @param $bFromSlave \bool   Whether to check the slave database instead of the master
1387         * @return \bool True if blocked, false otherwise
1388         */
1389        function isBlockedFrom( $title, $bFromSlave = false ) {
1390                global $wgBlockAllowsUTEdit;
1391                wfProfileIn( __METHOD__ );
1392                wfDebug( __METHOD__ . ": enter\n" );
1393
1394                wfDebug( __METHOD__ . ": asking isBlocked()\n" );
1395                $blocked = $this->isBlocked( $bFromSlave );
1396                $allowUsertalk = ( $wgBlockAllowsUTEdit ? $this->mAllowUsertalk : false );
1397                # If a user's name is suppressed, they cannot make edits anywhere
1398                if ( !$this->mHideName && $allowUsertalk && $title->getText() === $this->getName() &&
1399                  $title->getNamespace() == NS_USER_TALK ) {
1400                        $blocked = false;
1401                        wfDebug( __METHOD__ . ": self-talk page, ignoring any blocks\n" );
1402                }
1403
1404                wfRunHooks( 'UserIsBlockedFrom', array( $this, $title, &$blocked, &$allowUsertalk ) );
1405
1406                wfProfileOut( __METHOD__ );
1407                return $blocked;
1408        }
1409
1410        /**
1411         * If user is blocked, return the name of the user who placed the block
1412         * @return \string name of blocker
1413         */
1414        function blockedBy() {
1415                $this->getBlockedStatus();
1416                return $this->mBlockedby;
1417        }
1418
1419        /**
1420         * If user is blocked, return the specified reason for the block
1421         * @return \string Blocking reason
1422         */
1423        function blockedFor() {
1424                $this->getBlockedStatus();
1425                return $this->mBlockreason;
1426        }
1427
1428        /**
1429         * If user is blocked, return the ID for the block
1430         * @return \int Block ID
1431         */
1432        function getBlockId() {
1433                $this->getBlockedStatus();
1434                return ( $this->mBlock ? $this->mBlock->mId : false );
1435        }
1436
1437        /**
1438         * Check if user is blocked on all wikis.
1439         * Do not use for actual edit permission checks!
1440         * This is intented for quick UI checks.
1441         *
1442         * @param $ip \type{\string} IP address, uses current client if none given
1443         * @return \type{\bool} True if blocked, false otherwise
1444         */
1445        function isBlockedGlobally( $ip = '' ) {
1446                if( $this->mBlockedGlobally !== null ) {
1447                        return $this->mBlockedGlobally;
1448                }
1449                // User is already an IP?
1450                if( IP::isIPAddress( $this->getName() ) ) {
1451                        $ip = $this->getName();
1452                } else if( !$ip ) {
1453                        $ip = wfGetIP();
1454                }
1455                $blocked = false;
1456                wfRunHooks( 'UserIsBlockedGlobally', array( &$this, $ip, &$blocked ) );
1457                $this->mBlockedGlobally = (bool)$blocked;
1458                return $this->mBlockedGlobally;
1459        }
1460
1461        /**
1462         * Check if user account is locked
1463         *
1464         * @return \type{\bool} True if locked, false otherwise
1465         */
1466        function isLocked() {
1467                if( $this->mLocked !== null ) {
1468                        return $this->mLocked;
1469                }
1470                global $wgAuth;
1471                $authUser = $wgAuth->getUserInstance( $this );
1472                $this->mLocked = (bool)$authUser->isLocked();
1473                return $this->mLocked;
1474        }
1475
1476        /**
1477         * Check if user account is hidden
1478         *
1479         * @return \type{\bool} True if hidden, false otherwise
1480         */
1481        function isHidden() {
1482                if( $this->mHideName !== null ) {
1483                        return $this->mHideName;
1484                }
1485                $this->getBlockedStatus();
1486                if( !$this->mHideName ) {
1487                        global $wgAuth;
1488                        $authUser = $wgAuth->getUserInstance( $this );
1489                        $this->mHideName = (bool)$authUser->isHidden();
1490                }
1491                return $this->mHideName;
1492        }
1493
1494        /**
1495         * Get the user's ID.
1496         * @return \int The user's ID; 0 if the user is anonymous or nonexistent
1497         */
1498        function getId() {
1499                if( $this->mId === null and $this->mName !== null
1500                and User::isIP( $this->mName ) ) {
1501                        // Special case, we know the user is anonymous
1502                        return 0;
1503                } elseif( $this->mId === null ) {
1504                        // Don't load if this was initialized from an ID
1505                        $this->load();
1506                }
1507                return $this->mId;
1508        }
1509
1510        /**
1511         * Set the user and reload all fields according to a given ID
1512         * @param $v \int User ID to reload
1513         */
1514        function setId( $v ) {
1515                $this->mId = $v;
1516                $this->clearInstanceCache( 'id' );
1517        }
1518
1519        /**
1520         * Get the user name, or the IP of an anonymous user
1521         * @return \string User's name or IP address
1522         */
1523        function getName() {
1524                if ( !$this->mDataLoaded && $this->mFrom == 'name' ) {
1525                        # Special case optimisation
1526                        return $this->mName;
1527                } else {
1528                        $this->load();
1529                        if ( $this->mName === false ) {
1530                                # Clean up IPs
1531                                $this->mName = IP::sanitizeIP( wfGetIP() );
1532                        }
1533                        return $this->mName;
1534                }
1535        }
1536
1537        /**
1538         * Set the user name.
1539         *
1540         * This does not reload fields from the database according to the given
1541         * name. Rather, it is used to create a temporary "nonexistent user" for
1542         * later addition to the database. It can also be used to set the IP
1543         * address for an anonymous user to something other than the current
1544         * remote IP.
1545         *
1546         * @note User::newFromName() has rougly the same function, when the named user
1547         * does not exist.
1548         * @param $str \string New user name to set
1549         */
1550        function setName( $str ) {
1551                $this->load();
1552                $this->mName = $str;
1553        }
1554
1555        /**
1556         * Get the user's name escaped by underscores.
1557         * @return \string Username escaped by underscores.
1558         */
1559        function getTitleKey() {
1560                return str_replace( ' ', '_', $this->getName() );
1561        }
1562
1563        /**
1564         * Check if the user has new messages.
1565         * @return \bool True if the user has new messages
1566         */
1567        function getNewtalk() {
1568                $this->load();
1569
1570                # Load the newtalk status if it is unloaded (mNewtalk=-1)
1571                if( $this->mNewtalk === -1 ) {
1572                        $this->mNewtalk = false; # reset talk page status
1573
1574                        # Check memcached separately for anons, who have no
1575                        # entire User object stored in there.
1576                        if( !$this->mId ) {
1577                                global $wgMemc;
1578                                $key = wfMemcKey( 'newtalk', 'ip', $this->getName() );
1579                                $newtalk = $wgMemc->get( $key );
1580                                if( strval( $newtalk ) !== '' ) {
1581                                        $this->mNewtalk = (bool)$newtalk;
1582                                } else {
1583                                        // Since we are caching this, make sure it is up to date by getting it
1584                                        // from the master
1585                                        $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName(), true );
1586                                        $wgMemc->set( $key, (int)$this->mNewtalk, 1800 );
1587                                }
1588                        } else {
1589                                $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
1590                        }
1591                }
1592
1593                return (bool)$this->mNewtalk;
1594        }
1595
1596        /**
1597         * Return the talk page(s) this user has new messages on.
1598         * @return \type{\arrayof{\string}} Array of page URLs
1599         */
1600        function getNewMessageLinks() {
1601                $talks = array();
1602                if( !wfRunHooks( 'UserRetrieveNewTalks', array( &$this, &$talks ) ) )
1603                        return $talks;
1604
1605                if( !$this->getNewtalk() )
1606                        return array();
1607                $up = $this->getUserPage();
1608                $utp = $up->getTalkPage();
1609                return array( array( 'wiki' => wfWikiID(), 'link' => $utp->getLocalURL() ) );
1610        }
1611
1612        /**
1613         * Internal uncached check for new messages
1614         *
1615         * @see getNewtalk()
1616         * @param $field \string 'user_ip' for anonymous users, 'user_id' otherwise
1617         * @param $id \types{\string,\int} User's IP address for anonymous users, User ID otherwise
1618         * @param $fromMaster \bool true to fetch from the master, false for a slave
1619         * @return \bool True if the user has new messages
1620         * @private
1621         */
1622        function checkNewtalk( $field, $id, $fromMaster = false ) {
1623                if ( $fromMaster ) {
1624                        $db = wfGetDB( DB_MASTER );
1625                } else {
1626                        $db = wfGetDB( DB_SLAVE );
1627                }
1628                $ok = $db->selectField( 'user_newtalk', $field,
1629                        array( $field => $id ), __METHOD__ );
1630                return $ok !== false;
1631        }
1632
1633        /**
1634         * Add or update the new messages flag
1635         * @param $field \string 'user_ip' for anonymous users, 'user_id' otherwise
1636         * @param $id \types{\string,\int} User's IP address for anonymous users, User ID otherwise
1637         * @return \bool True if successful, false otherwise
1638         * @private
1639         */
1640        function updateNewtalk( $field, $id ) {
1641                $dbw = wfGetDB( DB_MASTER );
1642                $dbw->insert( 'user_newtalk',
1643                        array( $field => $id ),
1644                        __METHOD__,
1645                        'IGNORE' );
1646                if ( $dbw->affectedRows() ) {
1647                        wfDebug( __METHOD__ . ": set on ($field, $id)\n" );
1648                        return true;
1649                } else {
1650                        wfDebug( __METHOD__ . " already set ($field, $id)\n" );
1651                        return false;
1652                }
1653        }
1654
1655        /**
1656         * Clear the new messages flag for the given user
1657         * @param $field \string 'user_ip' for anonymous users, 'user_id' otherwise
1658         * @param $id \types{\string,\int} User's IP address for anonymous users, User ID otherwise
1659         * @return \bool True if successful, false otherwise
1660         * @private
1661         */
1662        function deleteNewtalk( $field, $id ) {
1663                $dbw = wfGetDB( DB_MASTER );
1664                $dbw->delete( 'user_newtalk',
1665                        array( $field => $id ),
1666                        __METHOD__ );
1667                if ( $dbw->affectedRows() ) {
1668                        wfDebug( __METHOD__ . ": killed on ($field, $id)\n" );
1669                        return true;
1670                } else {
1671                        wfDebug( __METHOD__ . ": already gone ($field, $id)\n" );
1672                        return false;
1673                }
1674        }
1675
1676        /**
1677         * Update the 'You have new messages!' status.
1678         * @param $val \bool Whether the user has new messages
1679         */
1680        function setNewtalk( $val ) {
1681                if( wfReadOnly() ) {
1682                        return;
1683                }
1684
1685                $this->load();
1686                $this->mNewtalk = $val;
1687
1688                if( $this->isAnon() ) {
1689                        $field = 'user_ip';
1690                        $id = $this->getName();
1691                } else {
1692                        $field = 'user_id';
1693                        $id = $this->getId();
1694                }
1695                global $wgMemc;
1696
1697                if( $val ) {
1698                        $changed = $this->updateNewtalk( $field, $id );
1699                } else {
1700                        $changed = $this->deleteNewtalk( $field, $id );
1701                }
1702
1703                if( $this->isAnon() ) {
1704                        // Anons have a separate memcached space, since
1705                        // user records aren't kept for them.
1706                        $key = wfMemcKey( 'newtalk', 'ip', $id );
1707                        $wgMemc->set( $key, $val ? 1 : 0, 1800 );
1708                }
1709                if ( $changed ) {
1710                        $this->invalidateCache();
1711                }
1712        }
1713
1714        /**
1715         * Generate a current or new-future timestamp to be stored in the
1716         * user_touched field when we update things.
1717         * @return \string Timestamp in TS_MW format
1718         */
1719        private static function newTouchedTimestamp() {
1720                global $wgClockSkewFudge;
1721                return wfTimestamp( TS_MW, time() + $wgClockSkewFudge );
1722        }
1723
1724        /**
1725         * Clear user data from memcached.
1726         * Use after applying fun updates to the database; caller's
1727         * responsibility to update user_touched if appropriate.
1728         *
1729         * Called implicitly from invalidateCache() and saveSettings().
1730         */
1731        private function clearSharedCache() {
1732                $this->load();
1733                if( $this->mId ) {
1734                        global $wgMemc;
1735                        $wgMemc->delete( wfMemcKey( 'user', 'id', $this->mId ) );
1736                }
1737        }
1738
1739        /**
1740         * Immediately touch the user data cache for this account.
1741         * Updates user_touched field, and removes account data from memcached
1742         * for reload on the next hit.
1743         */
1744        function invalidateCache() {
1745                if( wfReadOnly() ) {
1746                        return;
1747                }
1748                $this->load();
1749                if( $this->mId ) {
1750                        $this->mTouched = self::newTouchedTimestamp();
1751
1752                        $dbw = wfGetDB( DB_MASTER );
1753                        $dbw->update( 'user',
1754                                array( 'user_touched' => $dbw->timestamp( $this->mTouched ) ),
1755                                array( 'user_id' => $this->mId ),
1756                                __METHOD__ );
1757
1758                        $this->clearSharedCache();
1759                }
1760        }
1761
1762        /**
1763         * Validate the cache for this account.
1764         * @param $timestamp \string A timestamp in TS_MW format
1765         */
1766        function validateCache( $timestamp ) {
1767                $this->load();
1768                return ( $timestamp >= $this->mTouched );
1769        }
1770
1771        /**
1772         * Get the user touched timestamp
1773         */
1774        function getTouched() {
1775                $this->load();
1776                return $this->mTouched;
1777        }
1778
1779        /**
1780         * Set the password and reset the random token.
1781         * Calls through to authentication plugin if necessary;
1782         * will have no effect if the auth plugin refuses to
1783         * pass the change through or if the legal password
1784         * checks fail.
1785         *
1786         * As a special case, setting the password to null
1787         * wipes it, so the account cannot be logged in until
1788         * a new password is set, for instance via e-mail.
1789         *
1790         * @param $str \string New password to set
1791         * @throws PasswordError on failure
1792         */
1793        function setPassword( $str ) {
1794                global $wgAuth;
1795
1796                if( $str !== null ) {
1797                        if( !$wgAuth->allowPasswordChange() ) {
1798                                throw new PasswordError( wfMsg( 'password-change-forbidden' ) );
1799                        }
1800
1801                        if( !$this->isValidPassword( $str ) ) {
1802                                global $wgMinimalPasswordLength;
1803                                $valid = $this->getPasswordValidity( $str );
1804                                throw new PasswordError( wfMsgExt( $valid, array( 'parsemag' ),
1805                                        $wgMinimalPasswordLength ) );
1806                        }
1807                }
1808
1809                if( !$wgAuth->setPassword( $this, $str ) ) {
1810                        throw new PasswordError( wfMsg( 'externaldberror' ) );
1811                }
1812
1813                $this->setInternalPassword( $str );
1814
1815                return true;
1816        }
1817
1818        /**
1819         * Set the password and reset the random token unconditionally.
1820         *
1821         * @param $str \string New password to set
1822         */
1823        function setInternalPassword( $str ) {
1824                $this->load();
1825                $this->setToken();
1826
1827                if( $str === null ) {
1828                        // Save an invalid hash...
1829                        $this->mPassword = '';
1830                } else {
1831                        $this->mPassword = self::crypt( $str );
1832                }
1833                $this->mNewpassword = '';
1834                $this->mNewpassTime = null;
1835        }
1836
1837        /**
1838         * Get the user's current token.
1839         * @return \string Token
1840         */
1841        function getToken() {
1842                $this->load();
1843                return $this->mToken;
1844        }
1845
1846        /**
1847         * Set the random token (used for persistent authentication)
1848         * Called from loadDefaults() among other places.
1849         *
1850         * @param $token \string If specified, set the token to this value
1851         * @private
1852         */
1853        function setToken( $token = false ) {
1854                global $wgSecretKey, $wgProxyKey;
1855                $this->load();
1856                if ( !$token ) {
1857                        if ( $wgSecretKey ) {
1858                                $key = $wgSecretKey;
1859                        } elseif ( $wgProxyKey ) {
1860                                $key = $wgProxyKey;
1861                        } else {
1862                                $key = microtime();
1863                        }
1864                        $this->mToken = md5( $key . mt_rand( 0, 0x7fffffff ) . wfWikiID() . $this->mId );
1865                } else {
1866                        $this->mToken = $token;
1867                }
1868        }
1869
1870        /**
1871         * Set the cookie password
1872         *
1873         * @param $str \string New cookie password
1874         * @private
1875         */
1876        function setCookiePassword( $str ) {
1877                $this->load();
1878                $this->mCookiePassword = md5( $str );
1879        }
1880
1881        /**
1882         * Set the password for a password reminder or new account email
1883         *
1884         * @param $str \string New password to set
1885         * @param $throttle \bool If true, reset the throttle timestamp to the present
1886         */
1887        function setNewpassword( $str, $throttle = true ) {
1888                $this->load();
1889                $this->mNewpassword = self::crypt( $str );
1890                if ( $throttle ) {
1891                        $this->mNewpassTime = wfTimestampNow();
1892                }
1893        }
1894
1895        /**
1896         * Has password reminder email been sent within the last
1897         * $wgPasswordReminderResendTime hours?
1898         * @return \bool True or false
1899         */
1900        function isPasswordReminderThrottled() {
1901                global $wgPasswordReminderResendTime;
1902                $this->load();
1903                if ( !$this->mNewpassTime || !$wgPasswordReminderResendTime ) {
1904                        return false;
1905                }
1906                $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgPasswordReminderResendTime * 3600;
1907                return time() < $expiry;
1908        }
1909
1910        /**
1911         * Get the user's e-mail address
1912         * @return \string User's email address
1913         */
1914        function getEmail() {
1915                $this->load();
1916                wfRunHooks( 'UserGetEmail', array( $this, &$this->mEmail ) );
1917                return $this->mEmail;
1918        }
1919
1920        /**
1921         * Get the timestamp of the user's e-mail authentication
1922         * @return \string TS_MW timestamp
1923         */
1924        function getEmailAuthenticationTimestamp() {
1925                $this->load();
1926                wfRunHooks( 'UserGetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
1927                return $this->mEmailAuthenticated;
1928        }
1929
1930        /**
1931         * Set the user's e-mail address
1932         * @param $str \string New e-mail address
1933         */
1934        function setEmail( $str ) {
1935                $this->load();
1936                $this->mEmail = $str;
1937                wfRunHooks( 'UserSetEmail', array( $this, &$this->mEmail ) );
1938        }
1939
1940        /**
1941         * Get the user's real name
1942         * @return \string User's real name
1943         */
1944        function getRealName() {
1945                $this->load();
1946                return $this->mRealName;
1947        }
1948
1949        /**
1950         * Set the user's real name
1951         * @param $str \string New real name
1952         */
1953        function setRealName( $str ) {
1954                $this->load();
1955                $this->mRealName = $str;
1956        }
1957
1958        /**
1959         * Get the user's current setting for a given option.
1960         *
1961         * @param $oname \string The option to check
1962         * @param $defaultOverride \string A default value returned if the option does not exist
1963         * @return \string User's current value for the option
1964         * @see getBoolOption()
1965         * @see getIntOption()
1966         */
1967        function getOption( $oname, $defaultOverride = null ) {
1968                $this->loadOptions();
1969
1970                if ( is_null( $this->mOptions ) ) {
1971                        if($defaultOverride != '') {
1972                                return $defaultOverride;
1973                        }
1974                        $this->mOptions = User::getDefaultOptions();
1975                }
1976
1977                if ( array_key_exists( $oname, $this->mOptions ) ) {
1978                        return $this->mOptions[$oname];
1979                } else {
1980                        return $defaultOverride;
1981                }
1982        }
1983
1984        /**
1985         * Get all user's options
1986         *
1987         * @return array
1988         */
1989        public function getOptions() {
1990                $this->loadOptions();
1991                return $this->mOptions;
1992        }
1993
1994        /**
1995         * Get the user's current setting for a given option, as a boolean value.
1996         *
1997         * @param $oname \string The option to check
1998         * @return \bool User's current value for the option
1999         * @see getOption()
2000         */
2001        function getBoolOption( $oname ) {
2002                return (bool)$this->getOption( $oname );
2003        }
2004
2005
2006        /**
2007         * Get the user's current setting for a given option, as a boolean value.
2008         *
2009         * @param $oname \string The option to check
2010         * @param $defaultOverride \int A default value returned if the option does not exist
2011         * @return \int User's current value for the option
2012         * @see getOption()
2013         */
2014        function getIntOption( $oname, $defaultOverride=0 ) {
2015                $val = $this->getOption( $oname );
2016                if( $val == '' ) {
2017                        $val = $defaultOverride;
2018                }
2019                return intval( $val );
2020        }
2021
2022        /**
2023         * Set the given option for a user.
2024         *
2025         * @param $oname \string The option to set
2026         * @param $val \mixed New value to set
2027         */
2028        function setOption( $oname, $val ) {
2029                $this->load();
2030                $this->loadOptions();
2031
2032                if ( $oname == 'skin' ) {
2033                        # Clear cached skin, so the new one displays immediately in Special:Preferences
2034                        unset( $this->mSkin );
2035                }
2036
2037                // Explicitly NULL values should refer to defaults
2038                global $wgDefaultUserOptions;
2039                if( is_null( $val ) && isset( $wgDefaultUserOptions[$oname] ) ) {
2040                        $val = $wgDefaultUserOptions[$oname];
2041                }
2042
2043                $this->mOptions[$oname] = $val;
2044        }
2045
2046        /**
2047         * Reset all options to the site defaults
2048         */
2049        function resetOptions() {
2050                $this->mOptions = User::getDefaultOptions();
2051        }
2052
2053        /**
2054         * Get the user's preferred date format.
2055         * @return \string User's preferred date format
2056         */
2057        function getDatePreference() {
2058                // Important migration for old data rows
2059                if ( is_null( $this->mDatePreference ) ) {
2060                        global $wgLang;
2061                        $value = $this->getOption( 'date' );
2062                        $map = $wgLang->getDatePreferenceMigrationMap();
2063                        if ( isset( $map[$value] ) ) {
2064                                $value = $map[$value];
2065                        }
2066                        $this->mDatePreference = $value;
2067                }
2068                return $this->mDatePreference;
2069        }
2070
2071        /**
2072         * Get the permissions this user has.
2073         * @return \type{\arrayof{\string}} Array of permission names
2074         */
2075        function getRights() {
2076                if ( is_null( $this->mRights ) ) {
2077                        $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() );
2078                        wfRunHooks( 'UserGetRights', array( $this, &$this->mRights ) );
2079                        // Force reindexation of rights when a hook has unset one of them
2080                        $this->mRights = array_values( $this->mRights );
2081                }
2082                return $this->mRights;
2083        }
2084
2085        /**
2086         * Get the list of explicit group memberships this user has.
2087         * The implicit * and user groups are not included.
2088         * @return \type{\arrayof{\string}} Array of internal group names
2089         */
2090        function getGroups() {
2091                $this->load();
2092                return $this->mGroups;
2093        }
2094
2095        /**
2096         * Get the list of implicit group memberships this user has.
2097         * This includes all explicit groups, plus 'user' if logged in,
2098         * '*' for all accounts and autopromoted groups
2099         * @param $recache \bool Whether to avoid the cache
2100         * @return \type{\arrayof{\string}} Array of internal group names
2101         */
2102        function getEffectiveGroups( $recache = false ) {
2103                if ( $recache || is_null( $this->mEffectiveGroups ) ) {
2104                        wfProfileIn( __METHOD__ );
2105                        $this->mEffectiveGroups = $this->getGroups();
2106                        $this->mEffectiveGroups[] = '*';
2107                        if( $this->getId() ) {
2108                                $this->mEffectiveGroups[] = 'user';
2109
2110                                $this->mEffectiveGroups = array_unique( array_merge(
2111                                        $this->mEffectiveGroups,
2112                                        Autopromote::getAutopromoteGroups( $this )
2113                                ) );
2114
2115                                # Hook for additional groups
2116                                wfRunHooks( 'UserEffectiveGroups', array( &$this, &$this->mEffectiveGroups ) );
2117                        }
2118                        wfProfileOut( __METHOD__ );
2119                }
2120                return $this->mEffectiveGroups;
2121        }
2122
2123        /**
2124         * Get the user's edit count.
2125         * @return \int User'e edit count
2126         */
2127        function getEditCount() {
2128                if( $this->getId() ) {
2129                        if ( !isset( $this->mEditCount ) ) {
2130                                /* Populate the count, if it has not been populated yet */
2131                                $this->mEditCount = User::edits( $this->mId );
2132                        }
2133                        return $this->mEditCount;
2134                } else {
2135                        /* nil */
2136                        return null;
2137                }
2138        }
2139
2140        /**
2141         * Add the user to the given group.
2142         * This takes immediate effect.
2143         * @param $group \string Name of the group to add
2144         */
2145        function addGroup( $group ) {
2146                $dbw = wfGetDB( DB_MASTER );
2147                if( $this->getId() ) {
2148                        $dbw->insert( 'user_groups',
2149                                array(
2150                                        'ug_user'  => $this->getID(),
2151                                        'ug_group' => $group,
2152                                ),
2153                                'User::addGroup',
2154                                array( 'IGNORE' ) );
2155                }
2156
2157                $this->loadGroups();
2158                $this->mGroups[] = $group;
2159                $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
2160
2161                $this->invalidateCache();
2162        }
2163
2164        /**
2165         * Remove the user from the given group.
2166         * This takes immediate effect.
2167         * @param $group \string Name of the group to remove
2168         */
2169        function removeGroup( $group ) {
2170                $this->load();
2171                $dbw = wfGetDB( DB_MASTER );
2172                $dbw->delete( 'user_groups',
2173                        array(
2174                                'ug_user'  => $this->getID(),
2175                                'ug_group' => $group,
2176                        ),
2177                        'User::removeGroup' );
2178
2179                $this->loadGroups();
2180                $this->mGroups = array_diff( $this->mGroups, array( $group ) );
2181                $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
2182
2183                $this->invalidateCache();
2184        }
2185
2186        /**
2187         * Get whether the user is logged in
2188         * @return \bool True or false
2189         */
2190        function isLoggedIn() {
2191                return $this->getID() != 0;
2192        }
2193
2194        /**
2195         * Get whether the user is anonymous
2196         * @return \bool True or false
2197         */
2198        function isAnon() {
2199                return !$this->isLoggedIn();
2200        }
2201
2202        /**
2203         * Get whether the user is a bot
2204         * @return \bool True or false
2205         * @deprecated
2206         */
2207        function isBot() {
2208                wfDeprecated( __METHOD__ );
2209                return $this->isAllowed( 'bot' );
2210        }
2211
2212        /**
2213         * Check if user is allowed to access a feature / make an action
2214         * @param $action \string action to be checked
2215         * @return \bool True if action is allowed, else false
2216         */
2217        function isAllowed( $action = '' ) {
2218                if ( $action === '' )
2219                        return true; // In the spirit of DWIM
2220                # Patrolling may not be enabled
2221                if( $action === 'patrol' || $action === 'autopatrol' ) {
2222                        global $wgUseRCPatrol, $wgUseNPPatrol;
2223                        if( !$wgUseRCPatrol && !$wgUseNPPatrol )
2224                                return false;
2225                }
2226                # Use strict parameter to avoid matching numeric 0 accidentally inserted
2227                # by misconfiguration: 0 == 'foo'
2228                return in_array( $action, $this->getRights(), true );
2229        }
2230
2231        /**
2232         * Check whether to enable recent changes patrol features for this user
2233         * @return \bool True or false
2234         */
2235        public function useRCPatrol() {
2236                global $wgUseRCPatrol;
2237                return( $wgUseRCPatrol && ( $this->isAllowed( 'patrol' ) || $this->isAllowed( 'patrolmarks' ) ) );
2238        }
2239
2240        /**
2241         * Check whether to enable new pages patrol features for this user
2242         * @return \bool True or false
2243         */
2244        public function useNPPatrol() {
2245                global $wgUseRCPatrol, $wgUseNPPatrol;
2246                return( ( $wgUseRCPatrol || $wgUseNPPatrol ) && ( $this->isAllowed( 'patrol' ) || $this->isAllowed( 'patrolmarks' ) ) );
2247        }
2248
2249        /**
2250         * Get the current skin, loading it if required, and setting a title
2251         * @param $t Title: the title to use in the skin
2252         * @return Skin The current skin
2253         * @todo FIXME : need to check the old failback system [AV]
2254         */
2255        function &getSkin( $t = null ) {
2256                if ( !isset( $this->mSkin ) ) {
2257                        wfProfileIn( __METHOD__ );
2258
2259                        global $wgHiddenPrefs;
2260                        if( !in_array( 'skin', $wgHiddenPrefs ) ) {
2261                                # get the user skin
2262                                global $wgRequest;
2263                                $userSkin = $this->getOption( 'skin' );
2264                                $userSkin = $wgRequest->getVal( 'useskin', $userSkin );
2265                        } else {
2266                                # if we're not allowing users to override, then use the default
2267                                global $wgDefaultSkin;
2268                                $userSkin = $wgDefaultSkin;
2269                        }
2270
2271                        $this->mSkin =& Skin::newFromKey( $userSkin );
2272                        wfProfileOut( __METHOD__ );
2273                }
2274                if( $t || !$this->mSkin->getTitle() ) {
2275                        if ( !$t ) {
2276                                global $wgOut;
2277                                $t = $wgOut->getTitle();
2278                        }
2279                        $this->mSkin->setTitle( $t );
2280                }
2281                return $this->mSkin;
2282        }
2283
2284        /**
2285         * Check the watched status of an article.
2286         * @param $title \type{Title} Title of the article to look at
2287         * @return \bool True if article is watched
2288         */
2289        function isWatched( $title ) {
2290                $wl = WatchedItem::fromUserTitle( $this, $title );
2291                return $wl->isWatched();
2292        }
2293
2294        /**
2295         * Watch an article.
2296         * @param $title \type{Title} Title of the article to look at
2297         */
2298        function addWatch( $title ) {
2299                $wl = WatchedItem::fromUserTitle( $this, $title );
2300                $wl->addWatch();
2301                $this->invalidateCache();
2302        }
2303
2304        /**
2305         * Stop watching an article.
2306         * @param $title \type{Title} Title of the article to look at
2307         */
2308        function removeWatch( $title ) {
2309                $wl = WatchedItem::fromUserTitle( $this, $title );
2310                $wl->removeWatch();
2311                $this->invalidateCache();
2312        }
2313
2314        /**
2315         * Clear the user's notification timestamp for the given title.
2316         * If e-notif e-mails are on, they will receive notification mails on
2317         * the next change of the page if it's watched etc.
2318         * @param $title \type{Title} Title of the article to look at
2319         */
2320        function clearNotification( &$title ) {
2321                global $wgUser, $wgUseEnotif, $wgShowUpdatedMarker;
2322
2323                # Do nothing if the database is locked to writes
2324                if( wfReadOnly() ) {
2325                        return;
2326                }
2327
2328                if( $title->getNamespace() == NS_USER_TALK &&
2329                        $title->getText() == $this->getName() ) {
2330                        if( !wfRunHooks( 'UserClearNewTalkNotification', array( &$this ) ) )
2331                                return;
2332                        $this->setNewtalk( false );
2333                }
2334
2335                if( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
2336                        return;
2337                }
2338
2339                if( $this->isAnon() ) {
2340                        // Nothing else to do...
2341                        return;
2342                }
2343
2344                // Only update the timestamp if the page is being watched.
2345                // The query to find out if it is watched is cached both in memcached and per-invocation,
2346                // and when it does have to be executed, it can be on a slave
2347                // If this is the user's newtalk page, we always update the timestamp
2348                if( $title->getNamespace() == NS_USER_TALK &&
2349                        $title->getText() == $wgUser->getName() )
2350                {
2351                        $watched = true;
2352                } elseif ( $this->getId() == $wgUser->getId() ) {
2353                        $watched = $title->userIsWatching();
2354                } else {
2355                        $watched = true;
2356                }
2357
2358                // If the page is watched by the user (or may be watched), update the timestamp on any
2359                // any matching rows
2360                if ( $watched ) {
2361                        $dbw = wfGetDB( DB_MASTER );
2362                        $dbw->update( 'watchlist',
2363                                        array( /* SET */
2364                                                'wl_notificationtimestamp' => null
2365                                        ), array( /* WHERE */
2366                                                'wl_title' => $title->getDBkey(),
2367                                                'wl_namespace' => $title->getNamespace(),
2368                                                'wl_user' => $this->getID()
2369                                        ), __METHOD__
2370                        );
2371                }
2372        }
2373
2374        /**
2375         * Resets all of the given user's page-change notification timestamps.
2376         * If e-notif e-mails are on, they will receive notification mails on
2377         * the next change of any watched page.
2378         *
2379         * @param $currentUser \int User ID
2380         */
2381        function clearAllNotifications( $currentUser ) {
2382                global $wgUseEnotif, $wgShowUpdatedMarker;
2383                if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
2384                        $this->setNewtalk( false );
2385                        return;
2386                }
2387                if( $currentUser != 0 )  {
2388                        $dbw = wfGetDB( DB_MASTER );
2389                        $dbw->update( 'watchlist',
2390                                array( /* SET */
2391                                        'wl_notificationtimestamp' => null
2392                                ), array( /* WHERE */
2393                                        'wl_user' => $currentUser
2394                                ), __METHOD__
2395                        );
2396                #       We also need to clear here the "you have new message" notification for the own user_talk page
2397                #       This is cleared one page view later in Article::viewUpdates();
2398                }
2399        }
2400
2401        /**
2402         * Set this user's options from an encoded string
2403         * @param $str \string Encoded options to import
2404         * @private
2405         */
2406        function decodeOptions( $str ) {
2407                if( !$str )
2408                        return;
2409
2410                $this->mOptionsLoaded = true;
2411                $this->mOptionOverrides = array();
2412
2413                $this->mOptions = array();
2414                $a = explode( "\n", $str );
2415                foreach ( $a as $s ) {
2416                        $m = array();
2417                        if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
2418                                $this->mOptions[$m[1]] = $m[2];
2419                                $this->mOptionOverrides[$m[1]] = $m[2];
2420                        }
2421                }
2422        }
2423
2424        /**
2425         * Set a cookie on the user's client. Wrapper for
2426         * WebResponse::setCookie
2427         * @param $name \string Name of the cookie to set
2428         * @param $value \string Value to set
2429         * @param $exp \int Expiration time, as a UNIX time value;
2430         *                   if 0 or not specified, use the default $wgCookieExpiration
2431         */
2432        protected function setCookie( $name, $value, $exp = 0 ) {
2433                global $wgRequest;
2434                $wgRequest->response()->setcookie( $name, $value, $exp );
2435        }
2436
2437        /**
2438         * Clear a cookie on the user's client
2439         * @param $name \string Name of the cookie to clear
2440         */
2441        protected function clearCookie( $name ) {
2442                $this->setCookie( $name, '', time() - 86400 );
2443        }
2444
2445        /**
2446         * Set the default cookies for this session on the user's client.
2447         */
2448        function setCookies() {
2449                $this->load();
2450                if ( 0 == $this->mId ) return;
2451                $session = array(
2452                        'wsUserID' => $this->mId,
2453                        'wsToken' => $this->mToken,
2454                        'wsUserName' => $this->getName()
2455                );
2456                $cookies = array(
2457                        'UserID' => $this->mId,
2458                        'UserName' => $this->getName(),
2459                );
2460                if ( 1 == $this->getOption( 'rememberpassword' ) ) {
2461                        $cookies['Token'] = $this->mToken;
2462                } else {
2463                        $cookies['Token'] = false;
2464                }
2465
2466                wfRunHooks( 'UserSetCookies', array( $this, &$session, &$cookies ) );
2467                #check for null, since the hook could cause a null value
2468                if ( !is_null( $session ) && isset( $_SESSION ) ){
2469                        $_SESSION = $session + $_SESSION;
2470                }
2471                foreach ( $cookies as $name => $value ) {
2472                        if ( $value === false ) {
2473                                $this->clearCookie( $name );
2474                        } else {
2475                                $this->setCookie( $name, $value );
2476                        }
2477                }
2478        }
2479
2480        /**
2481         * Log this user out.
2482         */
2483        function logout() {
2484                if( wfRunHooks( 'UserLogout', array( &$this ) ) ) {
2485                        $this->doLogout();
2486                }
2487        }
2488
2489        /**
2490         * Clear the user's cookies and session, and reset the instance cache.
2491         * @private
2492         * @see logout()
2493         */
2494        function doLogout() {
2495                $this->clearInstanceCache( 'defaults' );
2496
2497                $_SESSION['wsUserID'] = 0;
2498
2499                $this->clearCookie( 'UserID' );
2500                $this->clearCookie( 'Token' );
2501
2502                # Remember when user logged out, to prevent seeing cached pages
2503                $this->setCookie( 'LoggedOut', wfTimestampNow(), time() + 86400 );
2504        }
2505
2506        /**
2507         * Save this user's settings into the database.
2508         * @todo Only rarely do all these fields need to be set!
2509         */
2510        function saveSettings() {
2511                $this->load();
2512                if ( wfReadOnly() ) { return; }
2513                if ( 0 == $this->mId ) { return; }
2514
2515                $this->mTouched = self::newTouchedTimestamp();
2516
2517                $dbw = wfGetDB( DB_MASTER );
2518                $dbw->update( 'user',
2519                        array( /* SET */
2520                                'user_name' => $this->mName,
2521                                'user_password' => $this->mPassword,
2522                                'user_newpassword' => $this->mNewpassword,
2523                                'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ),
2524                                'user_real_name' => $this->mRealName,
2525                                'user_email' => $this->mEmail,
2526                                'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
2527                                'user_options' => '',
2528                                'user_touched' => $dbw->timestamp( $this->mTouched ),
2529                                'user_token' => $this->mToken,
2530                                'user_email_token' => $this->mEmailToken,
2531                                'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
2532                        ), array( /* WHERE */
2533                                'user_id' => $this->mId
2534                        ), __METHOD__
2535                );
2536
2537                $this->saveOptions();
2538
2539                wfRunHooks( 'UserSaveSettings', array( $this ) );
2540                $this->clearSharedCache();
2541                $this->getUserPage()->invalidateCache();
2542        }
2543
2544        /**
2545         * If only this user's username is known, and it exists, return the user ID.
2546         */
2547        function idForName() {
2548                $s = trim( $this->getName() );
2549                if ( $s === '' ) return 0;
2550
2551                $dbr = wfGetDB( DB_SLAVE );
2552                $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), __METHOD__ );
2553                if ( $id === false ) {
2554                        $id = 0;
2555                }
2556                return $id;
2557        }
2558
2559        /**
2560         * Add a user to the database, return the user object
2561         *
2562         * @param $name \string Username to add
2563         * @param $params \type{\arrayof{\string}} Non-default parameters to save to the database:
2564         *   - password             The user's password. Password logins will be disabled if this is omitted.
2565         *   - newpassword          A temporary password mailed to the user
2566         *   - email                The user's email address
2567         *   - email_authenticated  The email authentication timestamp
2568         *   - real_name            The user's real name
2569         *   - options              An associative array of non-default options
2570         *   - token                Random authentication token. Do not set.
2571         *   - registration         Registration timestamp. Do not set.
2572         *
2573         * @return \type{User} A new User object, or null if the username already exists
2574         */
2575        static function createNew( $name, $params = array() ) {
2576                $user = new User;
2577                $user->load();
2578                if ( isset( $params['options'] ) ) {
2579                        $user->mOptions = $params['options'] + (array)$user->mOptions;
2580                        unset( $params['options'] );
2581                }
2582                $dbw = wfGetDB( DB_MASTER );
2583                $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
2584                $fields = array(
2585                        'user_id' => $seqVal,
2586                        'user_name' => $name,
2587                        'user_password' => $user->mPassword,
2588                        'user_newpassword' => $user->mNewpassword,
2589                        'user_newpass_time' => $dbw->timestamp( $user->mNewpassTime ),
2590                        'user_email' => $user->mEmail,
2591                        'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ),
2592                        'user_real_name' => $user->mRealName,
2593                        'user_options' => '',
2594                        'user_token' => $user->mToken,
2595                        'user_registration' => $dbw->timestamp( $user->mRegistration ),
2596                        'user_editcount' => 0,
2597                );
2598                foreach ( $params as $name => $value ) {
2599                        $fields["user_$name"] = $value;
2600                }
2601                $dbw->insert( 'user', $fields, __METHOD__, array( 'IGNORE' ) );
2602                if ( $dbw->affectedRows() ) {
2603                        $newUser = User::newFromId( $dbw->insertId() );
2604                } else {
2605                        $newUser = null;
2606                }
2607                return $newUser;
2608        }
2609
2610        /**
2611         * Add this existing user object to the database
2612         */
2613        function addToDatabase() {
2614                $this->load();
2615                $dbw = wfGetDB( DB_MASTER );
2616                $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
2617                $dbw->insert( 'user',
2618                        array(
2619                                'user_id' => $seqVal,
2620                                'user_name' => $this->mName,
2621                                'user_password' => $this->mPassword,
2622                                'user_newpassword' => $this->mNewpassword,
2623                                'user_newpass_time' => $dbw->timestamp( $this->mNewpassTime ),
2624                                'user_email' => $this->mEmail,
2625                                'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
2626                                'user_real_name' => $this->mRealName,
2627                                'user_options' => '',
2628                                'user_token' => $this->mToken,
2629                                'user_registration' => $dbw->timestamp( $this->mRegistration ),
2630                                'user_editcount' => 0,
2631                        ), __METHOD__
2632                );
2633                $this->mId = $dbw->insertId();
2634
2635                // Clear instance cache other than user table data, which is already accurate
2636                $this->clearInstanceCache();
2637
2638                $this->saveOptions();
2639        }
2640
2641        /**
2642         * If this (non-anonymous) user is blocked, block any IP address
2643         * they've successfully logged in from.
2644         */
2645        function spreadBlock() {
2646                wfDebug( __METHOD__ . "()\n" );
2647                $this->load();
2648                if ( $this->mId == 0 ) {
2649                        return;
2650                }
2651
2652                $userblock = Block::newFromDB( '', $this->mId );
2653                if ( !$userblock ) {
2654                        return;
2655                }
2656
2657                $userblock->doAutoblock( wfGetIP() );
2658        }
2659
2660        /**
2661         * Generate a string which will be different for any combination of
2662         * user options which would produce different parser output.
2663         * This will be used as part of the hash key for the parser cache,
2664         * so users with the same options can share the same cached data
2665         * safely.
2666         *
2667         * Extensions which require it should install 'PageRenderingHash' hook,
2668         * which will give them a chance to modify this key based on their own
2669         * settings.
2670         *
2671         * @return \string Page rendering hash
2672         */
2673        function getPageRenderingHash() {
2674                global $wgUseDynamicDates, $wgRenderHashAppend, $wgLang, $wgContLang;
2675                if( $this->mHash ){
2676                        return $this->mHash;
2677                }
2678
2679                // stubthreshold is only included below for completeness,
2680                // it will always be 0 when this function is called by parsercache.
2681
2682                $confstr =        $this->getOption( 'math' );
2683                $confstr .= '!' . $this->getOption( 'stubthreshold' );
2684                if ( $wgUseDynamicDates ) {
2685                        $confstr .= '!' . $this->getDatePreference();
2686                }
2687                $confstr .= '!' . ( $this->getOption( 'numberheadings' ) ? '1' : '' );
2688                $confstr .= '!' . $wgLang->getCode();
2689                $confstr .= '!' . $this->getOption( 'thumbsize' );
2690                // add in language specific options, if any
2691                $extra = $wgContLang->getExtraHashOptions();
2692                $confstr .= $extra;
2693
2694                $confstr .= $wgRenderHashAppend;
2695
2696                // Give a chance for extensions to modify the hash, if they have
2697                // extra options or other effects on the parser cache.
2698                wfRunHooks( 'PageRenderingHash', array( &$confstr ) );
2699
2700                // Make it a valid memcached key fragment
2701                $confstr = str_replace( ' ', '_', $confstr );
2702                $this->mHash = $confstr;
2703                return $confstr;
2704        }
2705
2706        /**
2707         * Get whether the user is explicitly blocked from account creation.
2708         * @return \bool True if blocked
2709         */
2710        function isBlockedFromCreateAccount() {
2711                $this->getBlockedStatus();
2712                return $this->mBlock && $this->mBlock->mCreateAccount;
2713        }
2714
2715        /**
2716         * Get whether the user is blocked from using Special:Emailuser.
2717         * @return \bool True if blocked
2718         */
2719        function isBlockedFromEmailuser() {
2720                $this->getBlockedStatus();
2721                return $this->mBlock && $this->mBlock->mBlockEmail;
2722        }
2723
2724        /**
2725         * Get whether the user is allowed to create an account.
2726         * @return \bool True if allowed
2727         */
2728        function isAllowedToCreateAccount() {
2729                return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount();
2730        }
2731
2732        /**
2733         * @deprecated
2734         */
2735        function setLoaded( $loaded ) {
2736                wfDeprecated( __METHOD__ );
2737        }
2738
2739        /**
2740         * Get this user's personal page title.
2741         *
2742         * @return \type{Title} User's personal page title
2743         */
2744        function getUserPage() {
2745                return Title::makeTitle( NS_USER, $this->getName() );
2746        }
2747
2748        /**
2749         * Get this user's talk page title.
2750         *
2751         * @return \type{Title} User's talk page title
2752         */
2753        function getTalkPage() {
2754                $title = $this->getUserPage();
2755                return $title->getTalkPage();
2756        }
2757
2758        /**
2759         * Get the maximum valid user ID.
2760         * @return \int User ID
2761         * @static
2762         */
2763        function getMaxID() {
2764                static $res; // cache
2765
2766                if ( isset( $res ) )
2767                        return $res;
2768                else {
2769                        $dbr = wfGetDB( DB_SLAVE );
2770                        return $res = $dbr->selectField( 'user', 'max(user_id)', false, 'User::getMaxID' );
2771                }
2772        }
2773
2774        /**
2775         * Determine whether the user is a newbie. Newbies are either
2776         * anonymous IPs, or the most recently created accounts.
2777         * @return \bool True if the user is a newbie
2778         */
2779        function isNewbie() {
2780                return !$this->isAllowed( 'autoconfirmed' );
2781        }
2782
2783        /**
2784         * Check to see if the given clear-text password is one of the accepted passwords
2785         * @param $password \string user password.
2786         * @return \bool True if the given password is correct, otherwise False.
2787         */
2788        function checkPassword( $password ) {
2789                global $wgAuth;
2790                $this->load();
2791
2792                // Even though we stop people from creating passwords that
2793                // are shorter than this, doesn't mean people wont be able
2794                // to. Certain authentication plugins do NOT want to save
2795                // domain passwords in a mysql database, so we should
2796                // check this (incase $wgAuth->strict() is false).
2797                if( !$this->isValidPassword( $password ) ) {
2798                        return false;
2799                }
2800
2801                if( $wgAuth->authenticate( $this->getName(), $password ) ) {
2802                        return true;
2803                } elseif( $wgAuth->strict() ) {
2804                        /* Auth plugin doesn't allow local authentication */
2805                        return false;
2806                } elseif( $wgAuth->strictUserAuth( $this->getName() ) ) {
2807                        /* Auth plugin doesn't allow local authentication for this user name */
2808                        return false;
2809                }
2810                if ( self::comparePasswords( $this->mPassword, $password, $this->mId ) ) {
2811                        return true;
2812                } elseif ( function_exists( 'iconv' ) ) {
2813                        # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
2814                        # Check for this with iconv
2815                        $cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password );
2816                        if ( self::comparePasswords( $this->mPassword, $cp1252Password, $this->mId ) ) {
2817                                return true;
2818                        }
2819                }
2820                return false;
2821        }
2822
2823        /**
2824         * Check if the given clear-text password matches the temporary password
2825         * sent by e-mail for password reset operations.
2826         * @return \bool True if matches, false otherwise
2827         */
2828        function checkTemporaryPassword( $plaintext ) {
2829                global $wgNewPasswordExpiry;
2830                if( self::comparePasswords( $this->mNewpassword, $plaintext, $this->getId() ) ) {
2831                        $this->load();
2832                        $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgNewPasswordExpiry;
2833                        return ( time() < $expiry );
2834                } else {
2835                        return false;
2836                }
2837        }
2838
2839        /**
2840         * Initialize (if necessary) and return a session token value
2841         * which can be used in edit forms to show that the user's
2842         * login credentials aren't being hijacked with a foreign form
2843         * submission.
2844         *
2845         * @param $salt \types{\string,\arrayof{\string}} Optional function-specific data for hashing
2846         * @return \string The new edit token
2847         */
2848        function editToken( $salt = '' ) {
2849                if ( $this->isAnon() ) {
2850                        return EDIT_TOKEN_SUFFIX;
2851                } else {
2852                        if( !isset( $_SESSION['wsEditToken'] ) ) {
2853                                $token = self::generateToken();
2854                                $_SESSION['wsEditToken'] = $token;
2855                        } else {
2856                                $token = $_SESSION['wsEditToken'];
2857                        }
2858                        if( is_array( $salt ) ) {
2859                                $salt = implode( '|', $salt );
2860                        }
2861                        return md5( $token . $salt ) . EDIT_TOKEN_SUFFIX;
2862                }
2863        }
2864
2865        /**
2866         * Generate a looking random token for various uses.
2867         *
2868         * @param $salt \string Optional salt value
2869         * @return \string The new random token
2870         */
2871        public static function generateToken( $salt = '' ) {
2872                $token = dechex( mt_rand() ) . dechex( mt_rand() );
2873                return md5( $token . $salt );
2874        }
2875
2876        /**
2877         * Check given value against the token value stored in the session.
2878         * A match should confirm that the form was submitted from the
2879         * user's own login session, not a form submission from a third-party
2880         * site.
2881         *
2882         * @param $val \string Input value to compare
2883         * @param $salt \string Optional function-specific data for hashing
2884         * @return \bool Whether the token matches
2885         */
2886        function matchEditToken( $val, $salt = '' ) {
2887                $sessionToken = $this->editToken( $salt );
2888                if ( $val != $sessionToken ) {
2889                        wfDebug( "User::matchEditToken: broken session data\n" );
2890                }
2891                return $val == $sessionToken;
2892        }
2893
2894        /**
2895         * Check given value against the token value stored in the session,
2896         * ignoring the suffix.
2897         *
2898         * @param $val \string Input value to compare
2899         * @param $salt \string Optional function-specific data for hashing
2900         * @return \bool Whether the token matches
2901         */
2902        function matchEditTokenNoSuffix( $val, $salt = '' ) {
2903                $sessionToken = $this->editToken( $salt );
2904                return substr( $sessionToken, 0, 32 ) == substr( $val, 0, 32 );
2905        }
2906
2907        /**
2908         * Generate a new e-mail confirmation token and send a confirmation/invalidation
2909         * mail to the user's given address.
2910         *
2911         * @return \types{\bool,\type{WikiError}} True on success, a WikiError object on failure.
2912         */
2913        function sendConfirmationMail() {
2914                global $wgLang;
2915                $expiration = null; // gets passed-by-ref and defined in next line.
2916                $token = $this->confirmationToken( $expiration );
2917                $url = $this->confirmationTokenUrl( $token );
2918                $invalidateURL = $this->invalidationTokenUrl( $token );
2919                $this->saveSettings();
2920
2921                return $this->sendMail( wfMsg( 'confirmemail_subject' ),
2922                        wfMsg( 'confirmemail_body',
2923                                wfGetIP(),
2924                                $this->getName(),
2925                                $url,
2926                                $wgLang->timeanddate( $expiration, false ),
2927                                $invalidateURL,
2928                                $wgLang->date( $expiration, false ),
2929                                $wgLang->time( $expiration, false ) ) );
2930        }
2931
2932        /**
2933         * Send an e-mail to this user's account. Does not check for
2934         * confirmed status or validity.
2935         *
2936         * @param $subject \string Message subject
2937         * @param $body \string Message body
2938         * @param $from \string Optional From address; if unspecified, default $wgPasswordSender will be used
2939         * @param $replyto \string Reply-To address
2940         * @return \types{\bool,\type{WikiError}} True on success, a WikiError object on failure
2941         */
2942        function sendMail( $subject, $body, $from = null, $replyto = null ) {
2943                if( is_null( $from ) ) {
2944                        global $wgPasswordSender;
2945                        $from = $wgPasswordSender;
2946                }
2947
2948                $to = new MailAddress( $this );
2949                $sender = new MailAddress( $from );
2950                // a hack
2951                return UserMailer::send( $to, $sender, $subject, $body, $replyto,'text/html;charset=utf-8' );           
2952                //return UserMailer::send( $to, $sender, $subject, $body, $replyto );
2953        }
2954
2955        /**
2956         * Generate, store, and return a new e-mail confirmation code.
2957         * A hash (unsalted, since it's used as a key) is stored.
2958         *
2959         * @note Call saveSettings() after calling this function to commit
2960         * this change to the database.
2961         *
2962         * @param[out] &$expiration \mixed Accepts the expiration time
2963         * @return \string New token
2964         * @private
2965         */
2966        function confirmationToken( &$expiration ) {
2967                $now = time();
2968                $expires = $now + 7 * 24 * 60 * 60;
2969                $expiration = wfTimestamp( TS_MW, $expires );
2970                $token = self::generateToken( $this->mId . $this->mEmail . $expires );
2971                $hash = md5( $token );
2972                $this->load();
2973                $this->mEmailToken = $hash;
2974                $this->mEmailTokenExpires = $expiration;
2975                return $token;
2976        }
2977
2978        /**
2979        * Return a URL the user can use to confirm their email address.
2980         * @param $token \string Accepts the email confirmation token
2981         * @return \string New token URL
2982         * @private
2983         */
2984        function confirmationTokenUrl( $token ) {
2985                return $this->getTokenUrl( 'ConfirmEmail', $token );
2986        }
2987
2988        /**
2989         * Return a URL the user can use to invalidate their email address.
2990         * @param $token \string Accepts the email confirmation token
2991         * @return \string New token URL
2992         * @private
2993         */
2994        function invalidationTokenUrl( $token ) {
2995                return $this->getTokenUrl( 'Invalidateemail', $token );
2996        }
2997
2998        /**
2999         * Internal function to format the e-mail validation/invalidation URLs.
3000         * This uses $wgArticlePath directly as a quickie hack to use the
3001         * hardcoded English names of the Special: pages, for ASCII safety.
3002         *
3003         * @note Since these URLs get dropped directly into emails, using the
3004         * short English names avoids insanely long URL-encoded links, which
3005         * also sometimes can get corrupted in some browsers/mailers
3006         * (bug 6957 with Gmail and Internet Explorer).
3007         *
3008         * @param $page \string Special page
3009         * @param $token \string Token
3010         * @return \string Formatted URL
3011         */
3012        protected function getTokenUrl( $page, $token ) {
3013                global $wgArticlePath;
3014                return wfExpandUrl(
3015                        str_replace(
3016                                '$1',
3017                                "Special:$page/$token",
3018                                $wgArticlePath ) );
3019        }
3020
3021        /**
3022         * Mark the e-mail address confirmed.
3023         *
3024         * @note Call saveSettings() after calling this function to commit the change.
3025         */
3026        function confirmEmail() {
3027                $this->setEmailAuthenticationTimestamp( wfTimestampNow() );
3028                wfRunHooks( 'ConfirmEmailComplete', array( $this ) );
3029                return true;
3030        }
3031
3032        /**
3033         * Invalidate the user's e-mail confirmation, and unauthenticate the e-mail
3034         * address if it was already confirmed.
3035         *
3036         * @note Call saveSettings() after calling this function to commit the change.
3037         */
3038        function invalidateEmail() {
3039                $this->load();
3040                $this->mEmailToken = null;
3041                $this->mEmailTokenExpires = null;
3042                $this->setEmailAuthenticationTimestamp( null );
3043                wfRunHooks( 'InvalidateEmailComplete', array( $this ) );
3044                return true;
3045        }
3046
3047        /**
3048         * Set the e-mail authentication timestamp.
3049         * @param $timestamp \string TS_MW timestamp
3050         */
3051        function setEmailAuthenticationTimestamp( $timestamp ) {
3052                $this->load();
3053                $this->mEmailAuthenticated = $timestamp;
3054                wfRunHooks( 'UserSetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
3055        }
3056
3057        /**
3058         * Is this user allowed to send e-mails within limits of current
3059         * site configuration?
3060         * @return \bool True if allowed
3061         */
3062        function canSendEmail() {
3063                global $wgEnableEmail, $wgEnableUserEmail;
3064                if( !$wgEnableEmail || !$wgEnableUserEmail || !$this->isAllowed( 'sendemail' ) ) {
3065                        return false;
3066                }
3067                $canSend = $this->isEmailConfirmed();
3068                wfRunHooks( 'UserCanSendEmail', array( &$this, &$canSend ) );
3069                return $canSend;
3070        }
3071
3072        /**
3073         * Is this user allowed to receive e-mails within limits of current
3074         * site configuration?
3075         * @return \bool True if allowed
3076         */
3077        function canReceiveEmail() {
3078                return $this->isEmailConfirmed() && !$this->getOption( 'disablemail' );
3079        }
3080
3081        /**
3082         * Is this user's e-mail address valid-looking and confirmed within
3083         * limits of the current site configuration?
3084         *
3085         * @note If $wgEmailAuthentication is on, this may require the user to have
3086         * confirmed their address by returning a code or using a password
3087         * sent to the address from the wiki.
3088         *
3089         * @return \bool True if confirmed
3090         */
3091        function isEmailConfirmed() {
3092                global $wgEmailAuthentication;
3093                $this->load();
3094                $confirmed = true;
3095                if( wfRunHooks( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) {
3096                        if( $this->isAnon() )
3097                                return false;
3098                        if( !self::isValidEmailAddr( $this->mEmail ) )
3099                                return false;
3100                        if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() )
3101                                return false;
3102                        return true;
3103                } else {
3104                        return $confirmed;
3105                }
3106        }
3107
3108        /**
3109         * Check whether there is an outstanding request for e-mail confirmation.
3110         * @return \bool True if pending
3111         */
3112        function isEmailConfirmationPending() {
3113                global $wgEmailAuthentication;
3114                return $wgEmailAuthentication &&
3115                        !$this->isEmailConfirmed() &&
3116                        $this->mEmailToken &&
3117                        $this->mEmailTokenExpires > wfTimestamp();
3118        }
3119
3120        /**
3121         * Get the timestamp of account creation.
3122         *
3123         * @return \types{\string,\bool} string Timestamp of account creation, or false for
3124         *                                non-existent/anonymous user accounts.
3125         */
3126        public function getRegistration() {
3127                return $this->getId() > 0
3128                        ? $this->mRegistration
3129                        : false;
3130        }
3131
3132        /**
3133         * Get the timestamp of the first edit
3134         *
3135         * @return \types{\string,\bool} string Timestamp of first edit, or false for
3136         *                                non-existent/anonymous user accounts.
3137         */
3138        public function getFirstEditTimestamp() {
3139                if( $this->getId() == 0 ) return false; // anons
3140                $dbr = wfGetDB( DB_SLAVE );
3141                $time = $dbr->selectField( 'revision', 'rev_timestamp',
3142                        array( 'rev_user' => $this->getId() ),
3143                        __METHOD__,
3144                        array( 'ORDER BY' => 'rev_timestamp ASC' )
3145                );
3146                if( !$time ) return false; // no edits
3147                return wfTimestamp( TS_MW, $time );
3148        }
3149
3150        /**
3151         * Get the permissions associated with a given list of groups
3152         *
3153         * @param $groups \type{\arrayof{\string}} List of internal group names
3154         * @return \type{\arrayof{\string}} List of permission key names for given groups combined
3155         */
3156        static function getGroupPermissions( $groups ) {
3157                global $wgGroupPermissions, $wgRevokePermissions;
3158                $rights = array();
3159                // grant every granted permission first
3160                foreach( $groups as $group ) {
3161                        if( isset( $wgGroupPermissions[$group] ) ) {
3162                                $rights = array_merge( $rights,
3163                                        // array_filter removes empty items
3164                                        array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
3165                        }
3166                }
3167                // now revoke the revoked permissions
3168                foreach( $groups as $group ) {
3169                        if( isset( $wgRevokePermissions[$group] ) ) {
3170                                $rights = array_diff( $rights,
3171                                        array_keys( array_filter( $wgRevokePermissions[$group] ) ) );
3172                        }
3173                }
3174                return array_unique( $rights );
3175        }
3176
3177        /**
3178         * Get all the groups who have a given permission
3179         *
3180         * @param $role \string Role to check
3181         * @return \type{\arrayof{\string}} List of internal group names with the given permission
3182         */
3183        static function getGroupsWithPermission( $role ) {
3184                global $wgGroupPermissions;
3185                $allowedGroups = array();
3186                foreach ( $wgGroupPermissions as $group => $rights ) {
3187                        if ( isset( $rights[$role] ) && $rights[$role] ) {
3188                                $allowedGroups[] = $group;
3189                        }
3190                }
3191                return $allowedGroups;
3192        }
3193
3194        /**
3195         * Get the localized descriptive name for a group, if it exists
3196         *
3197         * @param $group \string Internal group name
3198         * @return \string Localized descriptive group name
3199         */
3200        static function getGroupName( $group ) {
3201                global $wgMessageCache;
3202                $wgMessageCache->loadAllMessages();
3203                $key = "group-$group";
3204                $name = wfMsg( $key );
3205                return $name == '' || wfEmptyMsg( $key, $name )
3206                        ? $group
3207                        : $name;
3208        }
3209
3210        /**
3211         * Get the localized descriptive name for a member of a group, if it exists
3212         *
3213         * @param $group \string Internal group name
3214         * @return \string Localized name for group member
3215         */
3216        static function getGroupMember( $group ) {
3217                global $wgMessageCache;
3218                $wgMessageCache->loadAllMessages();
3219                $key = "group-$group-member";
3220                $name = wfMsg( $key );
3221                return $name == '' || wfEmptyMsg( $key, $name )
3222                        ? $group
3223                        : $name;
3224        }
3225
3226        /**
3227         * Return the set of defined explicit groups.
3228         * The implicit groups (by default *, 'user' and 'autoconfirmed')
3229         * are not included, as they are defined automatically, not in the database.
3230         * @return \type{\arrayof{\string}} Array of internal group names
3231         */
3232        static function getAllGroups() {
3233                global $wgGroupPermissions, $wgRevokePermissions;
3234                return array_diff(
3235                        array_merge( array_keys( $wgGroupPermissions ), array_keys( $wgRevokePermissions ) ),
3236                        self::getImplicitGroups()
3237                );
3238        }
3239
3240        /**
3241         * Get a list of all available permissions.
3242         * @return \type{\arrayof{\string}} Array of permission names
3243         */
3244        static function getAllRights() {
3245                if ( self::$mAllRights === false ) {
3246                        global $wgAvailableRights;
3247                        if ( count( $wgAvailableRights ) ) {
3248                                self::$mAllRights = array_unique( array_merge( self::$mCoreRights, $wgAvailableRights ) );
3249                        } else {
3250                                self::$mAllRights = self::$mCoreRights;
3251                        }
3252                        wfRunHooks( 'UserGetAllRights', array( &self::$mAllRights ) );
3253                }
3254                return self::$mAllRights;
3255        }
3256
3257        /**
3258         * Get a list of implicit groups
3259         * @return \type{\arrayof{\string}} Array of internal group names
3260         */
3261        public static function getImplicitGroups() {
3262                global $wgImplicitGroups;
3263                $groups = $wgImplicitGroups;
3264                wfRunHooks( 'UserGetImplicitGroups', array( &$groups ) );       #deprecated, use $wgImplictGroups instead
3265                return $groups;
3266        }
3267
3268        /**
3269         * Get the title of a page describing a particular group
3270         *
3271         * @param $group \string Internal group name
3272         * @return \types{\type{Title},\bool} Title of the page if it exists, false otherwise
3273         */
3274        static function getGroupPage( $group ) {
3275                global $wgMessageCache;
3276                $wgMessageCache->loadAllMessages();
3277                $page = wfMsgForContent( 'grouppage-' . $group );
3278                if( !wfEmptyMsg( 'grouppage-' . $group, $page ) ) {
3279                        $title = Title::newFromText( $page );
3280                        if( is_object( $title ) )
3281                                return $title;
3282                }
3283                return false;
3284        }
3285
3286        /**
3287         * Create a link to the group in HTML, if available;
3288         * else return the group name.
3289         *
3290         * @param $group \string Internal name of the group
3291         * @param $text \string The text of the link
3292         * @return \string HTML link to the group
3293         */
3294        static function makeGroupLinkHTML( $group, $text = '' ) {
3295                if( $text == '' ) {
3296                        $text = self::getGroupName( $group );
3297                }
3298                $title = self::getGroupPage( $group );
3299                if( $title ) {
3300                        global $wgUser;
3301                        $sk = $wgUser->getSkin();
3302                        return $sk->link( $title, htmlspecialchars( $text ) );
3303                } else {
3304                        return $text;
3305                }
3306        }
3307
3308        /**
3309         * Create a link to the group in Wikitext, if available;
3310         * else return the group name.
3311         *
3312         * @param $group \string Internal name of the group
3313         * @param $text \string The text of the link
3314         * @return \string Wikilink to the group
3315         */
3316        static function makeGroupLinkWiki( $group, $text = '' ) {
3317                if( $text == '' ) {
3318                        $text = self::getGroupName( $group );
3319                }
3320                $title = self::getGroupPage( $group );
3321                if( $title ) {
3322                        $page = $title->getPrefixedText();
3323                        return "[[$page|$text]]";
3324                } else {
3325                        return $text;
3326                }
3327        }
3328
3329        /**
3330         * Returns an array of the groups that a particular group can add/remove.
3331         *
3332         * @param $group String: the group to check for whether it can add/remove
3333         * @return Array array( 'add' => array( addablegroups ),
3334         *  'remove' => array( removablegroups ),
3335         *  'add-self' => array( addablegroups to self),
3336         *  'remove-self' => array( removable groups from self) )
3337         */
3338        static function changeableByGroup( $group ) {
3339                global $wgAddGroups, $wgRemoveGroups, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
3340
3341                $groups = array( 'add' => array(), 'remove' => array(), 'add-self' => array(), 'remove-self' => array() );
3342                if( empty( $wgAddGroups[$group] ) ) {
3343                        // Don't add anything to $groups
3344                } elseif( $wgAddGroups[$group] === true ) {
3345                        // You get everything
3346                        $groups['add'] = self::getAllGroups();
3347                } elseif( is_array( $wgAddGroups[$group] ) ) {
3348                        $groups['add'] = $wgAddGroups[$group];
3349                }
3350
3351                // Same thing for remove
3352                if( empty( $wgRemoveGroups[$group] ) ) {
3353                } elseif( $wgRemoveGroups[$group] === true ) {
3354                        $groups['remove'] = self::getAllGroups();
3355                } elseif( is_array( $wgRemoveGroups[$group] ) ) {
3356                        $groups['remove'] = $wgRemoveGroups[$group];
3357                }
3358
3359                // Re-map numeric keys of AddToSelf/RemoveFromSelf to the 'user' key for backwards compatibility
3360                if( empty( $wgGroupsAddToSelf['user']) || $wgGroupsAddToSelf['user'] !== true ) {
3361                        foreach( $wgGroupsAddToSelf as $key => $value ) {
3362                                if( is_int( $key ) ) {
3363                                        $wgGroupsAddToSelf['user'][] = $value;
3364                                }
3365                        }
3366                }
3367
3368                if( empty( $wgGroupsRemoveFromSelf['user']) || $wgGroupsRemoveFromSelf['user'] !== true ) {
3369                        foreach( $wgGroupsRemoveFromSelf as $key => $value ) {
3370                                if( is_int( $key ) ) {
3371                                        $wgGroupsRemoveFromSelf['user'][] = $value;
3372                                }
3373                        }
3374                }
3375
3376                // Now figure out what groups the user can add to him/herself
3377                if( empty( $wgGroupsAddToSelf[$group] ) ) {
3378                } elseif( $wgGroupsAddToSelf[$group] === true ) {
3379                        // No idea WHY this would be used, but it's there
3380                        $groups['add-self'] = User::getAllGroups();
3381                } elseif( is_array( $wgGroupsAddToSelf[$group] ) ) {
3382                        $groups['add-self'] = $wgGroupsAddToSelf[$group];
3383                }
3384
3385                if( empty( $wgGroupsRemoveFromSelf[$group] ) ) {
3386                } elseif( $wgGroupsRemoveFromSelf[$group] === true ) {
3387                        $groups['remove-self'] = User::getAllGroups();
3388                } elseif( is_array( $wgGroupsRemoveFromSelf[$group] ) ) {
3389                        $groups['remove-self'] = $wgGroupsRemoveFromSelf[$group];
3390                }
3391
3392                return $groups;
3393        }
3394
3395        /**
3396         * Returns an array of groups that this user can add and remove
3397         * @return Array array( 'add' => array( addablegroups ),
3398         *  'remove' => array( removablegroups ),
3399         *  'add-self' => array( addablegroups to self),
3400         *  'remove-self' => array( removable groups from self) )
3401         */
3402        function changeableGroups() {
3403                if( $this->isAllowed( 'userrights' ) ) {
3404                        // This group gives the right to modify everything (reverse-
3405                        // compatibility with old "userrights lets you change
3406                        // everything")
3407                        // Using array_merge to make the groups reindexed
3408                        $all = array_merge( User::getAllGroups() );
3409                        return array(
3410                                'add' => $all,
3411                                'remove' => $all,
3412                                'add-self' => array(),
3413                                'remove-self' => array()
3414                        );
3415                }
3416
3417                // Okay, it's not so simple, we will have to go through the arrays
3418                $groups = array(
3419                        'add' => array(),
3420                        'remove' => array(),
3421                        'add-self' => array(),
3422                        'remove-self' => array()
3423                );
3424                $addergroups = $this->getEffectiveGroups();
3425
3426                foreach( $addergroups as $addergroup ) {
3427                        $groups = array_merge_recursive(
3428                                $groups, $this->changeableByGroup( $addergroup )
3429                        );
3430                        $groups['add']    = array_unique( $groups['add'] );
3431                        $groups['remove'] = array_unique( $groups['remove'] );
3432                        $groups['add-self'] = array_unique( $groups['add-self'] );
3433                        $groups['remove-self'] = array_unique( $groups['remove-self'] );
3434                }
3435                return $groups;
3436        }
3437
3438        /**
3439         * Increment the user's edit-count field.
3440         * Will have no effect for anonymous users.
3441         */
3442        function incEditCount() {
3443                if( !$this->isAnon() ) {
3444                        $dbw = wfGetDB( DB_MASTER );
3445                        $dbw->update( 'user',
3446                                array( 'user_editcount=user_editcount+1' ),
3447                                array( 'user_id' => $this->getId() ),
3448                                __METHOD__ );
3449
3450                        // Lazy initialization check...
3451                        if( $dbw->affectedRows() == 0 ) {
3452                                // Pull from a slave to be less cruel to servers
3453                                // Accuracy isn't the point anyway here
3454                                $dbr = wfGetDB( DB_SLAVE );
3455                                $count = $dbr->selectField( 'revision',
3456                                        'COUNT(rev_user)',
3457                                        array( 'rev_user' => $this->getId() ),
3458                                        __METHOD__ );
3459
3460                                // Now here's a goddamn hack...
3461                                if( $dbr !== $dbw ) {
3462                                        // If we actually have a slave server, the count is
3463                                        // at least one behind because the current transaction
3464                                        // has not been committed and replicated.
3465                                        $count++;
3466                                } else {
3467                                        // But if DB_SLAVE is selecting the master, then the
3468                                        // count we just read includes the revision that was
3469                                        // just added in the working transaction.
3470                                }
3471
3472                                $dbw->update( 'user',
3473                                        array( 'user_editcount' => $count ),
3474                                        array( 'user_id' => $this->getId() ),
3475                                        __METHOD__ );
3476                        }
3477                }
3478                // edit count in user cache too
3479                $this->invalidateCache();
3480        }
3481
3482        /**
3483         * Get the description of a given right
3484         *
3485         * @param $right \string Right to query
3486         * @return \string Localized description of the right
3487         */
3488        static function getRightDescription( $right ) {
3489                global $wgMessageCache;
3490                $wgMessageCache->loadAllMessages();
3491                $key = "right-$right";
3492                $name = wfMsg( $key );
3493                return $name == '' || wfEmptyMsg( $key, $name )
3494                        ? $right
3495                        : $name;
3496        }
3497
3498        /**
3499         * Make an old-style password hash
3500         *
3501         * @param $password \string Plain-text password
3502         * @param $userId \string User ID
3503         * @return \string Password hash
3504         */
3505        static function oldCrypt( $password, $userId ) {
3506                global $wgPasswordSalt;
3507                if ( $wgPasswordSalt ) {
3508                        return md5( $userId . '-' . md5( $password ) );
3509                } else {
3510                        return md5( $password );
3511                }
3512        }
3513
3514        /**
3515         * Make a new-style password hash
3516         *
3517         * @param $password \string Plain-text password
3518         * @param $salt \string Optional salt, may be random or the user ID.
3519         *                     If unspecified or false, will generate one automatically
3520         * @return \string Password hash
3521         */
3522        static function crypt( $password, $salt = false ) {
3523                global $wgPasswordSalt;
3524
3525                $hash = '';
3526                if( !wfRunHooks( 'UserCryptPassword', array( &$password, &$salt, &$wgPasswordSalt, &$hash ) ) ) {
3527                        return $hash;
3528                }
3529
3530                if( $wgPasswordSalt ) {
3531                        if ( $salt === false ) {
3532                                $salt = substr( wfGenerateToken(), 0, 8 );
3533                        }
3534                        return ':B:' . $salt . ':' . md5( $salt . '-' . md5( $password ) );
3535                } else {
3536                        return ':A:' . md5( $password );
3537                }
3538        }
3539
3540        /**
3541         * Compare a password hash with a plain-text password. Requires the user
3542         * ID if there's a chance that the hash is an old-style hash.
3543         *
3544         * @param $hash \string Password hash
3545         * @param $password \string Plain-text password to compare
3546         * @param $userId \string User ID for old-style password salt
3547         * @return \bool
3548         */
3549        static function comparePasswords( $hash, $password, $userId = false ) {
3550                $m = false;
3551                $type = substr( $hash, 0, 3 );
3552
3553                $result = false;
3554                if( !wfRunHooks( 'UserComparePasswords', array( &$hash, &$password, &$userId, &$result ) ) ) {
3555                        return $result;
3556                }
3557
3558                if ( $type == ':A:' ) {
3559                        # Unsalted
3560                        return md5( $password ) === substr( $hash, 3 );
3561                } elseif ( $type == ':B:' ) {
3562                        # Salted
3563                        list( $salt, $realHash ) = explode( ':', substr( $hash, 3 ), 2 );
3564                        return md5( $salt.'-'.md5( $password ) ) == $realHash;
3565                } else {
3566                        # Old-style
3567                        return self::oldCrypt( $password, $userId ) === $hash;
3568                }
3569        }
3570
3571        /**
3572         * Add a newuser log entry for this user
3573         * @param $byEmail Boolean: account made by email?
3574         */
3575        public function addNewUserLogEntry( $byEmail = false ) {
3576                global $wgUser, $wgNewUserLog;
3577                if( empty( $wgNewUserLog ) ) {
3578                        return true; // disabled
3579                }
3580
3581                if( $this->getName() == $wgUser->getName() ) {
3582                        $action = 'create';
3583                        $message = '';
3584                } else {
3585                        $action = 'create2';
3586                        $message = $byEmail
3587                                ? wfMsgForContent( 'newuserlog-byemail' )
3588                                : '';
3589                }
3590                $log = new LogPage( 'newusers' );
3591                $log->addEntry(
3592                        $action,
3593                        $this->getUserPage(),
3594                        $message,
3595                        array( $this->getId() )
3596                );
3597                return true;
3598        }
3599
3600        /**
3601         * Add an autocreate newuser log entry for this user
3602         * Used by things like CentralAuth and perhaps other authplugins.
3603         */
3604        public function addNewUserLogEntryAutoCreate() {
3605                global $wgNewUserLog;
3606                if( empty( $wgNewUserLog ) ) {
3607                        return true; // disabled
3608                }
3609                $log = new LogPage( 'newusers', false );
3610                $log->addEntry( 'autocreate', $this->getUserPage(), '', array( $this->getId() ) );
3611                return true;
3612        }
3613
3614        protected function loadOptions() {
3615                $this->load();
3616                if ( $this->mOptionsLoaded || !$this->getId() )
3617                        return;
3618
3619                $this->mOptions = self::getDefaultOptions();
3620
3621                // Maybe load from the object
3622                if ( !is_null( $this->mOptionOverrides ) ) {
3623                        wfDebug( "Loading options for user " . $this->getId() . " from override cache.\n" );
3624                        foreach( $this->mOptionOverrides as $key => $value ) {
3625                                $this->mOptions[$key] = $value;
3626                        }
3627                } else {
3628                        wfDebug( "Loading options for user " . $this->getId() . " from database.\n" );
3629                        // Load from database
3630                        $dbr = wfGetDB( DB_SLAVE );
3631
3632                        $res = $dbr->select(
3633                                'user_properties',
3634                                '*',
3635                                array( 'up_user' => $this->getId() ),
3636                                __METHOD__
3637                        );
3638
3639                        while( $row = $dbr->fetchObject( $res ) ) {
3640                                $this->mOptionOverrides[$row->up_property] = $row->up_value;
3641                                $this->mOptions[$row->up_property] = $row->up_value;
3642                        }
3643                }
3644
3645                $this->mOptionsLoaded = true;
3646
3647                wfRunHooks( 'UserLoadOptions', array( $this, &$this->mOptions ) );
3648        }
3649
3650        protected function saveOptions() {
3651                global $wgAllowPrefChange;
3652
3653                $extuser = ExternalUser::newFromUser( $this );
3654
3655                $this->loadOptions();
3656                $dbw = wfGetDB( DB_MASTER );
3657
3658                $insert_rows = array();
3659
3660                $saveOptions = $this->mOptions;
3661
3662                // Allow hooks to abort, for instance to save to a global profile.
3663                // Reset options to default state before saving.
3664                if( !wfRunHooks( 'UserSaveOptions', array( $this, &$saveOptions ) ) )
3665                        return;
3666
3667                foreach( $saveOptions as $key => $value ) {
3668                        # Don't bother storing default values
3669                        if ( ( is_null( self::getDefaultOption( $key ) ) &&
3670                                        !( $value === false || is_null($value) ) ) ||
3671                                        $value != self::getDefaultOption( $key ) ) {
3672                                $insert_rows[] = array(
3673                                                'up_user' => $this->getId(),
3674                                                'up_property' => $key,
3675                                                'up_value' => $value,
3676                                        );
3677                        }
3678                        if ( $extuser && isset( $wgAllowPrefChange[$key] ) ) {
3679                                switch ( $wgAllowPrefChange[$key] ) {
3680                                        case 'local':
3681                                        case 'message':
3682                                                break;
3683                                        case 'semiglobal':
3684                                        case 'global':
3685                                                $extuser->setPref( $key, $value );
3686                                }
3687                        }
3688                }
3689
3690                $dbw->begin();
3691                $dbw->delete( 'user_properties', array( 'up_user' => $this->getId() ), __METHOD__ );
3692                $dbw->insert( 'user_properties', $insert_rows, __METHOD__ );
3693                $dbw->commit();
3694        }
3695
3696        /**
3697         * Provide an array of HTML5 attributes to put on an input element
3698         * intended for the user to enter a new password.  This may include
3699         * required, title, and/or pattern, depending on $wgMinimalPasswordLength.
3700         *
3701         * Do *not* use this when asking the user to enter his current password!
3702         * Regardless of configuration, users may have invalid passwords for whatever
3703         * reason (e.g., they were set before requirements were tightened up).
3704         * Only use it when asking for a new password, like on account creation or
3705         * ResetPass.
3706         *
3707         * Obviously, you still need to do server-side checking.
3708         *
3709         * @return array Array of HTML attributes suitable for feeding to
3710         *   Html::element(), directly or indirectly.  (Don't feed to Xml::*()!
3711         *   That will potentially output invalid XHTML 1.0 Transitional, and will
3712         *   get confused by the boolean attribute syntax used.)
3713         */
3714        public static function passwordChangeInputAttribs() {
3715                global $wgMinimalPasswordLength;
3716
3717                if ( $wgMinimalPasswordLength == 0 ) {
3718                        return array();
3719                }
3720
3721                # Note that the pattern requirement will always be satisfied if the
3722                # input is empty, so we need required in all cases.
3723                $ret = array( 'required' );
3724
3725                # We can't actually do this right now, because Opera 9.6 will print out
3726                # the entered password visibly in its error message!  When other
3727                # browsers add support for this attribute, or Opera fixes its support,
3728                # we can add support with a version check to avoid doing this on Opera
3729                # versions where it will be a problem.  Reported to Opera as
3730                # DSK-262266, but they don't have a public bug tracker for us to follow.
3731                /*
3732                if ( $wgMinimalPasswordLength > 1 ) {
3733                        $ret['pattern'] = '.{' . intval( $wgMinimalPasswordLength ) . ',}';
3734                        $ret['title'] = wfMsgExt( 'passwordtooshort', 'parsemag',
3735                                $wgMinimalPasswordLength );
3736                }
3737                */
3738
3739                return $ret;
3740        }
3741}
Note: See TracBrowser for help on using the repository browser.