Merge "Add scripts to generate update builds of OOjs and OOjs UI"
[lhc/web/wiklou.git] / includes / User.php
1 <?php
2 /**
3 * Implements the User class for the %MediaWiki software.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 */
22
23 /**
24 * Int Number of characters in user_token field.
25 * @ingroup Constants
26 */
27 define( 'USER_TOKEN_LENGTH', 32 );
28
29 /**
30 * Int Serialized record version.
31 * @ingroup Constants
32 */
33 define( 'MW_USER_VERSION', 9 );
34
35 /**
36 * String Some punctuation to prevent editing from broken text-mangling proxies.
37 * @ingroup Constants
38 */
39 define( 'EDIT_TOKEN_SUFFIX', '+\\' );
40
41 /**
42 * Thrown by User::setPassword() on error.
43 * @ingroup Exception
44 */
45 class PasswordError extends MWException {
46 // NOP
47 }
48
49 /**
50 * The User object encapsulates all of the user-specific settings (user_id,
51 * name, rights, password, email address, options, last login time). Client
52 * classes use the getXXX() functions to access these fields. These functions
53 * do all the work of determining whether the user is logged in,
54 * whether the requested option can be satisfied from cookies or
55 * whether a database query is needed. Most of the settings needed
56 * for rendering normal pages are set in the cookie to minimize use
57 * of the database.
58 */
59 class User {
60 /**
61 * Global constants made accessible as class constants so that autoloader
62 * magic can be used.
63 */
64 const USER_TOKEN_LENGTH = USER_TOKEN_LENGTH;
65 const MW_USER_VERSION = MW_USER_VERSION;
66 const EDIT_TOKEN_SUFFIX = EDIT_TOKEN_SUFFIX;
67
68 /**
69 * Maximum items in $mWatchedItems
70 */
71 const MAX_WATCHED_ITEMS_CACHE = 100;
72
73 /**
74 * Array of Strings List of member variables which are saved to the
75 * shared cache (memcached). Any operation which changes the
76 * corresponding database fields must call a cache-clearing function.
77 * @showinitializer
78 */
79 static $mCacheVars = array(
80 // user table
81 'mId',
82 'mName',
83 'mRealName',
84 'mPassword',
85 'mNewpassword',
86 'mNewpassTime',
87 'mEmail',
88 'mTouched',
89 'mToken',
90 'mEmailAuthenticated',
91 'mEmailToken',
92 'mEmailTokenExpires',
93 'mPasswordExpires',
94 'mRegistration',
95 'mEditCount',
96 // user_groups table
97 'mGroups',
98 // user_properties table
99 'mOptionOverrides',
100 );
101
102 /**
103 * Array of Strings Core rights.
104 * Each of these should have a corresponding message of the form
105 * "right-$right".
106 * @showinitializer
107 */
108 static $mCoreRights = array(
109 'apihighlimits',
110 'autoconfirmed',
111 'autopatrol',
112 'bigdelete',
113 'block',
114 'blockemail',
115 'bot',
116 'browsearchive',
117 'createaccount',
118 'createpage',
119 'createtalk',
120 'delete',
121 'deletedhistory',
122 'deletedtext',
123 'deletelogentry',
124 'deleterevision',
125 'edit',
126 'editinterface',
127 'editprotected',
128 'editmyoptions',
129 'editmyprivateinfo',
130 'editmyusercss',
131 'editmyuserjs',
132 'editmywatchlist',
133 'editsemiprotected',
134 'editusercssjs', #deprecated
135 'editusercss',
136 'edituserjs',
137 'hideuser',
138 'import',
139 'importupload',
140 'ipblock-exempt',
141 'markbotedits',
142 'mergehistory',
143 'minoredit',
144 'move',
145 'movefile',
146 'move-rootuserpages',
147 'move-subpages',
148 'nominornewtalk',
149 'noratelimit',
150 'override-export-depth',
151 'passwordreset',
152 'patrol',
153 'patrolmarks',
154 'protect',
155 'proxyunbannable',
156 'purge',
157 'read',
158 'reupload',
159 'reupload-own',
160 'reupload-shared',
161 'rollback',
162 'sendemail',
163 'siteadmin',
164 'suppressionlog',
165 'suppressredirect',
166 'suppressrevision',
167 'unblockself',
168 'undelete',
169 'unwatchedpages',
170 'upload',
171 'upload_by_url',
172 'userrights',
173 'userrights-interwiki',
174 'viewmyprivateinfo',
175 'viewmywatchlist',
176 'writeapi',
177 );
178 /**
179 * String Cached results of getAllRights()
180 */
181 static $mAllRights = false;
182
183 /** @name Cache variables */
184 //@{
185 var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime,
186 $mEmail, $mTouched, $mToken, $mEmailAuthenticated,
187 $mEmailToken, $mEmailTokenExpires, $mRegistration, $mEditCount,
188 $mGroups, $mOptionOverrides;
189
190 protected $mPasswordExpires;
191 //@}
192
193 /**
194 * Bool Whether the cache variables have been loaded.
195 */
196 //@{
197 var $mOptionsLoaded;
198
199 /**
200 * Array with already loaded items or true if all items have been loaded.
201 */
202 private $mLoadedItems = array();
203 //@}
204
205 /**
206 * String Initialization data source if mLoadedItems!==true. May be one of:
207 * - 'defaults' anonymous user initialised from class defaults
208 * - 'name' initialise from mName
209 * - 'id' initialise from mId
210 * - 'session' log in from cookies or session if possible
211 *
212 * Use the User::newFrom*() family of functions to set this.
213 */
214 var $mFrom;
215
216 /**
217 * Lazy-initialized variables, invalidated with clearInstanceCache
218 */
219 var $mNewtalk, $mDatePreference, $mBlockedby, $mHash, $mRights,
220 $mBlockreason, $mEffectiveGroups, $mImplicitGroups, $mFormerGroups, $mBlockedGlobally,
221 $mLocked, $mHideName, $mOptions;
222
223 /**
224 * @var WebRequest
225 */
226 private $mRequest;
227
228 /**
229 * @var Block
230 */
231 var $mBlock;
232
233 /**
234 * @var bool
235 */
236 var $mAllowUsertalk;
237
238 /**
239 * @var Block
240 */
241 private $mBlockedFromCreateAccount = false;
242
243 /**
244 * @var Array
245 */
246 private $mWatchedItems = array();
247
248 static $idCacheByName = array();
249
250 /**
251 * Lightweight constructor for an anonymous user.
252 * Use the User::newFrom* factory functions for other kinds of users.
253 *
254 * @see newFromName()
255 * @see newFromId()
256 * @see newFromConfirmationCode()
257 * @see newFromSession()
258 * @see newFromRow()
259 */
260 public function __construct() {
261 $this->clearInstanceCache( 'defaults' );
262 }
263
264 /**
265 * @return string
266 */
267 public function __toString() {
268 return $this->getName();
269 }
270
271 /**
272 * Load the user table data for this object from the source given by mFrom.
273 */
274 public function load() {
275 if ( $this->mLoadedItems === true ) {
276 return;
277 }
278 wfProfileIn( __METHOD__ );
279
280 // Set it now to avoid infinite recursion in accessors
281 $this->mLoadedItems = true;
282
283 switch ( $this->mFrom ) {
284 case 'defaults':
285 $this->loadDefaults();
286 break;
287 case 'name':
288 $this->mId = self::idFromName( $this->mName );
289 if ( !$this->mId ) {
290 // Nonexistent user placeholder object
291 $this->loadDefaults( $this->mName );
292 } else {
293 $this->loadFromId();
294 }
295 break;
296 case 'id':
297 $this->loadFromId();
298 break;
299 case 'session':
300 if ( !$this->loadFromSession() ) {
301 // Loading from session failed. Load defaults.
302 $this->loadDefaults();
303 }
304 wfRunHooks( 'UserLoadAfterLoadFromSession', array( $this ) );
305 break;
306 default:
307 wfProfileOut( __METHOD__ );
308 throw new MWException( "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" );
309 }
310 wfProfileOut( __METHOD__ );
311 }
312
313 /**
314 * Load user table data, given mId has already been set.
315 * @return bool false if the ID does not exist, true otherwise
316 */
317 public function loadFromId() {
318 global $wgMemc;
319 if ( $this->mId == 0 ) {
320 $this->loadDefaults();
321 return false;
322 }
323
324 // Try cache
325 $key = wfMemcKey( 'user', 'id', $this->mId );
326 $data = $wgMemc->get( $key );
327 if ( !is_array( $data ) || $data['mVersion'] < MW_USER_VERSION ) {
328 // Object is expired, load from DB
329 $data = false;
330 }
331
332 if ( !$data ) {
333 wfDebug( "User: cache miss for user {$this->mId}\n" );
334 // Load from DB
335 if ( !$this->loadFromDatabase() ) {
336 // Can't load from ID, user is anonymous
337 return false;
338 }
339 $this->saveToCache();
340 } else {
341 wfDebug( "User: got user {$this->mId} from cache\n" );
342 // Restore from cache
343 foreach ( self::$mCacheVars as $name ) {
344 $this->$name = $data[$name];
345 }
346 }
347
348 $this->mLoadedItems = true;
349
350 return true;
351 }
352
353 /**
354 * Save user data to the shared cache
355 */
356 public function saveToCache() {
357 $this->load();
358 $this->loadGroups();
359 $this->loadOptions();
360 if ( $this->isAnon() ) {
361 // Anonymous users are uncached
362 return;
363 }
364 $data = array();
365 foreach ( self::$mCacheVars as $name ) {
366 $data[$name] = $this->$name;
367 }
368 $data['mVersion'] = MW_USER_VERSION;
369 $key = wfMemcKey( 'user', 'id', $this->mId );
370 global $wgMemc;
371 $wgMemc->set( $key, $data );
372 }
373
374 /** @name newFrom*() static factory methods */
375 //@{
376
377 /**
378 * Static factory method for creation from username.
379 *
380 * This is slightly less efficient than newFromId(), so use newFromId() if
381 * you have both an ID and a name handy.
382 *
383 * @param string $name Username, validated by Title::newFromText()
384 * @param string|bool $validate Validate username. Takes the same parameters as
385 * User::getCanonicalName(), except that true is accepted as an alias
386 * for 'valid', for BC.
387 *
388 * @return User|bool User object, or false if the username is invalid
389 * (e.g. if it contains illegal characters or is an IP address). If the
390 * username is not present in the database, the result will be a user object
391 * with a name, zero user ID and default settings.
392 */
393 public static function newFromName( $name, $validate = 'valid' ) {
394 if ( $validate === true ) {
395 $validate = 'valid';
396 }
397 $name = self::getCanonicalName( $name, $validate );
398 if ( $name === false ) {
399 return false;
400 } else {
401 // Create unloaded user object
402 $u = new User;
403 $u->mName = $name;
404 $u->mFrom = 'name';
405 $u->setItemLoaded( 'name' );
406 return $u;
407 }
408 }
409
410 /**
411 * Static factory method for creation from a given user ID.
412 *
413 * @param int $id Valid user ID
414 * @return User The corresponding User object
415 */
416 public static function newFromId( $id ) {
417 $u = new User;
418 $u->mId = $id;
419 $u->mFrom = 'id';
420 $u->setItemLoaded( 'id' );
421 return $u;
422 }
423
424 /**
425 * Factory method to fetch whichever user has a given email confirmation code.
426 * This code is generated when an account is created or its e-mail address
427 * has changed.
428 *
429 * If the code is invalid or has expired, returns NULL.
430 *
431 * @param string $code Confirmation code
432 * @return User|null
433 */
434 public static function newFromConfirmationCode( $code ) {
435 $dbr = wfGetDB( DB_SLAVE );
436 $id = $dbr->selectField( 'user', 'user_id', array(
437 'user_email_token' => md5( $code ),
438 'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ),
439 ) );
440 if ( $id !== false ) {
441 return User::newFromId( $id );
442 } else {
443 return null;
444 }
445 }
446
447 /**
448 * Create a new user object using data from session or cookies. If the
449 * login credentials are invalid, the result is an anonymous user.
450 *
451 * @param WebRequest $request Object to use; $wgRequest will be used if omitted.
452 * @return User object
453 */
454 public static function newFromSession( WebRequest $request = null ) {
455 $user = new User;
456 $user->mFrom = 'session';
457 $user->mRequest = $request;
458 return $user;
459 }
460
461 /**
462 * Create a new user object from a user row.
463 * The row should have the following fields from the user table in it:
464 * - either user_name or user_id to load further data if needed (or both)
465 * - user_real_name
466 * - all other fields (email, password, etc.)
467 * It is useless to provide the remaining fields if either user_id,
468 * user_name and user_real_name are not provided because the whole row
469 * will be loaded once more from the database when accessing them.
470 *
471 * @param stdClass $row A row from the user table
472 * @param array $data Further data to load into the object (see User::loadFromRow for valid keys)
473 * @return User
474 */
475 public static function newFromRow( $row, $data = null ) {
476 $user = new User;
477 $user->loadFromRow( $row, $data );
478 return $user;
479 }
480
481 //@}
482
483 /**
484 * Get the username corresponding to a given user ID
485 * @param int $id User ID
486 * @return string|bool The corresponding username
487 */
488 public static function whoIs( $id ) {
489 return UserCache::singleton()->getProp( $id, 'name' );
490 }
491
492 /**
493 * Get the real name of a user given their user ID
494 *
495 * @param int $id User ID
496 * @return string|bool The corresponding user's real name
497 */
498 public static function whoIsReal( $id ) {
499 return UserCache::singleton()->getProp( $id, 'real_name' );
500 }
501
502 /**
503 * Get database id given a user name
504 * @param string $name Username
505 * @return int|null The corresponding user's ID, or null if user is nonexistent
506 */
507 public static function idFromName( $name ) {
508 $nt = Title::makeTitleSafe( NS_USER, $name );
509 if ( is_null( $nt ) ) {
510 // Illegal name
511 return null;
512 }
513
514 if ( isset( self::$idCacheByName[$name] ) ) {
515 return self::$idCacheByName[$name];
516 }
517
518 $dbr = wfGetDB( DB_SLAVE );
519 $s = $dbr->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), __METHOD__ );
520
521 if ( $s === false ) {
522 $result = null;
523 } else {
524 $result = $s->user_id;
525 }
526
527 self::$idCacheByName[$name] = $result;
528
529 if ( count( self::$idCacheByName ) > 1000 ) {
530 self::$idCacheByName = array();
531 }
532
533 return $result;
534 }
535
536 /**
537 * Reset the cache used in idFromName(). For use in tests.
538 */
539 public static function resetIdByNameCache() {
540 self::$idCacheByName = array();
541 }
542
543 /**
544 * Does the string match an anonymous IPv4 address?
545 *
546 * This function exists for username validation, in order to reject
547 * usernames which are similar in form to IP addresses. Strings such
548 * as 300.300.300.300 will return true because it looks like an IP
549 * address, despite not being strictly valid.
550 *
551 * We match "\d{1,3}\.\d{1,3}\.\d{1,3}\.xxx" as an anonymous IP
552 * address because the usemod software would "cloak" anonymous IP
553 * addresses like this, if we allowed accounts like this to be created
554 * new users could get the old edits of these anonymous users.
555 *
556 * @param string $name Name to match
557 * @return bool
558 */
559 public static function isIP( $name ) {
560 return preg_match( '/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/', $name ) || IP::isIPv6( $name );
561 }
562
563 /**
564 * Is the input a valid username?
565 *
566 * Checks if the input is a valid username, we don't want an empty string,
567 * an IP address, anything that contains slashes (would mess up subpages),
568 * is longer than the maximum allowed username size or doesn't begin with
569 * a capital letter.
570 *
571 * @param string $name Name to match
572 * @return bool
573 */
574 public static function isValidUserName( $name ) {
575 global $wgContLang, $wgMaxNameChars;
576
577 if ( $name == ''
578 || User::isIP( $name )
579 || strpos( $name, '/' ) !== false
580 || strlen( $name ) > $wgMaxNameChars
581 || $name != $wgContLang->ucfirst( $name ) ) {
582 wfDebugLog( 'username', __METHOD__ .
583 ": '$name' invalid due to empty, IP, slash, length, or lowercase" );
584 return false;
585 }
586
587 // Ensure that the name can't be misresolved as a different title,
588 // such as with extra namespace keys at the start.
589 $parsed = Title::newFromText( $name );
590 if ( is_null( $parsed )
591 || $parsed->getNamespace()
592 || strcmp( $name, $parsed->getPrefixedText() ) ) {
593 wfDebugLog( 'username', __METHOD__ .
594 ": '$name' invalid due to ambiguous prefixes" );
595 return false;
596 }
597
598 // Check an additional blacklist of troublemaker characters.
599 // Should these be merged into the title char list?
600 $unicodeBlacklist = '/[' .
601 '\x{0080}-\x{009f}' . # iso-8859-1 control chars
602 '\x{00a0}' . # non-breaking space
603 '\x{2000}-\x{200f}' . # various whitespace
604 '\x{2028}-\x{202f}' . # breaks and control chars
605 '\x{3000}' . # ideographic space
606 '\x{e000}-\x{f8ff}' . # private use
607 ']/u';
608 if ( preg_match( $unicodeBlacklist, $name ) ) {
609 wfDebugLog( 'username', __METHOD__ .
610 ": '$name' invalid due to blacklisted characters" );
611 return false;
612 }
613
614 return true;
615 }
616
617 /**
618 * Usernames which fail to pass this function will be blocked
619 * from user login and new account registrations, but may be used
620 * internally by batch processes.
621 *
622 * If an account already exists in this form, login will be blocked
623 * by a failure to pass this function.
624 *
625 * @param string $name Name to match
626 * @return bool
627 */
628 public static function isUsableName( $name ) {
629 global $wgReservedUsernames;
630 // Must be a valid username, obviously ;)
631 if ( !self::isValidUserName( $name ) ) {
632 return false;
633 }
634
635 static $reservedUsernames = false;
636 if ( !$reservedUsernames ) {
637 $reservedUsernames = $wgReservedUsernames;
638 wfRunHooks( 'UserGetReservedNames', array( &$reservedUsernames ) );
639 }
640
641 // Certain names may be reserved for batch processes.
642 foreach ( $reservedUsernames as $reserved ) {
643 if ( substr( $reserved, 0, 4 ) == 'msg:' ) {
644 $reserved = wfMessage( substr( $reserved, 4 ) )->inContentLanguage()->text();
645 }
646 if ( $reserved == $name ) {
647 return false;
648 }
649 }
650 return true;
651 }
652
653 /**
654 * Usernames which fail to pass this function will be blocked
655 * from new account registrations, but may be used internally
656 * either by batch processes or by user accounts which have
657 * already been created.
658 *
659 * Additional blacklisting may be added here rather than in
660 * isValidUserName() to avoid disrupting existing accounts.
661 *
662 * @param string $name to match
663 * @return bool
664 */
665 public static function isCreatableName( $name ) {
666 global $wgInvalidUsernameCharacters;
667
668 // Ensure that the username isn't longer than 235 bytes, so that
669 // (at least for the builtin skins) user javascript and css files
670 // will work. (bug 23080)
671 if ( strlen( $name ) > 235 ) {
672 wfDebugLog( 'username', __METHOD__ .
673 ": '$name' invalid due to length" );
674 return false;
675 }
676
677 // Preg yells if you try to give it an empty string
678 if ( $wgInvalidUsernameCharacters !== '' ) {
679 if ( preg_match( '/[' . preg_quote( $wgInvalidUsernameCharacters, '/' ) . ']/', $name ) ) {
680 wfDebugLog( 'username', __METHOD__ .
681 ": '$name' invalid due to wgInvalidUsernameCharacters" );
682 return false;
683 }
684 }
685
686 return self::isUsableName( $name );
687 }
688
689 /**
690 * Is the input a valid password for this user?
691 *
692 * @param string $password Desired password
693 * @return bool
694 */
695 public function isValidPassword( $password ) {
696 //simple boolean wrapper for getPasswordValidity
697 return $this->getPasswordValidity( $password ) === true;
698 }
699
700 /**
701 * Given unvalidated password input, return error message on failure.
702 *
703 * @param string $password Desired password
704 * @return mixed: true on success, string or array of error message on failure
705 */
706 public function getPasswordValidity( $password ) {
707 global $wgMinimalPasswordLength, $wgContLang;
708
709 static $blockedLogins = array(
710 'Useruser' => 'Passpass', 'Useruser1' => 'Passpass1', # r75589
711 'Apitestsysop' => 'testpass', 'Apitestuser' => 'testpass' # r75605
712 );
713
714 $result = false; //init $result to false for the internal checks
715
716 if ( !wfRunHooks( 'isValidPassword', array( $password, &$result, $this ) ) ) {
717 return $result;
718 }
719
720 if ( $result === false ) {
721 if ( strlen( $password ) < $wgMinimalPasswordLength ) {
722 return 'passwordtooshort';
723 } elseif ( $wgContLang->lc( $password ) == $wgContLang->lc( $this->mName ) ) {
724 return 'password-name-match';
725 } elseif ( isset( $blockedLogins[$this->getName()] ) && $password == $blockedLogins[$this->getName()] ) {
726 return 'password-login-forbidden';
727 } else {
728 //it seems weird returning true here, but this is because of the
729 //initialization of $result to false above. If the hook is never run or it
730 //doesn't modify $result, then we will likely get down into this if with
731 //a valid password.
732 return true;
733 }
734 } elseif ( $result === true ) {
735 return true;
736 } else {
737 return $result; //the isValidPassword hook set a string $result and returned true
738 }
739 }
740
741 /**
742 * Expire a user's password
743 * @since 1.23
744 * @param $ts Mixed: optional timestamp to convert, default 0 for the current time
745 */
746 public function expirePassword( $ts = 0 ) {
747 $this->load();
748 $timestamp = wfTimestamp( TS_MW, $ts );
749 $this->mPasswordExpires = $timestamp;
750 $this->saveSettings();
751 }
752
753 /**
754 * Clear the password expiration for a user
755 * @since 1.23
756 * @param bool $load ensure user object is loaded first
757 */
758 public function resetPasswordExpiration( $load = true ) {
759 global $wgPasswordExpirationDays;
760 if ( $load ) {
761 $this->load();
762 }
763 $newExpire = null;
764 if ( $wgPasswordExpirationDays ) {
765 $newExpire = wfTimestamp(
766 TS_MW,
767 time() + ( $wgPasswordExpirationDays * 24 * 3600 )
768 );
769 }
770 // Give extensions a chance to force an expiration
771 wfRunHooks( 'ResetPasswordExpiration', array( $this, &$newExpire ) );
772 $this->mPasswordExpires = $newExpire;
773 }
774
775 /**
776 * Check if the user's password is expired.
777 * TODO: Put this and password length into a PasswordPolicy object
778 * @since 1.23
779 * @return string|bool The expiration type, or false if not expired
780 * hard: A password change is required to login
781 * soft: Allow login, but encourage password change
782 * false: Password is not expired
783 */
784 public function getPasswordExpired() {
785 global $wgPasswordExpireGrace;
786 $expired = false;
787 $now = wfTimestamp();
788 $expiration = $this->getPasswordExpireDate();
789 $expUnix = wfTimestamp( TS_UNIX, $expiration );
790 if ( $expiration !== null && $expUnix < $now ) {
791 $expired = ( $expUnix + $wgPasswordExpireGrace < $now ) ? 'hard' : 'soft';
792 }
793 return $expired;
794 }
795
796 /**
797 * Get this user's password expiration date. Since this may be using
798 * the cached User object, we assume that whatever mechanism is setting
799 * the expiration date is also expiring the User cache.
800 * @since 1.23
801 * @return string|false the datestamp of the expiration, or null if not set
802 */
803 public function getPasswordExpireDate() {
804 $this->load();
805 return $this->mPasswordExpires;
806 }
807
808 /**
809 * Does a string look like an e-mail address?
810 *
811 * This validates an email address using an HTML5 specification found at:
812 * http://www.whatwg.org/html/states-of-the-type-attribute.html#valid-e-mail-address
813 * Which as of 2011-01-24 says:
814 *
815 * A valid e-mail address is a string that matches the ABNF production
816 * 1*( atext / "." ) "@" ldh-str *( "." ldh-str ) where atext is defined
817 * in RFC 5322 section 3.2.3, and ldh-str is defined in RFC 1034 section
818 * 3.5.
819 *
820 * This function is an implementation of the specification as requested in
821 * bug 22449.
822 *
823 * Client-side forms will use the same standard validation rules via JS or
824 * HTML 5 validation; additional restrictions can be enforced server-side
825 * by extensions via the 'isValidEmailAddr' hook.
826 *
827 * Note that this validation doesn't 100% match RFC 2822, but is believed
828 * to be liberal enough for wide use. Some invalid addresses will still
829 * pass validation here.
830 *
831 * @param string $addr E-mail address
832 * @return bool
833 * @deprecated since 1.18 call Sanitizer::isValidEmail() directly
834 */
835 public static function isValidEmailAddr( $addr ) {
836 wfDeprecated( __METHOD__, '1.18' );
837 return Sanitizer::validateEmail( $addr );
838 }
839
840 /**
841 * Given unvalidated user input, return a canonical username, or false if
842 * the username is invalid.
843 * @param string $name User input
844 * @param string|bool $validate type of validation to use:
845 * - false No validation
846 * - 'valid' Valid for batch processes
847 * - 'usable' Valid for batch processes and login
848 * - 'creatable' Valid for batch processes, login and account creation
849 *
850 * @throws MWException
851 * @return bool|string
852 */
853 public static function getCanonicalName( $name, $validate = 'valid' ) {
854 // Force usernames to capital
855 global $wgContLang;
856 $name = $wgContLang->ucfirst( $name );
857
858 # Reject names containing '#'; these will be cleaned up
859 # with title normalisation, but then it's too late to
860 # check elsewhere
861 if ( strpos( $name, '#' ) !== false ) {
862 return false;
863 }
864
865 // Clean up name according to title rules
866 $t = ( $validate === 'valid' ) ?
867 Title::newFromText( $name ) : Title::makeTitle( NS_USER, $name );
868 // Check for invalid titles
869 if ( is_null( $t ) ) {
870 return false;
871 }
872
873 // Reject various classes of invalid names
874 global $wgAuth;
875 $name = $wgAuth->getCanonicalName( $t->getText() );
876
877 switch ( $validate ) {
878 case false:
879 break;
880 case 'valid':
881 if ( !User::isValidUserName( $name ) ) {
882 $name = false;
883 }
884 break;
885 case 'usable':
886 if ( !User::isUsableName( $name ) ) {
887 $name = false;
888 }
889 break;
890 case 'creatable':
891 if ( !User::isCreatableName( $name ) ) {
892 $name = false;
893 }
894 break;
895 default:
896 throw new MWException( 'Invalid parameter value for $validate in ' . __METHOD__ );
897 }
898 return $name;
899 }
900
901 /**
902 * Count the number of edits of a user
903 *
904 * @param int $uid User ID to check
905 * @return int The user's edit count
906 *
907 * @deprecated since 1.21 in favour of User::getEditCount
908 */
909 public static function edits( $uid ) {
910 wfDeprecated( __METHOD__, '1.21' );
911 $user = self::newFromId( $uid );
912 return $user->getEditCount();
913 }
914
915 /**
916 * Return a random password.
917 *
918 * @return string New random password
919 */
920 public static function randomPassword() {
921 global $wgMinimalPasswordLength;
922 // Decide the final password length based on our min password length, stopping at a minimum of 10 chars
923 $length = max( 10, $wgMinimalPasswordLength );
924 // Multiply by 1.25 to get the number of hex characters we need
925 $length = $length * 1.25;
926 // Generate random hex chars
927 $hex = MWCryptRand::generateHex( $length );
928 // Convert from base 16 to base 32 to get a proper password like string
929 return wfBaseConvert( $hex, 16, 32 );
930 }
931
932 /**
933 * Set cached properties to default.
934 *
935 * @note This no longer clears uncached lazy-initialised properties;
936 * the constructor does that instead.
937 *
938 * @param $name string|bool
939 */
940 public function loadDefaults( $name = false ) {
941 wfProfileIn( __METHOD__ );
942
943 $this->mId = 0;
944 $this->mName = $name;
945 $this->mRealName = '';
946 $this->mPassword = $this->mNewpassword = '';
947 $this->mNewpassTime = null;
948 $this->mEmail = '';
949 $this->mOptionOverrides = null;
950 $this->mOptionsLoaded = false;
951
952 $loggedOut = $this->getRequest()->getCookie( 'LoggedOut' );
953 if ( $loggedOut !== null ) {
954 $this->mTouched = wfTimestamp( TS_MW, $loggedOut );
955 } else {
956 $this->mTouched = '1'; # Allow any pages to be cached
957 }
958
959 $this->mToken = null; // Don't run cryptographic functions till we need a token
960 $this->mEmailAuthenticated = null;
961 $this->mEmailToken = '';
962 $this->mEmailTokenExpires = null;
963 $this->mPasswordExpires = null;
964 $this->resetPasswordExpiration( false );
965 $this->mRegistration = wfTimestamp( TS_MW );
966 $this->mGroups = array();
967
968 wfRunHooks( 'UserLoadDefaults', array( $this, $name ) );
969
970 wfProfileOut( __METHOD__ );
971 }
972
973 /**
974 * Return whether an item has been loaded.
975 *
976 * @param string $item item to check. Current possibilities:
977 * - id
978 * - name
979 * - realname
980 * @param string $all 'all' to check if the whole object has been loaded
981 * or any other string to check if only the item is available (e.g.
982 * for optimisation)
983 * @return boolean
984 */
985 public function isItemLoaded( $item, $all = 'all' ) {
986 return ( $this->mLoadedItems === true && $all === 'all' ) ||
987 ( isset( $this->mLoadedItems[$item] ) && $this->mLoadedItems[$item] === true );
988 }
989
990 /**
991 * Set that an item has been loaded
992 *
993 * @param string $item
994 */
995 protected function setItemLoaded( $item ) {
996 if ( is_array( $this->mLoadedItems ) ) {
997 $this->mLoadedItems[$item] = true;
998 }
999 }
1000
1001 /**
1002 * Load user data from the session or login cookie.
1003 * @return bool True if the user is logged in, false otherwise.
1004 */
1005 private function loadFromSession() {
1006 $result = null;
1007 wfRunHooks( 'UserLoadFromSession', array( $this, &$result ) );
1008 if ( $result !== null ) {
1009 return $result;
1010 }
1011
1012 $request = $this->getRequest();
1013
1014 $cookieId = $request->getCookie( 'UserID' );
1015 $sessId = $request->getSessionData( 'wsUserID' );
1016
1017 if ( $cookieId !== null ) {
1018 $sId = intval( $cookieId );
1019 if ( $sessId !== null && $cookieId != $sessId ) {
1020 wfDebugLog( 'loginSessions', "Session user ID ($sessId) and
1021 cookie user ID ($sId) don't match!" );
1022 return false;
1023 }
1024 $request->setSessionData( 'wsUserID', $sId );
1025 } elseif ( $sessId !== null && $sessId != 0 ) {
1026 $sId = $sessId;
1027 } else {
1028 return false;
1029 }
1030
1031 if ( $request->getSessionData( 'wsUserName' ) !== null ) {
1032 $sName = $request->getSessionData( 'wsUserName' );
1033 } elseif ( $request->getCookie( 'UserName' ) !== null ) {
1034 $sName = $request->getCookie( 'UserName' );
1035 $request->setSessionData( 'wsUserName', $sName );
1036 } else {
1037 return false;
1038 }
1039
1040 $proposedUser = User::newFromId( $sId );
1041 if ( !$proposedUser->isLoggedIn() ) {
1042 // Not a valid ID
1043 return false;
1044 }
1045
1046 global $wgBlockDisablesLogin;
1047 if ( $wgBlockDisablesLogin && $proposedUser->isBlocked() ) {
1048 // User blocked and we've disabled blocked user logins
1049 return false;
1050 }
1051
1052 if ( $request->getSessionData( 'wsToken' ) ) {
1053 $passwordCorrect = ( $proposedUser->getToken( false ) === $request->getSessionData( 'wsToken' ) );
1054 $from = 'session';
1055 } elseif ( $request->getCookie( 'Token' ) ) {
1056 # Get the token from DB/cache and clean it up to remove garbage padding.
1057 # This deals with historical problems with bugs and the default column value.
1058 $token = rtrim( $proposedUser->getToken( false ) ); // correct token
1059 $passwordCorrect = ( strlen( $token ) && $token === $request->getCookie( 'Token' ) );
1060 $from = 'cookie';
1061 } else {
1062 // No session or persistent login cookie
1063 return false;
1064 }
1065
1066 if ( ( $sName === $proposedUser->getName() ) && $passwordCorrect ) {
1067 $this->loadFromUserObject( $proposedUser );
1068 $request->setSessionData( 'wsToken', $this->mToken );
1069 wfDebug( "User: logged in from $from\n" );
1070 return true;
1071 } else {
1072 // Invalid credentials
1073 wfDebug( "User: can't log in from $from, invalid credentials\n" );
1074 return false;
1075 }
1076 }
1077
1078 /**
1079 * Load user and user_group data from the database.
1080 * $this->mId must be set, this is how the user is identified.
1081 *
1082 * @return bool True if the user exists, false if the user is anonymous
1083 */
1084 public function loadFromDatabase() {
1085 // Paranoia
1086 $this->mId = intval( $this->mId );
1087
1088 // Anonymous user
1089 if ( !$this->mId ) {
1090 $this->loadDefaults();
1091 return false;
1092 }
1093
1094 $dbr = wfGetDB( DB_MASTER );
1095 $s = $dbr->selectRow( 'user', self::selectFields(), array( 'user_id' => $this->mId ), __METHOD__ );
1096
1097 wfRunHooks( 'UserLoadFromDatabase', array( $this, &$s ) );
1098
1099 if ( $s !== false ) {
1100 // Initialise user table data
1101 $this->loadFromRow( $s );
1102 $this->mGroups = null; // deferred
1103 $this->getEditCount(); // revalidation for nulls
1104 return true;
1105 } else {
1106 // Invalid user_id
1107 $this->mId = 0;
1108 $this->loadDefaults();
1109 return false;
1110 }
1111 }
1112
1113 /**
1114 * Initialize this object from a row from the user table.
1115 *
1116 * @param stdClass $row Row from the user table to load.
1117 * @param array $data Further user data to load into the object
1118 *
1119 * user_groups Array with groups out of the user_groups table
1120 * user_properties Array with properties out of the user_properties table
1121 */
1122 public function loadFromRow( $row, $data = null ) {
1123 $all = true;
1124
1125 $this->mGroups = null; // deferred
1126
1127 if ( isset( $row->user_name ) ) {
1128 $this->mName = $row->user_name;
1129 $this->mFrom = 'name';
1130 $this->setItemLoaded( 'name' );
1131 } else {
1132 $all = false;
1133 }
1134
1135 if ( isset( $row->user_real_name ) ) {
1136 $this->mRealName = $row->user_real_name;
1137 $this->setItemLoaded( 'realname' );
1138 } else {
1139 $all = false;
1140 }
1141
1142 if ( isset( $row->user_id ) ) {
1143 $this->mId = intval( $row->user_id );
1144 $this->mFrom = 'id';
1145 $this->setItemLoaded( 'id' );
1146 } else {
1147 $all = false;
1148 }
1149
1150 if ( isset( $row->user_editcount ) ) {
1151 $this->mEditCount = $row->user_editcount;
1152 } else {
1153 $all = false;
1154 }
1155
1156 if ( isset( $row->user_password ) ) {
1157 $this->mPassword = $row->user_password;
1158 $this->mNewpassword = $row->user_newpassword;
1159 $this->mNewpassTime = wfTimestampOrNull( TS_MW, $row->user_newpass_time );
1160 $this->mEmail = $row->user_email;
1161 if ( isset( $row->user_options ) ) {
1162 $this->decodeOptions( $row->user_options );
1163 }
1164 $this->mTouched = wfTimestamp( TS_MW, $row->user_touched );
1165 $this->mToken = $row->user_token;
1166 if ( $this->mToken == '' ) {
1167 $this->mToken = null;
1168 }
1169 $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated );
1170 $this->mEmailToken = $row->user_email_token;
1171 $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires );
1172 $this->mPasswordExpires = wfTimestampOrNull( TS_MW, $row->user_password_expires );
1173 $this->mRegistration = wfTimestampOrNull( TS_MW, $row->user_registration );
1174 } else {
1175 $all = false;
1176 }
1177
1178 if ( $all ) {
1179 $this->mLoadedItems = true;
1180 }
1181
1182 if ( is_array( $data ) ) {
1183 if ( isset( $data['user_groups'] ) && is_array( $data['user_groups'] ) ) {
1184 $this->mGroups = $data['user_groups'];
1185 }
1186 if ( isset( $data['user_properties'] ) && is_array( $data['user_properties'] ) ) {
1187 $this->loadOptions( $data['user_properties'] );
1188 }
1189 }
1190 }
1191
1192 /**
1193 * Load the data for this user object from another user object.
1194 *
1195 * @param $user User
1196 */
1197 protected function loadFromUserObject( $user ) {
1198 $user->load();
1199 $user->loadGroups();
1200 $user->loadOptions();
1201 foreach ( self::$mCacheVars as $var ) {
1202 $this->$var = $user->$var;
1203 }
1204 }
1205
1206 /**
1207 * Load the groups from the database if they aren't already loaded.
1208 */
1209 private function loadGroups() {
1210 if ( is_null( $this->mGroups ) ) {
1211 $dbr = wfGetDB( DB_MASTER );
1212 $res = $dbr->select( 'user_groups',
1213 array( 'ug_group' ),
1214 array( 'ug_user' => $this->mId ),
1215 __METHOD__ );
1216 $this->mGroups = array();
1217 foreach ( $res as $row ) {
1218 $this->mGroups[] = $row->ug_group;
1219 }
1220 }
1221 }
1222
1223 /**
1224 * Add the user to the group if he/she meets given criteria.
1225 *
1226 * Contrary to autopromotion by \ref $wgAutopromote, the group will be
1227 * possible to remove manually via Special:UserRights. In such case it
1228 * will not be re-added automatically. The user will also not lose the
1229 * group if they no longer meet the criteria.
1230 *
1231 * @param string $event key in $wgAutopromoteOnce (each one has groups/criteria)
1232 *
1233 * @return array Array of groups the user has been promoted to.
1234 *
1235 * @see $wgAutopromoteOnce
1236 */
1237 public function addAutopromoteOnceGroups( $event ) {
1238 global $wgAutopromoteOnceLogInRC, $wgAuth;
1239
1240 $toPromote = array();
1241 if ( $this->getId() ) {
1242 $toPromote = Autopromote::getAutopromoteOnceGroups( $this, $event );
1243 if ( count( $toPromote ) ) {
1244 $oldGroups = $this->getGroups(); // previous groups
1245
1246 foreach ( $toPromote as $group ) {
1247 $this->addGroup( $group );
1248 }
1249 // update groups in external authentication database
1250 $wgAuth->updateExternalDBGroups( $this, $toPromote );
1251
1252 $newGroups = array_merge( $oldGroups, $toPromote ); // all groups
1253
1254 $logEntry = new ManualLogEntry( 'rights', 'autopromote' );
1255 $logEntry->setPerformer( $this );
1256 $logEntry->setTarget( $this->getUserPage() );
1257 $logEntry->setParameters( array(
1258 '4::oldgroups' => $oldGroups,
1259 '5::newgroups' => $newGroups,
1260 ) );
1261 $logid = $logEntry->insert();
1262 if ( $wgAutopromoteOnceLogInRC ) {
1263 $logEntry->publish( $logid );
1264 }
1265 }
1266 }
1267 return $toPromote;
1268 }
1269
1270 /**
1271 * Clear various cached data stored in this object. The cache of the user table
1272 * data (i.e. self::$mCacheVars) is not cleared unless $reloadFrom is given.
1273 *
1274 * @param bool|string $reloadFrom Reload user and user_groups table data from a
1275 * given source. May be "name", "id", "defaults", "session", or false for
1276 * no reload.
1277 */
1278 public function clearInstanceCache( $reloadFrom = false ) {
1279 $this->mNewtalk = -1;
1280 $this->mDatePreference = null;
1281 $this->mBlockedby = -1; # Unset
1282 $this->mHash = false;
1283 $this->mRights = null;
1284 $this->mEffectiveGroups = null;
1285 $this->mImplicitGroups = null;
1286 $this->mGroups = null;
1287 $this->mOptions = null;
1288 $this->mOptionsLoaded = false;
1289 $this->mEditCount = null;
1290
1291 if ( $reloadFrom ) {
1292 $this->mLoadedItems = array();
1293 $this->mFrom = $reloadFrom;
1294 }
1295 }
1296
1297 /**
1298 * Combine the language default options with any site-specific options
1299 * and add the default language variants.
1300 *
1301 * @return Array of String options
1302 */
1303 public static function getDefaultOptions() {
1304 global $wgNamespacesToBeSearchedDefault, $wgDefaultUserOptions, $wgContLang, $wgDefaultSkin;
1305
1306 static $defOpt = null;
1307 if ( !defined( 'MW_PHPUNIT_TEST' ) && $defOpt !== null ) {
1308 // Disabling this for the unit tests, as they rely on being able to change $wgContLang
1309 // mid-request and see that change reflected in the return value of this function.
1310 // Which is insane and would never happen during normal MW operation
1311 return $defOpt;
1312 }
1313
1314 $defOpt = $wgDefaultUserOptions;
1315 // Default language setting
1316 $defOpt['language'] = $wgContLang->getCode();
1317 foreach ( LanguageConverter::$languagesWithVariants as $langCode ) {
1318 $defOpt[$langCode == $wgContLang->getCode() ? 'variant' : "variant-$langCode"] = $langCode;
1319 }
1320 foreach ( SearchEngine::searchableNamespaces() as $nsnum => $nsname ) {
1321 $defOpt['searchNs' . $nsnum] = !empty( $wgNamespacesToBeSearchedDefault[$nsnum] );
1322 }
1323 $defOpt['skin'] = $wgDefaultSkin;
1324
1325 wfRunHooks( 'UserGetDefaultOptions', array( &$defOpt ) );
1326
1327 return $defOpt;
1328 }
1329
1330 /**
1331 * Get a given default option value.
1332 *
1333 * @param string $opt Name of option to retrieve
1334 * @return string Default option value
1335 */
1336 public static function getDefaultOption( $opt ) {
1337 $defOpts = self::getDefaultOptions();
1338 if ( isset( $defOpts[$opt] ) ) {
1339 return $defOpts[$opt];
1340 } else {
1341 return null;
1342 }
1343 }
1344
1345 /**
1346 * Get blocking information
1347 * @param bool $bFromSlave Whether to check the slave database first. To
1348 * improve performance, non-critical checks are done
1349 * against slaves. Check when actually saving should be
1350 * done against master.
1351 */
1352 private function getBlockedStatus( $bFromSlave = true ) {
1353 global $wgProxyWhitelist, $wgUser, $wgApplyIpBlocksToXff;
1354
1355 if ( -1 != $this->mBlockedby ) {
1356 return;
1357 }
1358
1359 wfProfileIn( __METHOD__ );
1360 wfDebug( __METHOD__ . ": checking...\n" );
1361
1362 // Initialize data...
1363 // Otherwise something ends up stomping on $this->mBlockedby when
1364 // things get lazy-loaded later, causing false positive block hits
1365 // due to -1 !== 0. Probably session-related... Nothing should be
1366 // overwriting mBlockedby, surely?
1367 $this->load();
1368
1369 # We only need to worry about passing the IP address to the Block generator if the
1370 # user is not immune to autoblocks/hardblocks, and they are the current user so we
1371 # know which IP address they're actually coming from
1372 if ( !$this->isAllowed( 'ipblock-exempt' ) && $this->getID() == $wgUser->getID() ) {
1373 $ip = $this->getRequest()->getIP();
1374 } else {
1375 $ip = null;
1376 }
1377
1378 // User/IP blocking
1379 $block = Block::newFromTarget( $this, $ip, !$bFromSlave );
1380
1381 // Proxy blocking
1382 if ( !$block instanceof Block && $ip !== null && !$this->isAllowed( 'proxyunbannable' )
1383 && !in_array( $ip, $wgProxyWhitelist )
1384 ) {
1385 // Local list
1386 if ( self::isLocallyBlockedProxy( $ip ) ) {
1387 $block = new Block;
1388 $block->setBlocker( wfMessage( 'proxyblocker' )->text() );
1389 $block->mReason = wfMessage( 'proxyblockreason' )->text();
1390 $block->setTarget( $ip );
1391 } elseif ( $this->isAnon() && $this->isDnsBlacklisted( $ip ) ) {
1392 $block = new Block;
1393 $block->setBlocker( wfMessage( 'sorbs' )->text() );
1394 $block->mReason = wfMessage( 'sorbsreason' )->text();
1395 $block->setTarget( $ip );
1396 }
1397 }
1398
1399 // (bug 23343) Apply IP blocks to the contents of XFF headers, if enabled
1400 if ( !$block instanceof Block
1401 && $wgApplyIpBlocksToXff
1402 && $ip !== null
1403 && !$this->isAllowed( 'proxyunbannable' )
1404 && !in_array( $ip, $wgProxyWhitelist )
1405 ) {
1406 $xff = $this->getRequest()->getHeader( 'X-Forwarded-For' );
1407 $xff = array_map( 'trim', explode( ',', $xff ) );
1408 $xff = array_diff( $xff, array( $ip ) );
1409 $xffblocks = Block::getBlocksForIPList( $xff, $this->isAnon(), !$bFromSlave );
1410 $block = Block::chooseBlock( $xffblocks, $xff );
1411 if ( $block instanceof Block ) {
1412 # Mangle the reason to alert the user that the block
1413 # originated from matching the X-Forwarded-For header.
1414 $block->mReason = wfMessage( 'xffblockreason', $block->mReason )->text();
1415 }
1416 }
1417
1418 if ( $block instanceof Block ) {
1419 wfDebug( __METHOD__ . ": Found block.\n" );
1420 $this->mBlock = $block;
1421 $this->mBlockedby = $block->getByName();
1422 $this->mBlockreason = $block->mReason;
1423 $this->mHideName = $block->mHideName;
1424 $this->mAllowUsertalk = !$block->prevents( 'editownusertalk' );
1425 } else {
1426 $this->mBlockedby = '';
1427 $this->mHideName = 0;
1428 $this->mAllowUsertalk = false;
1429 }
1430
1431 // Extensions
1432 wfRunHooks( 'GetBlockedStatus', array( &$this ) );
1433
1434 wfProfileOut( __METHOD__ );
1435 }
1436
1437 /**
1438 * Whether the given IP is in a DNS blacklist.
1439 *
1440 * @param string $ip IP to check
1441 * @param bool $checkWhitelist whether to check the whitelist first
1442 * @return bool True if blacklisted.
1443 */
1444 public function isDnsBlacklisted( $ip, $checkWhitelist = false ) {
1445 global $wgEnableSorbs, $wgEnableDnsBlacklist,
1446 $wgSorbsUrl, $wgDnsBlacklistUrls, $wgProxyWhitelist;
1447
1448 if ( !$wgEnableDnsBlacklist && !$wgEnableSorbs ) {
1449 return false;
1450 }
1451
1452 if ( $checkWhitelist && in_array( $ip, $wgProxyWhitelist ) ) {
1453 return false;
1454 }
1455
1456 $urls = array_merge( $wgDnsBlacklistUrls, (array)$wgSorbsUrl );
1457 return $this->inDnsBlacklist( $ip, $urls );
1458 }
1459
1460 /**
1461 * Whether the given IP is in a given DNS blacklist.
1462 *
1463 * @param string $ip IP to check
1464 * @param string|array $bases of Strings: URL of the DNS blacklist
1465 * @return bool True if blacklisted.
1466 */
1467 public function inDnsBlacklist( $ip, $bases ) {
1468 wfProfileIn( __METHOD__ );
1469
1470 $found = false;
1471 // @todo FIXME: IPv6 ??? (http://bugs.php.net/bug.php?id=33170)
1472 if ( IP::isIPv4( $ip ) ) {
1473 // Reverse IP, bug 21255
1474 $ipReversed = implode( '.', array_reverse( explode( '.', $ip ) ) );
1475
1476 foreach ( (array)$bases as $base ) {
1477 // Make hostname
1478 // If we have an access key, use that too (ProjectHoneypot, etc.)
1479 if ( is_array( $base ) ) {
1480 if ( count( $base ) >= 2 ) {
1481 // Access key is 1, base URL is 0
1482 $host = "{$base[1]}.$ipReversed.{$base[0]}";
1483 } else {
1484 $host = "$ipReversed.{$base[0]}";
1485 }
1486 } else {
1487 $host = "$ipReversed.$base";
1488 }
1489
1490 // Send query
1491 $ipList = gethostbynamel( $host );
1492
1493 if ( $ipList ) {
1494 wfDebugLog( 'dnsblacklist', "Hostname $host is {$ipList[0]}, it's a proxy says $base!" );
1495 $found = true;
1496 break;
1497 } else {
1498 wfDebugLog( 'dnsblacklist', "Requested $host, not found in $base." );
1499 }
1500 }
1501 }
1502
1503 wfProfileOut( __METHOD__ );
1504 return $found;
1505 }
1506
1507 /**
1508 * Check if an IP address is in the local proxy list
1509 *
1510 * @param $ip string
1511 *
1512 * @return bool
1513 */
1514 public static function isLocallyBlockedProxy( $ip ) {
1515 global $wgProxyList;
1516
1517 if ( !$wgProxyList ) {
1518 return false;
1519 }
1520 wfProfileIn( __METHOD__ );
1521
1522 if ( !is_array( $wgProxyList ) ) {
1523 // Load from the specified file
1524 $wgProxyList = array_map( 'trim', file( $wgProxyList ) );
1525 }
1526
1527 if ( !is_array( $wgProxyList ) ) {
1528 $ret = false;
1529 } elseif ( array_search( $ip, $wgProxyList ) !== false ) {
1530 $ret = true;
1531 } elseif ( array_key_exists( $ip, $wgProxyList ) ) {
1532 // Old-style flipped proxy list
1533 $ret = true;
1534 } else {
1535 $ret = false;
1536 }
1537 wfProfileOut( __METHOD__ );
1538 return $ret;
1539 }
1540
1541 /**
1542 * Is this user subject to rate limiting?
1543 *
1544 * @return bool True if rate limited
1545 */
1546 public function isPingLimitable() {
1547 global $wgRateLimitsExcludedIPs;
1548 if ( in_array( $this->getRequest()->getIP(), $wgRateLimitsExcludedIPs ) ) {
1549 // No other good way currently to disable rate limits
1550 // for specific IPs. :P
1551 // But this is a crappy hack and should die.
1552 return false;
1553 }
1554 return !$this->isAllowed( 'noratelimit' );
1555 }
1556
1557 /**
1558 * Primitive rate limits: enforce maximum actions per time period
1559 * to put a brake on flooding.
1560 *
1561 * @note When using a shared cache like memcached, IP-address
1562 * last-hit counters will be shared across wikis.
1563 *
1564 * @param string $action Action to enforce; 'edit' if unspecified
1565 * @param integer $incrBy Positive amount to increment counter by [defaults to 1]
1566 * @return bool True if a rate limiter was tripped
1567 */
1568 public function pingLimiter( $action = 'edit', $incrBy = 1 ) {
1569 // Call the 'PingLimiter' hook
1570 $result = false;
1571 if ( !wfRunHooks( 'PingLimiter', array( &$this, $action, &$result, $incrBy ) ) ) {
1572 return $result;
1573 }
1574
1575 global $wgRateLimits;
1576 if ( !isset( $wgRateLimits[$action] ) ) {
1577 return false;
1578 }
1579
1580 // Some groups shouldn't trigger the ping limiter, ever
1581 if ( !$this->isPingLimitable() ) {
1582 return false;
1583 }
1584
1585 global $wgMemc, $wgRateLimitLog;
1586 wfProfileIn( __METHOD__ );
1587
1588 $limits = $wgRateLimits[$action];
1589 $keys = array();
1590 $id = $this->getId();
1591 $userLimit = false;
1592
1593 if ( isset( $limits['anon'] ) && $id == 0 ) {
1594 $keys[wfMemcKey( 'limiter', $action, 'anon' )] = $limits['anon'];
1595 }
1596
1597 if ( isset( $limits['user'] ) && $id != 0 ) {
1598 $userLimit = $limits['user'];
1599 }
1600 if ( $this->isNewbie() ) {
1601 if ( isset( $limits['newbie'] ) && $id != 0 ) {
1602 $keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $limits['newbie'];
1603 }
1604 if ( isset( $limits['ip'] ) ) {
1605 $ip = $this->getRequest()->getIP();
1606 $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip'];
1607 }
1608 if ( isset( $limits['subnet'] ) ) {
1609 $ip = $this->getRequest()->getIP();
1610 $matches = array();
1611 $subnet = false;
1612 if ( IP::isIPv6( $ip ) ) {
1613 $parts = IP::parseRange( "$ip/64" );
1614 $subnet = $parts[0];
1615 } elseif ( preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
1616 // IPv4
1617 $subnet = $matches[1];
1618 }
1619 if ( $subnet !== false ) {
1620 $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
1621 }
1622 }
1623 }
1624 // Check for group-specific permissions
1625 // If more than one group applies, use the group with the highest limit
1626 foreach ( $this->getGroups() as $group ) {
1627 if ( isset( $limits[$group] ) ) {
1628 if ( $userLimit === false || $limits[$group] > $userLimit ) {
1629 $userLimit = $limits[$group];
1630 }
1631 }
1632 }
1633 // Set the user limit key
1634 if ( $userLimit !== false ) {
1635 list( $max, $period ) = $userLimit;
1636 wfDebug( __METHOD__ . ": effective user limit: $max in {$period}s\n" );
1637 $keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $userLimit;
1638 }
1639
1640 $triggered = false;
1641 foreach ( $keys as $key => $limit ) {
1642 list( $max, $period ) = $limit;
1643 $summary = "(limit $max in {$period}s)";
1644 $count = $wgMemc->get( $key );
1645 // Already pinged?
1646 if ( $count ) {
1647 if ( $count >= $max ) {
1648 wfDebug( __METHOD__ . ": tripped! $key at $count $summary\n" );
1649 if ( $wgRateLimitLog ) {
1650 wfSuppressWarnings();
1651 file_put_contents( $wgRateLimitLog, wfTimestamp( TS_MW ) . ' ' . wfWikiID() . ': ' . $this->getName() . " tripped $key at $count $summary\n", FILE_APPEND );
1652 wfRestoreWarnings();
1653 }
1654 $triggered = true;
1655 } else {
1656 wfDebug( __METHOD__ . ": ok. $key at $count $summary\n" );
1657 }
1658 } else {
1659 wfDebug( __METHOD__ . ": adding record for $key $summary\n" );
1660 if ( $incrBy > 0 ) {
1661 $wgMemc->add( $key, 0, intval( $period ) ); // first ping
1662 }
1663 }
1664 if ( $incrBy > 0 ) {
1665 $wgMemc->incr( $key, $incrBy );
1666 }
1667 }
1668
1669 wfProfileOut( __METHOD__ );
1670 return $triggered;
1671 }
1672
1673 /**
1674 * Check if user is blocked
1675 *
1676 * @param bool $bFromSlave Whether to check the slave database instead of the master
1677 * @return bool True if blocked, false otherwise
1678 */
1679 public function isBlocked( $bFromSlave = true ) { // hacked from false due to horrible probs on site
1680 return $this->getBlock( $bFromSlave ) instanceof Block && $this->getBlock()->prevents( 'edit' );
1681 }
1682
1683 /**
1684 * Get the block affecting the user, or null if the user is not blocked
1685 *
1686 * @param bool $bFromSlave Whether to check the slave database instead of the master
1687 * @return Block|null
1688 */
1689 public function getBlock( $bFromSlave = true ) {
1690 $this->getBlockedStatus( $bFromSlave );
1691 return $this->mBlock instanceof Block ? $this->mBlock : null;
1692 }
1693
1694 /**
1695 * Check if user is blocked from editing a particular article
1696 *
1697 * @param Title $title Title to check
1698 * @param bool $bFromSlave whether to check the slave database instead of the master
1699 * @return bool
1700 */
1701 public function isBlockedFrom( $title, $bFromSlave = false ) {
1702 global $wgBlockAllowsUTEdit;
1703 wfProfileIn( __METHOD__ );
1704
1705 $blocked = $this->isBlocked( $bFromSlave );
1706 $allowUsertalk = ( $wgBlockAllowsUTEdit ? $this->mAllowUsertalk : false );
1707 // If a user's name is suppressed, they cannot make edits anywhere
1708 if ( !$this->mHideName && $allowUsertalk && $title->getText() === $this->getName()
1709 && $title->getNamespace() == NS_USER_TALK ) {
1710 $blocked = false;
1711 wfDebug( __METHOD__ . ": self-talk page, ignoring any blocks\n" );
1712 }
1713
1714 wfRunHooks( 'UserIsBlockedFrom', array( $this, $title, &$blocked, &$allowUsertalk ) );
1715
1716 wfProfileOut( __METHOD__ );
1717 return $blocked;
1718 }
1719
1720 /**
1721 * If user is blocked, return the name of the user who placed the block
1722 * @return string Name of blocker
1723 */
1724 public function blockedBy() {
1725 $this->getBlockedStatus();
1726 return $this->mBlockedby;
1727 }
1728
1729 /**
1730 * If user is blocked, return the specified reason for the block
1731 * @return string Blocking reason
1732 */
1733 public function blockedFor() {
1734 $this->getBlockedStatus();
1735 return $this->mBlockreason;
1736 }
1737
1738 /**
1739 * If user is blocked, return the ID for the block
1740 * @return int Block ID
1741 */
1742 public function getBlockId() {
1743 $this->getBlockedStatus();
1744 return ( $this->mBlock ? $this->mBlock->getId() : false );
1745 }
1746
1747 /**
1748 * Check if user is blocked on all wikis.
1749 * Do not use for actual edit permission checks!
1750 * This is intended for quick UI checks.
1751 *
1752 * @param string $ip IP address, uses current client if none given
1753 * @return bool True if blocked, false otherwise
1754 */
1755 public function isBlockedGlobally( $ip = '' ) {
1756 if ( $this->mBlockedGlobally !== null ) {
1757 return $this->mBlockedGlobally;
1758 }
1759 // User is already an IP?
1760 if ( IP::isIPAddress( $this->getName() ) ) {
1761 $ip = $this->getName();
1762 } elseif ( !$ip ) {
1763 $ip = $this->getRequest()->getIP();
1764 }
1765 $blocked = false;
1766 wfRunHooks( 'UserIsBlockedGlobally', array( &$this, $ip, &$blocked ) );
1767 $this->mBlockedGlobally = (bool)$blocked;
1768 return $this->mBlockedGlobally;
1769 }
1770
1771 /**
1772 * Check if user account is locked
1773 *
1774 * @return bool True if locked, false otherwise
1775 */
1776 public function isLocked() {
1777 if ( $this->mLocked !== null ) {
1778 return $this->mLocked;
1779 }
1780 global $wgAuth;
1781 StubObject::unstub( $wgAuth );
1782 $authUser = $wgAuth->getUserInstance( $this );
1783 $this->mLocked = (bool)$authUser->isLocked();
1784 return $this->mLocked;
1785 }
1786
1787 /**
1788 * Check if user account is hidden
1789 *
1790 * @return bool True if hidden, false otherwise
1791 */
1792 public function isHidden() {
1793 if ( $this->mHideName !== null ) {
1794 return $this->mHideName;
1795 }
1796 $this->getBlockedStatus();
1797 if ( !$this->mHideName ) {
1798 global $wgAuth;
1799 StubObject::unstub( $wgAuth );
1800 $authUser = $wgAuth->getUserInstance( $this );
1801 $this->mHideName = (bool)$authUser->isHidden();
1802 }
1803 return $this->mHideName;
1804 }
1805
1806 /**
1807 * Get the user's ID.
1808 * @return int The user's ID; 0 if the user is anonymous or nonexistent
1809 */
1810 public function getId() {
1811 if ( $this->mId === null && $this->mName !== null && User::isIP( $this->mName ) ) {
1812 // Special case, we know the user is anonymous
1813 return 0;
1814 } elseif ( !$this->isItemLoaded( 'id' ) ) {
1815 // Don't load if this was initialized from an ID
1816 $this->load();
1817 }
1818 return $this->mId;
1819 }
1820
1821 /**
1822 * Set the user and reload all fields according to a given ID
1823 * @param int $v User ID to reload
1824 */
1825 public function setId( $v ) {
1826 $this->mId = $v;
1827 $this->clearInstanceCache( 'id' );
1828 }
1829
1830 /**
1831 * Get the user name, or the IP of an anonymous user
1832 * @return string User's name or IP address
1833 */
1834 public function getName() {
1835 if ( $this->isItemLoaded( 'name', 'only' ) ) {
1836 // Special case optimisation
1837 return $this->mName;
1838 } else {
1839 $this->load();
1840 if ( $this->mName === false ) {
1841 // Clean up IPs
1842 $this->mName = IP::sanitizeIP( $this->getRequest()->getIP() );
1843 }
1844 return $this->mName;
1845 }
1846 }
1847
1848 /**
1849 * Set the user name.
1850 *
1851 * This does not reload fields from the database according to the given
1852 * name. Rather, it is used to create a temporary "nonexistent user" for
1853 * later addition to the database. It can also be used to set the IP
1854 * address for an anonymous user to something other than the current
1855 * remote IP.
1856 *
1857 * @note User::newFromName() has roughly the same function, when the named user
1858 * does not exist.
1859 * @param string $str New user name to set
1860 */
1861 public function setName( $str ) {
1862 $this->load();
1863 $this->mName = $str;
1864 }
1865
1866 /**
1867 * Get the user's name escaped by underscores.
1868 * @return string Username escaped by underscores.
1869 */
1870 public function getTitleKey() {
1871 return str_replace( ' ', '_', $this->getName() );
1872 }
1873
1874 /**
1875 * Check if the user has new messages.
1876 * @return bool True if the user has new messages
1877 */
1878 public function getNewtalk() {
1879 $this->load();
1880
1881 // Load the newtalk status if it is unloaded (mNewtalk=-1)
1882 if ( $this->mNewtalk === -1 ) {
1883 $this->mNewtalk = false; # reset talk page status
1884
1885 // Check memcached separately for anons, who have no
1886 // entire User object stored in there.
1887 if ( !$this->mId ) {
1888 global $wgDisableAnonTalk;
1889 if ( $wgDisableAnonTalk ) {
1890 // Anon newtalk disabled by configuration.
1891 $this->mNewtalk = false;
1892 } else {
1893 global $wgMemc;
1894 $key = wfMemcKey( 'newtalk', 'ip', $this->getName() );
1895 $newtalk = $wgMemc->get( $key );
1896 if ( strval( $newtalk ) !== '' ) {
1897 $this->mNewtalk = (bool)$newtalk;
1898 } else {
1899 // Since we are caching this, make sure it is up to date by getting it
1900 // from the master
1901 $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName(), true );
1902 $wgMemc->set( $key, (int)$this->mNewtalk, 1800 );
1903 }
1904 }
1905 } else {
1906 $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
1907 }
1908 }
1909
1910 return (bool)$this->mNewtalk;
1911 }
1912
1913 /**
1914 * Return the data needed to construct links for new talk page message
1915 * alerts. If there are new messages, this will return an associative array
1916 * with the following data:
1917 * wiki: The database name of the wiki
1918 * link: Root-relative link to the user's talk page
1919 * rev: The last talk page revision that the user has seen or null. This
1920 * is useful for building diff links.
1921 * If there are no new messages, it returns an empty array.
1922 * @note This function was designed to accomodate multiple talk pages, but
1923 * currently only returns a single link and revision.
1924 * @return Array
1925 */
1926 public function getNewMessageLinks() {
1927 $talks = array();
1928 if ( !wfRunHooks( 'UserRetrieveNewTalks', array( &$this, &$talks ) ) ) {
1929 return $talks;
1930 } elseif ( !$this->getNewtalk() ) {
1931 return array();
1932 }
1933 $utp = $this->getTalkPage();
1934 $dbr = wfGetDB( DB_SLAVE );
1935 // Get the "last viewed rev" timestamp from the oldest message notification
1936 $timestamp = $dbr->selectField( 'user_newtalk',
1937 'MIN(user_last_timestamp)',
1938 $this->isAnon() ? array( 'user_ip' => $this->getName() ) : array( 'user_id' => $this->getID() ),
1939 __METHOD__ );
1940 $rev = $timestamp ? Revision::loadFromTimestamp( $dbr, $utp, $timestamp ) : null;
1941 return array( array( 'wiki' => wfWikiID(), 'link' => $utp->getLocalURL(), 'rev' => $rev ) );
1942 }
1943
1944 /**
1945 * Get the revision ID for the last talk page revision viewed by the talk
1946 * page owner.
1947 * @return int|null Revision ID or null
1948 */
1949 public function getNewMessageRevisionId() {
1950 $newMessageRevisionId = null;
1951 $newMessageLinks = $this->getNewMessageLinks();
1952 if ( $newMessageLinks ) {
1953 // Note: getNewMessageLinks() never returns more than a single link
1954 // and it is always for the same wiki, but we double-check here in
1955 // case that changes some time in the future.
1956 if ( count( $newMessageLinks ) === 1
1957 && $newMessageLinks[0]['wiki'] === wfWikiID()
1958 && $newMessageLinks[0]['rev']
1959 ) {
1960 $newMessageRevision = $newMessageLinks[0]['rev'];
1961 $newMessageRevisionId = $newMessageRevision->getId();
1962 }
1963 }
1964 return $newMessageRevisionId;
1965 }
1966
1967 /**
1968 * Internal uncached check for new messages
1969 *
1970 * @see getNewtalk()
1971 * @param string $field 'user_ip' for anonymous users, 'user_id' otherwise
1972 * @param string|int $id User's IP address for anonymous users, User ID otherwise
1973 * @param bool $fromMaster true to fetch from the master, false for a slave
1974 * @return bool True if the user has new messages
1975 */
1976 protected function checkNewtalk( $field, $id, $fromMaster = false ) {
1977 if ( $fromMaster ) {
1978 $db = wfGetDB( DB_MASTER );
1979 } else {
1980 $db = wfGetDB( DB_SLAVE );
1981 }
1982 $ok = $db->selectField( 'user_newtalk', $field,
1983 array( $field => $id ), __METHOD__ );
1984 return $ok !== false;
1985 }
1986
1987 /**
1988 * Add or update the new messages flag
1989 * @param string $field 'user_ip' for anonymous users, 'user_id' otherwise
1990 * @param string|int $id User's IP address for anonymous users, User ID otherwise
1991 * @param $curRev Revision new, as yet unseen revision of the user talk page. Ignored if null.
1992 * @return bool True if successful, false otherwise
1993 */
1994 protected function updateNewtalk( $field, $id, $curRev = null ) {
1995 // Get timestamp of the talk page revision prior to the current one
1996 $prevRev = $curRev ? $curRev->getPrevious() : false;
1997 $ts = $prevRev ? $prevRev->getTimestamp() : null;
1998 // Mark the user as having new messages since this revision
1999 $dbw = wfGetDB( DB_MASTER );
2000 $dbw->insert( 'user_newtalk',
2001 array( $field => $id, 'user_last_timestamp' => $dbw->timestampOrNull( $ts ) ),
2002 __METHOD__,
2003 'IGNORE' );
2004 if ( $dbw->affectedRows() ) {
2005 wfDebug( __METHOD__ . ": set on ($field, $id)\n" );
2006 return true;
2007 } else {
2008 wfDebug( __METHOD__ . " already set ($field, $id)\n" );
2009 return false;
2010 }
2011 }
2012
2013 /**
2014 * Clear the new messages flag for the given user
2015 * @param string $field 'user_ip' for anonymous users, 'user_id' otherwise
2016 * @param string|int $id User's IP address for anonymous users, User ID otherwise
2017 * @return bool True if successful, false otherwise
2018 */
2019 protected function deleteNewtalk( $field, $id ) {
2020 $dbw = wfGetDB( DB_MASTER );
2021 $dbw->delete( 'user_newtalk',
2022 array( $field => $id ),
2023 __METHOD__ );
2024 if ( $dbw->affectedRows() ) {
2025 wfDebug( __METHOD__ . ": killed on ($field, $id)\n" );
2026 return true;
2027 } else {
2028 wfDebug( __METHOD__ . ": already gone ($field, $id)\n" );
2029 return false;
2030 }
2031 }
2032
2033 /**
2034 * Update the 'You have new messages!' status.
2035 * @param bool $val Whether the user has new messages
2036 * @param $curRev Revision new, as yet unseen revision of the user talk page. Ignored if null or !$val.
2037 */
2038 public function setNewtalk( $val, $curRev = null ) {
2039 if ( wfReadOnly() ) {
2040 return;
2041 }
2042
2043 $this->load();
2044 $this->mNewtalk = $val;
2045
2046 if ( $this->isAnon() ) {
2047 $field = 'user_ip';
2048 $id = $this->getName();
2049 } else {
2050 $field = 'user_id';
2051 $id = $this->getId();
2052 }
2053 global $wgMemc;
2054
2055 if ( $val ) {
2056 $changed = $this->updateNewtalk( $field, $id, $curRev );
2057 } else {
2058 $changed = $this->deleteNewtalk( $field, $id );
2059 }
2060
2061 if ( $this->isAnon() ) {
2062 // Anons have a separate memcached space, since
2063 // user records aren't kept for them.
2064 $key = wfMemcKey( 'newtalk', 'ip', $id );
2065 $wgMemc->set( $key, $val ? 1 : 0, 1800 );
2066 }
2067 if ( $changed ) {
2068 $this->invalidateCache();
2069 }
2070 }
2071
2072 /**
2073 * Generate a current or new-future timestamp to be stored in the
2074 * user_touched field when we update things.
2075 * @return string Timestamp in TS_MW format
2076 */
2077 private static function newTouchedTimestamp() {
2078 global $wgClockSkewFudge;
2079 return wfTimestamp( TS_MW, time() + $wgClockSkewFudge );
2080 }
2081
2082 /**
2083 * Clear user data from memcached.
2084 * Use after applying fun updates to the database; caller's
2085 * responsibility to update user_touched if appropriate.
2086 *
2087 * Called implicitly from invalidateCache() and saveSettings().
2088 */
2089 private function clearSharedCache() {
2090 $this->load();
2091 if ( $this->mId ) {
2092 global $wgMemc;
2093 $wgMemc->delete( wfMemcKey( 'user', 'id', $this->mId ) );
2094 }
2095 }
2096
2097 /**
2098 * Immediately touch the user data cache for this account.
2099 * Updates user_touched field, and removes account data from memcached
2100 * for reload on the next hit.
2101 */
2102 public function invalidateCache() {
2103 if ( wfReadOnly() ) {
2104 return;
2105 }
2106 $this->load();
2107 if ( $this->mId ) {
2108 $this->mTouched = self::newTouchedTimestamp();
2109
2110 $dbw = wfGetDB( DB_MASTER );
2111 $userid = $this->mId;
2112 $touched = $this->mTouched;
2113 $method = __METHOD__;
2114 $dbw->onTransactionIdle( function() use ( $dbw, $userid, $touched, $method ) {
2115 // Prevent contention slams by checking user_touched first
2116 $encTouched = $dbw->addQuotes( $dbw->timestamp( $touched ) );
2117 $needsPurge = $dbw->selectField( 'user', '1',
2118 array( 'user_id' => $userid, 'user_touched < ' . $encTouched ) );
2119 if ( $needsPurge ) {
2120 $dbw->update( 'user',
2121 array( 'user_touched' => $dbw->timestamp( $touched ) ),
2122 array( 'user_id' => $userid, 'user_touched < ' . $encTouched ),
2123 $method
2124 );
2125 }
2126 } );
2127 $this->clearSharedCache();
2128 }
2129 }
2130
2131 /**
2132 * Validate the cache for this account.
2133 * @param string $timestamp A timestamp in TS_MW format
2134 * @return bool
2135 */
2136 public function validateCache( $timestamp ) {
2137 $this->load();
2138 return ( $timestamp >= $this->mTouched );
2139 }
2140
2141 /**
2142 * Get the user touched timestamp
2143 * @return string timestamp
2144 */
2145 public function getTouched() {
2146 $this->load();
2147 return $this->mTouched;
2148 }
2149
2150 /**
2151 * Set the password and reset the random token.
2152 * Calls through to authentication plugin if necessary;
2153 * will have no effect if the auth plugin refuses to
2154 * pass the change through or if the legal password
2155 * checks fail.
2156 *
2157 * As a special case, setting the password to null
2158 * wipes it, so the account cannot be logged in until
2159 * a new password is set, for instance via e-mail.
2160 *
2161 * @param string $str New password to set
2162 * @throws PasswordError on failure
2163 *
2164 * @return bool
2165 */
2166 public function setPassword( $str ) {
2167 global $wgAuth;
2168
2169 if ( $str !== null ) {
2170 if ( !$wgAuth->allowPasswordChange() ) {
2171 throw new PasswordError( wfMessage( 'password-change-forbidden' )->text() );
2172 }
2173
2174 if ( !$this->isValidPassword( $str ) ) {
2175 global $wgMinimalPasswordLength;
2176 $valid = $this->getPasswordValidity( $str );
2177 if ( is_array( $valid ) ) {
2178 $message = array_shift( $valid );
2179 $params = $valid;
2180 } else {
2181 $message = $valid;
2182 $params = array( $wgMinimalPasswordLength );
2183 }
2184 throw new PasswordError( wfMessage( $message, $params )->text() );
2185 }
2186 }
2187
2188 if ( !$wgAuth->setPassword( $this, $str ) ) {
2189 throw new PasswordError( wfMessage( 'externaldberror' )->text() );
2190 }
2191
2192 $this->setInternalPassword( $str );
2193
2194 return true;
2195 }
2196
2197 /**
2198 * Set the password and reset the random token unconditionally.
2199 *
2200 * @param string|null $str New password to set or null to set an invalid
2201 * password hash meaning that the user will not be able to log in
2202 * through the web interface.
2203 */
2204 public function setInternalPassword( $str ) {
2205 $this->load();
2206 $this->setToken();
2207
2208 if ( $str === null ) {
2209 // Save an invalid hash...
2210 $this->mPassword = '';
2211 } else {
2212 $this->mPassword = self::crypt( $str );
2213 }
2214 $this->mNewpassword = '';
2215 $this->mNewpassTime = null;
2216 }
2217
2218 /**
2219 * Get the user's current token.
2220 * @param bool $forceCreation Force the generation of a new token if the user doesn't have one (default=true for backwards compatibility)
2221 * @return string Token
2222 */
2223 public function getToken( $forceCreation = true ) {
2224 $this->load();
2225 if ( !$this->mToken && $forceCreation ) {
2226 $this->setToken();
2227 }
2228 return $this->mToken;
2229 }
2230
2231 /**
2232 * Set the random token (used for persistent authentication)
2233 * Called from loadDefaults() among other places.
2234 *
2235 * @param string|bool $token If specified, set the token to this value
2236 */
2237 public function setToken( $token = false ) {
2238 $this->load();
2239 if ( !$token ) {
2240 $this->mToken = MWCryptRand::generateHex( USER_TOKEN_LENGTH );
2241 } else {
2242 $this->mToken = $token;
2243 }
2244 }
2245
2246 /**
2247 * Set the password for a password reminder or new account email
2248 *
2249 * @param $str New password to set or null to set an invalid
2250 * password hash meaning that the user will not be able to use it
2251 * @param bool $throttle If true, reset the throttle timestamp to the present
2252 */
2253 public function setNewpassword( $str, $throttle = true ) {
2254 $this->load();
2255
2256 if ( $str === null ) {
2257 $this->mNewpassword = '';
2258 $this->mNewpassTime = null;
2259 } else {
2260 $this->mNewpassword = self::crypt( $str );
2261 if ( $throttle ) {
2262 $this->mNewpassTime = wfTimestampNow();
2263 }
2264 }
2265 }
2266
2267 /**
2268 * Has password reminder email been sent within the last
2269 * $wgPasswordReminderResendTime hours?
2270 * @return bool
2271 */
2272 public function isPasswordReminderThrottled() {
2273 global $wgPasswordReminderResendTime;
2274 $this->load();
2275 if ( !$this->mNewpassTime || !$wgPasswordReminderResendTime ) {
2276 return false;
2277 }
2278 $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgPasswordReminderResendTime * 3600;
2279 return time() < $expiry;
2280 }
2281
2282 /**
2283 * Get the user's e-mail address
2284 * @return string User's email address
2285 */
2286 public function getEmail() {
2287 $this->load();
2288 wfRunHooks( 'UserGetEmail', array( $this, &$this->mEmail ) );
2289 return $this->mEmail;
2290 }
2291
2292 /**
2293 * Get the timestamp of the user's e-mail authentication
2294 * @return string TS_MW timestamp
2295 */
2296 public function getEmailAuthenticationTimestamp() {
2297 $this->load();
2298 wfRunHooks( 'UserGetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
2299 return $this->mEmailAuthenticated;
2300 }
2301
2302 /**
2303 * Set the user's e-mail address
2304 * @param string $str New e-mail address
2305 */
2306 public function setEmail( $str ) {
2307 $this->load();
2308 if ( $str == $this->mEmail ) {
2309 return;
2310 }
2311 $this->mEmail = $str;
2312 $this->invalidateEmail();
2313 wfRunHooks( 'UserSetEmail', array( $this, &$this->mEmail ) );
2314 }
2315
2316 /**
2317 * Set the user's e-mail address and a confirmation mail if needed.
2318 *
2319 * @since 1.20
2320 * @param string $str New e-mail address
2321 * @return Status
2322 */
2323 public function setEmailWithConfirmation( $str ) {
2324 global $wgEnableEmail, $wgEmailAuthentication;
2325
2326 if ( !$wgEnableEmail ) {
2327 return Status::newFatal( 'emaildisabled' );
2328 }
2329
2330 $oldaddr = $this->getEmail();
2331 if ( $str === $oldaddr ) {
2332 return Status::newGood( true );
2333 }
2334
2335 $this->setEmail( $str );
2336
2337 if ( $str !== '' && $wgEmailAuthentication ) {
2338 // Send a confirmation request to the new address if needed
2339 $type = $oldaddr != '' ? 'changed' : 'set';
2340 $result = $this->sendConfirmationMail( $type );
2341 if ( $result->isGood() ) {
2342 // Say the the caller that a confirmation mail has been sent
2343 $result->value = 'eauth';
2344 }
2345 } else {
2346 $result = Status::newGood( true );
2347 }
2348
2349 return $result;
2350 }
2351
2352 /**
2353 * Get the user's real name
2354 * @return string User's real name
2355 */
2356 public function getRealName() {
2357 if ( !$this->isItemLoaded( 'realname' ) ) {
2358 $this->load();
2359 }
2360
2361 return $this->mRealName;
2362 }
2363
2364 /**
2365 * Set the user's real name
2366 * @param string $str New real name
2367 */
2368 public function setRealName( $str ) {
2369 $this->load();
2370 $this->mRealName = $str;
2371 }
2372
2373 /**
2374 * Get the user's current setting for a given option.
2375 *
2376 * @param string $oname The option to check
2377 * @param string $defaultOverride A default value returned if the option does not exist
2378 * @param bool $ignoreHidden Whether to ignore the effects of $wgHiddenPrefs
2379 * @return string User's current value for the option
2380 * @see getBoolOption()
2381 * @see getIntOption()
2382 */
2383 public function getOption( $oname, $defaultOverride = null, $ignoreHidden = false ) {
2384 global $wgHiddenPrefs;
2385 $this->loadOptions();
2386
2387 # We want 'disabled' preferences to always behave as the default value for
2388 # users, even if they have set the option explicitly in their settings (ie they
2389 # set it, and then it was disabled removing their ability to change it). But
2390 # we don't want to erase the preferences in the database in case the preference
2391 # is re-enabled again. So don't touch $mOptions, just override the returned value
2392 if ( !$ignoreHidden && in_array( $oname, $wgHiddenPrefs ) ) {
2393 return self::getDefaultOption( $oname );
2394 }
2395
2396 if ( array_key_exists( $oname, $this->mOptions ) ) {
2397 return $this->mOptions[$oname];
2398 } else {
2399 return $defaultOverride;
2400 }
2401 }
2402
2403 /**
2404 * Get all user's options
2405 *
2406 * @return array
2407 */
2408 public function getOptions() {
2409 global $wgHiddenPrefs;
2410 $this->loadOptions();
2411 $options = $this->mOptions;
2412
2413 # We want 'disabled' preferences to always behave as the default value for
2414 # users, even if they have set the option explicitly in their settings (ie they
2415 # set it, and then it was disabled removing their ability to change it). But
2416 # we don't want to erase the preferences in the database in case the preference
2417 # is re-enabled again. So don't touch $mOptions, just override the returned value
2418 foreach ( $wgHiddenPrefs as $pref ) {
2419 $default = self::getDefaultOption( $pref );
2420 if ( $default !== null ) {
2421 $options[$pref] = $default;
2422 }
2423 }
2424
2425 return $options;
2426 }
2427
2428 /**
2429 * Get the user's current setting for a given option, as a boolean value.
2430 *
2431 * @param string $oname The option to check
2432 * @return bool User's current value for the option
2433 * @see getOption()
2434 */
2435 public function getBoolOption( $oname ) {
2436 return (bool)$this->getOption( $oname );
2437 }
2438
2439 /**
2440 * Get the user's current setting for a given option, as an integer value.
2441 *
2442 * @param string $oname The option to check
2443 * @param int $defaultOverride A default value returned if the option does not exist
2444 * @return int User's current value for the option
2445 * @see getOption()
2446 */
2447 public function getIntOption( $oname, $defaultOverride = 0 ) {
2448 $val = $this->getOption( $oname );
2449 if ( $val == '' ) {
2450 $val = $defaultOverride;
2451 }
2452 return intval( $val );
2453 }
2454
2455 /**
2456 * Set the given option for a user.
2457 *
2458 * @param string $oname The option to set
2459 * @param mixed $val New value to set
2460 */
2461 public function setOption( $oname, $val ) {
2462 $this->loadOptions();
2463
2464 // Explicitly NULL values should refer to defaults
2465 if ( is_null( $val ) ) {
2466 $val = self::getDefaultOption( $oname );
2467 }
2468
2469 $this->mOptions[$oname] = $val;
2470 }
2471
2472 /**
2473 * Get a token stored in the preferences (like the watchlist one),
2474 * resetting it if it's empty (and saving changes).
2475 *
2476 * @param string $oname The option name to retrieve the token from
2477 * @return string|bool User's current value for the option, or false if this option is disabled.
2478 * @see resetTokenFromOption()
2479 * @see getOption()
2480 */
2481 public function getTokenFromOption( $oname ) {
2482 global $wgHiddenPrefs;
2483 if ( in_array( $oname, $wgHiddenPrefs ) ) {
2484 return false;
2485 }
2486
2487 $token = $this->getOption( $oname );
2488 if ( !$token ) {
2489 $token = $this->resetTokenFromOption( $oname );
2490 $this->saveSettings();
2491 }
2492 return $token;
2493 }
2494
2495 /**
2496 * Reset a token stored in the preferences (like the watchlist one).
2497 * *Does not* save user's preferences (similarly to setOption()).
2498 *
2499 * @param string $oname The option name to reset the token in
2500 * @return string|bool New token value, or false if this option is disabled.
2501 * @see getTokenFromOption()
2502 * @see setOption()
2503 */
2504 public function resetTokenFromOption( $oname ) {
2505 global $wgHiddenPrefs;
2506 if ( in_array( $oname, $wgHiddenPrefs ) ) {
2507 return false;
2508 }
2509
2510 $token = MWCryptRand::generateHex( 40 );
2511 $this->setOption( $oname, $token );
2512 return $token;
2513 }
2514
2515 /**
2516 * Return a list of the types of user options currently returned by
2517 * User::getOptionKinds().
2518 *
2519 * Currently, the option kinds are:
2520 * - 'registered' - preferences which are registered in core MediaWiki or
2521 * by extensions using the UserGetDefaultOptions hook.
2522 * - 'registered-multiselect' - as above, using the 'multiselect' type.
2523 * - 'registered-checkmatrix' - as above, using the 'checkmatrix' type.
2524 * - 'userjs' - preferences with names starting with 'userjs-', intended to
2525 * be used by user scripts.
2526 * - 'special' - "preferences" that are not accessible via User::getOptions
2527 * or User::setOptions.
2528 * - 'unused' - preferences about which MediaWiki doesn't know anything.
2529 * These are usually legacy options, removed in newer versions.
2530 *
2531 * The API (and possibly others) use this function to determine the possible
2532 * option types for validation purposes, so make sure to update this when a
2533 * new option kind is added.
2534 *
2535 * @see User::getOptionKinds
2536 * @return array Option kinds
2537 */
2538 public static function listOptionKinds() {
2539 return array(
2540 'registered',
2541 'registered-multiselect',
2542 'registered-checkmatrix',
2543 'userjs',
2544 'special',
2545 'unused'
2546 );
2547 }
2548
2549 /**
2550 * Return an associative array mapping preferences keys to the kind of a preference they're
2551 * used for. Different kinds are handled differently when setting or reading preferences.
2552 *
2553 * See User::listOptionKinds for the list of valid option types that can be provided.
2554 *
2555 * @see User::listOptionKinds
2556 * @param $context IContextSource
2557 * @param array $options assoc. array with options keys to check as keys. Defaults to $this->mOptions.
2558 * @return array the key => kind mapping data
2559 */
2560 public function getOptionKinds( IContextSource $context, $options = null ) {
2561 $this->loadOptions();
2562 if ( $options === null ) {
2563 $options = $this->mOptions;
2564 }
2565
2566 $prefs = Preferences::getPreferences( $this, $context );
2567 $mapping = array();
2568
2569 // Pull out the "special" options, so they don't get converted as
2570 // multiselect or checkmatrix.
2571 $specialOptions = array_fill_keys( Preferences::getSaveBlacklist(), true );
2572 foreach ( $specialOptions as $name => $value ) {
2573 unset( $prefs[$name] );
2574 }
2575
2576 // Multiselect and checkmatrix options are stored in the database with
2577 // one key per option, each having a boolean value. Extract those keys.
2578 $multiselectOptions = array();
2579 foreach ( $prefs as $name => $info ) {
2580 if ( ( isset( $info['type'] ) && $info['type'] == 'multiselect' ) ||
2581 ( isset( $info['class'] ) && $info['class'] == 'HTMLMultiSelectField' ) ) {
2582 $opts = HTMLFormField::flattenOptions( $info['options'] );
2583 $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name;
2584
2585 foreach ( $opts as $value ) {
2586 $multiselectOptions["$prefix$value"] = true;
2587 }
2588
2589 unset( $prefs[$name] );
2590 }
2591 }
2592 $checkmatrixOptions = array();
2593 foreach ( $prefs as $name => $info ) {
2594 if ( ( isset( $info['type'] ) && $info['type'] == 'checkmatrix' ) ||
2595 ( isset( $info['class'] ) && $info['class'] == 'HTMLCheckMatrix' ) ) {
2596 $columns = HTMLFormField::flattenOptions( $info['columns'] );
2597 $rows = HTMLFormField::flattenOptions( $info['rows'] );
2598 $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name;
2599
2600 foreach ( $columns as $column ) {
2601 foreach ( $rows as $row ) {
2602 $checkmatrixOptions["$prefix-$column-$row"] = true;
2603 }
2604 }
2605
2606 unset( $prefs[$name] );
2607 }
2608 }
2609
2610 // $value is ignored
2611 foreach ( $options as $key => $value ) {
2612 if ( isset( $prefs[$key] ) ) {
2613 $mapping[$key] = 'registered';
2614 } elseif ( isset( $multiselectOptions[$key] ) ) {
2615 $mapping[$key] = 'registered-multiselect';
2616 } elseif ( isset( $checkmatrixOptions[$key] ) ) {
2617 $mapping[$key] = 'registered-checkmatrix';
2618 } elseif ( isset( $specialOptions[$key] ) ) {
2619 $mapping[$key] = 'special';
2620 } elseif ( substr( $key, 0, 7 ) === 'userjs-' ) {
2621 $mapping[$key] = 'userjs';
2622 } else {
2623 $mapping[$key] = 'unused';
2624 }
2625 }
2626
2627 return $mapping;
2628 }
2629
2630 /**
2631 * Reset certain (or all) options to the site defaults
2632 *
2633 * The optional parameter determines which kinds of preferences will be reset.
2634 * Supported values are everything that can be reported by getOptionKinds()
2635 * and 'all', which forces a reset of *all* preferences and overrides everything else.
2636 *
2637 * @param array|string $resetKinds which kinds of preferences to reset. Defaults to
2638 * array( 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused' )
2639 * for backwards-compatibility.
2640 * @param $context IContextSource|null context source used when $resetKinds
2641 * does not contain 'all', passed to getOptionKinds().
2642 * Defaults to RequestContext::getMain() when null.
2643 */
2644 public function resetOptions(
2645 $resetKinds = array( 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused' ),
2646 IContextSource $context = null
2647 ) {
2648 $this->load();
2649 $defaultOptions = self::getDefaultOptions();
2650
2651 if ( !is_array( $resetKinds ) ) {
2652 $resetKinds = array( $resetKinds );
2653 }
2654
2655 if ( in_array( 'all', $resetKinds ) ) {
2656 $newOptions = $defaultOptions;
2657 } else {
2658 if ( $context === null ) {
2659 $context = RequestContext::getMain();
2660 }
2661
2662 $optionKinds = $this->getOptionKinds( $context );
2663 $resetKinds = array_intersect( $resetKinds, self::listOptionKinds() );
2664 $newOptions = array();
2665
2666 // Use default values for the options that should be deleted, and
2667 // copy old values for the ones that shouldn't.
2668 foreach ( $this->mOptions as $key => $value ) {
2669 if ( in_array( $optionKinds[$key], $resetKinds ) ) {
2670 if ( array_key_exists( $key, $defaultOptions ) ) {
2671 $newOptions[$key] = $defaultOptions[$key];
2672 }
2673 } else {
2674 $newOptions[$key] = $value;
2675 }
2676 }
2677 }
2678
2679 $this->mOptions = $newOptions;
2680 $this->mOptionsLoaded = true;
2681 }
2682
2683 /**
2684 * Get the user's preferred date format.
2685 * @return string User's preferred date format
2686 */
2687 public function getDatePreference() {
2688 // Important migration for old data rows
2689 if ( is_null( $this->mDatePreference ) ) {
2690 global $wgLang;
2691 $value = $this->getOption( 'date' );
2692 $map = $wgLang->getDatePreferenceMigrationMap();
2693 if ( isset( $map[$value] ) ) {
2694 $value = $map[$value];
2695 }
2696 $this->mDatePreference = $value;
2697 }
2698 return $this->mDatePreference;
2699 }
2700
2701 /**
2702 * Determine based on the wiki configuration and the user's options,
2703 * whether this user must be over HTTPS no matter what.
2704 *
2705 * @return bool
2706 */
2707 public function requiresHTTPS() {
2708 global $wgSecureLogin;
2709 if ( !$wgSecureLogin ) {
2710 return false;
2711 } else {
2712 $https = $this->getBoolOption( 'prefershttps' );
2713 wfRunHooks( 'UserRequiresHTTPS', array( $this, &$https ) );
2714 if ( $https ) {
2715 $https = wfCanIPUseHTTPS( $this->getRequest()->getIP() );
2716 }
2717 return $https;
2718 }
2719 }
2720
2721 /**
2722 * Get the user preferred stub threshold
2723 *
2724 * @return int
2725 */
2726 public function getStubThreshold() {
2727 global $wgMaxArticleSize; # Maximum article size, in Kb
2728 $threshold = $this->getIntOption( 'stubthreshold' );
2729 if ( $threshold > $wgMaxArticleSize * 1024 ) {
2730 // If they have set an impossible value, disable the preference
2731 // so we can use the parser cache again.
2732 $threshold = 0;
2733 }
2734 return $threshold;
2735 }
2736
2737 /**
2738 * Get the permissions this user has.
2739 * @return Array of String permission names
2740 */
2741 public function getRights() {
2742 if ( is_null( $this->mRights ) ) {
2743 $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() );
2744 wfRunHooks( 'UserGetRights', array( $this, &$this->mRights ) );
2745 // Force reindexation of rights when a hook has unset one of them
2746 $this->mRights = array_values( array_unique( $this->mRights ) );
2747 }
2748 return $this->mRights;
2749 }
2750
2751 /**
2752 * Get the list of explicit group memberships this user has.
2753 * The implicit * and user groups are not included.
2754 * @return Array of String internal group names
2755 */
2756 public function getGroups() {
2757 $this->load();
2758 $this->loadGroups();
2759 return $this->mGroups;
2760 }
2761
2762 /**
2763 * Get the list of implicit group memberships this user has.
2764 * This includes all explicit groups, plus 'user' if logged in,
2765 * '*' for all accounts, and autopromoted groups
2766 * @param bool $recache Whether to avoid the cache
2767 * @return Array of String internal group names
2768 */
2769 public function getEffectiveGroups( $recache = false ) {
2770 if ( $recache || is_null( $this->mEffectiveGroups ) ) {
2771 wfProfileIn( __METHOD__ );
2772 $this->mEffectiveGroups = array_unique( array_merge(
2773 $this->getGroups(), // explicit groups
2774 $this->getAutomaticGroups( $recache ) // implicit groups
2775 ) );
2776 // Hook for additional groups
2777 wfRunHooks( 'UserEffectiveGroups', array( &$this, &$this->mEffectiveGroups ) );
2778 // Force reindexation of groups when a hook has unset one of them
2779 $this->mEffectiveGroups = array_values( array_unique( $this->mEffectiveGroups ) );
2780 wfProfileOut( __METHOD__ );
2781 }
2782 return $this->mEffectiveGroups;
2783 }
2784
2785 /**
2786 * Get the list of implicit group memberships this user has.
2787 * This includes 'user' if logged in, '*' for all accounts,
2788 * and autopromoted groups
2789 * @param bool $recache Whether to avoid the cache
2790 * @return Array of String internal group names
2791 */
2792 public function getAutomaticGroups( $recache = false ) {
2793 if ( $recache || is_null( $this->mImplicitGroups ) ) {
2794 wfProfileIn( __METHOD__ );
2795 $this->mImplicitGroups = array( '*' );
2796 if ( $this->getId() ) {
2797 $this->mImplicitGroups[] = 'user';
2798
2799 $this->mImplicitGroups = array_unique( array_merge(
2800 $this->mImplicitGroups,
2801 Autopromote::getAutopromoteGroups( $this )
2802 ) );
2803 }
2804 if ( $recache ) {
2805 // Assure data consistency with rights/groups,
2806 // as getEffectiveGroups() depends on this function
2807 $this->mEffectiveGroups = null;
2808 }
2809 wfProfileOut( __METHOD__ );
2810 }
2811 return $this->mImplicitGroups;
2812 }
2813
2814 /**
2815 * Returns the groups the user has belonged to.
2816 *
2817 * The user may still belong to the returned groups. Compare with getGroups().
2818 *
2819 * The function will not return groups the user had belonged to before MW 1.17
2820 *
2821 * @return array Names of the groups the user has belonged to.
2822 */
2823 public function getFormerGroups() {
2824 if ( is_null( $this->mFormerGroups ) ) {
2825 $dbr = wfGetDB( DB_MASTER );
2826 $res = $dbr->select( 'user_former_groups',
2827 array( 'ufg_group' ),
2828 array( 'ufg_user' => $this->mId ),
2829 __METHOD__ );
2830 $this->mFormerGroups = array();
2831 foreach ( $res as $row ) {
2832 $this->mFormerGroups[] = $row->ufg_group;
2833 }
2834 }
2835 return $this->mFormerGroups;
2836 }
2837
2838 /**
2839 * Get the user's edit count.
2840 * @return int, null for anonymous users
2841 */
2842 public function getEditCount() {
2843 if ( !$this->getId() ) {
2844 return null;
2845 }
2846
2847 if ( !isset( $this->mEditCount ) ) {
2848 /* Populate the count, if it has not been populated yet */
2849 wfProfileIn( __METHOD__ );
2850 $dbr = wfGetDB( DB_SLAVE );
2851 // check if the user_editcount field has been initialized
2852 $count = $dbr->selectField(
2853 'user', 'user_editcount',
2854 array( 'user_id' => $this->mId ),
2855 __METHOD__
2856 );
2857
2858 if ( $count === null ) {
2859 // it has not been initialized. do so.
2860 $count = $this->initEditCount();
2861 }
2862 $this->mEditCount = $count;
2863 wfProfileOut( __METHOD__ );
2864 }
2865 return (int)$this->mEditCount;
2866 }
2867
2868 /**
2869 * Add the user to the given group.
2870 * This takes immediate effect.
2871 * @param string $group Name of the group to add
2872 */
2873 public function addGroup( $group ) {
2874 if ( wfRunHooks( 'UserAddGroup', array( $this, &$group ) ) ) {
2875 $dbw = wfGetDB( DB_MASTER );
2876 if ( $this->getId() ) {
2877 $dbw->insert( 'user_groups',
2878 array(
2879 'ug_user' => $this->getID(),
2880 'ug_group' => $group,
2881 ),
2882 __METHOD__,
2883 array( 'IGNORE' ) );
2884 }
2885 }
2886 $this->loadGroups();
2887 $this->mGroups[] = $group;
2888 // In case loadGroups was not called before, we now have the right twice.
2889 // Get rid of the duplicate.
2890 $this->mGroups = array_unique( $this->mGroups );
2891
2892 // Refresh the groups caches, and clear the rights cache so it will be
2893 // refreshed on the next call to $this->getRights().
2894 $this->getEffectiveGroups( true );
2895 $this->mRights = null;
2896
2897 $this->invalidateCache();
2898 }
2899
2900 /**
2901 * Remove the user from the given group.
2902 * This takes immediate effect.
2903 * @param string $group Name of the group to remove
2904 */
2905 public function removeGroup( $group ) {
2906 $this->load();
2907 if ( wfRunHooks( 'UserRemoveGroup', array( $this, &$group ) ) ) {
2908 $dbw = wfGetDB( DB_MASTER );
2909 $dbw->delete( 'user_groups',
2910 array(
2911 'ug_user' => $this->getID(),
2912 'ug_group' => $group,
2913 ), __METHOD__ );
2914 // Remember that the user was in this group
2915 $dbw->insert( 'user_former_groups',
2916 array(
2917 'ufg_user' => $this->getID(),
2918 'ufg_group' => $group,
2919 ),
2920 __METHOD__,
2921 array( 'IGNORE' ) );
2922 }
2923 $this->loadGroups();
2924 $this->mGroups = array_diff( $this->mGroups, array( $group ) );
2925
2926 // Refresh the groups caches, and clear the rights cache so it will be
2927 // refreshed on the next call to $this->getRights().
2928 $this->getEffectiveGroups( true );
2929 $this->mRights = null;
2930
2931 $this->invalidateCache();
2932 }
2933
2934 /**
2935 * Get whether the user is logged in
2936 * @return bool
2937 */
2938 public function isLoggedIn() {
2939 return $this->getID() != 0;
2940 }
2941
2942 /**
2943 * Get whether the user is anonymous
2944 * @return bool
2945 */
2946 public function isAnon() {
2947 return !$this->isLoggedIn();
2948 }
2949
2950 /**
2951 * Check if user is allowed to access a feature / make an action
2952 *
2953 * @internal param \String $varargs permissions to test
2954 * @return boolean: True if user is allowed to perform *any* of the given actions
2955 *
2956 * @return bool
2957 */
2958 public function isAllowedAny( /*...*/ ) {
2959 $permissions = func_get_args();
2960 foreach ( $permissions as $permission ) {
2961 if ( $this->isAllowed( $permission ) ) {
2962 return true;
2963 }
2964 }
2965 return false;
2966 }
2967
2968 /**
2969 *
2970 * @internal param $varargs string
2971 * @return bool True if the user is allowed to perform *all* of the given actions
2972 */
2973 public function isAllowedAll( /*...*/ ) {
2974 $permissions = func_get_args();
2975 foreach ( $permissions as $permission ) {
2976 if ( !$this->isAllowed( $permission ) ) {
2977 return false;
2978 }
2979 }
2980 return true;
2981 }
2982
2983 /**
2984 * Internal mechanics of testing a permission
2985 * @param string $action
2986 * @return bool
2987 */
2988 public function isAllowed( $action = '' ) {
2989 if ( $action === '' ) {
2990 return true; // In the spirit of DWIM
2991 }
2992 // Patrolling may not be enabled
2993 if ( $action === 'patrol' || $action === 'autopatrol' ) {
2994 global $wgUseRCPatrol, $wgUseNPPatrol;
2995 if ( !$wgUseRCPatrol && !$wgUseNPPatrol ) {
2996 return false;
2997 }
2998 }
2999 // Use strict parameter to avoid matching numeric 0 accidentally inserted
3000 // by misconfiguration: 0 == 'foo'
3001 return in_array( $action, $this->getRights(), true );
3002 }
3003
3004 /**
3005 * Check whether to enable recent changes patrol features for this user
3006 * @return boolean: True or false
3007 */
3008 public function useRCPatrol() {
3009 global $wgUseRCPatrol;
3010 return $wgUseRCPatrol && $this->isAllowedAny( 'patrol', 'patrolmarks' );
3011 }
3012
3013 /**
3014 * Check whether to enable new pages patrol features for this user
3015 * @return bool True or false
3016 */
3017 public function useNPPatrol() {
3018 global $wgUseRCPatrol, $wgUseNPPatrol;
3019 return (
3020 ( $wgUseRCPatrol || $wgUseNPPatrol )
3021 && ( $this->isAllowedAny( 'patrol', 'patrolmarks' ) )
3022 );
3023 }
3024
3025 /**
3026 * Get the WebRequest object to use with this object
3027 *
3028 * @return WebRequest
3029 */
3030 public function getRequest() {
3031 if ( $this->mRequest ) {
3032 return $this->mRequest;
3033 } else {
3034 global $wgRequest;
3035 return $wgRequest;
3036 }
3037 }
3038
3039 /**
3040 * Get the current skin, loading it if required
3041 * @return Skin The current skin
3042 * @todo FIXME: Need to check the old failback system [AV]
3043 * @deprecated since 1.18 Use ->getSkin() in the most relevant outputting context you have
3044 */
3045 public function getSkin() {
3046 wfDeprecated( __METHOD__, '1.18' );
3047 return RequestContext::getMain()->getSkin();
3048 }
3049
3050 /**
3051 * Get a WatchedItem for this user and $title.
3052 *
3053 * @since 1.22 $checkRights parameter added
3054 * @param $title Title
3055 * @param $checkRights int Whether to check 'viewmywatchlist'/'editmywatchlist' rights.
3056 * Pass WatchedItem::CHECK_USER_RIGHTS or WatchedItem::IGNORE_USER_RIGHTS.
3057 * @return WatchedItem
3058 */
3059 public function getWatchedItem( $title, $checkRights = WatchedItem::CHECK_USER_RIGHTS ) {
3060 $key = $checkRights . ':' . $title->getNamespace() . ':' . $title->getDBkey();
3061
3062 if ( isset( $this->mWatchedItems[$key] ) ) {
3063 return $this->mWatchedItems[$key];
3064 }
3065
3066 if ( count( $this->mWatchedItems ) >= self::MAX_WATCHED_ITEMS_CACHE ) {
3067 $this->mWatchedItems = array();
3068 }
3069
3070 $this->mWatchedItems[$key] = WatchedItem::fromUserTitle( $this, $title, $checkRights );
3071 return $this->mWatchedItems[$key];
3072 }
3073
3074 /**
3075 * Check the watched status of an article.
3076 * @since 1.22 $checkRights parameter added
3077 * @param $title Title of the article to look at
3078 * @param $checkRights int Whether to check 'viewmywatchlist'/'editmywatchlist' rights.
3079 * Pass WatchedItem::CHECK_USER_RIGHTS or WatchedItem::IGNORE_USER_RIGHTS.
3080 * @return bool
3081 */
3082 public function isWatched( $title, $checkRights = WatchedItem::CHECK_USER_RIGHTS ) {
3083 return $this->getWatchedItem( $title, $checkRights )->isWatched();
3084 }
3085
3086 /**
3087 * Watch an article.
3088 * @since 1.22 $checkRights parameter added
3089 * @param $title Title of the article to look at
3090 * @param $checkRights int Whether to check 'viewmywatchlist'/'editmywatchlist' rights.
3091 * Pass WatchedItem::CHECK_USER_RIGHTS or WatchedItem::IGNORE_USER_RIGHTS.
3092 */
3093 public function addWatch( $title, $checkRights = WatchedItem::CHECK_USER_RIGHTS ) {
3094 $this->getWatchedItem( $title, $checkRights )->addWatch();
3095 $this->invalidateCache();
3096 }
3097
3098 /**
3099 * Stop watching an article.
3100 * @since 1.22 $checkRights parameter added
3101 * @param $title Title of the article to look at
3102 * @param $checkRights int Whether to check 'viewmywatchlist'/'editmywatchlist' rights.
3103 * Pass WatchedItem::CHECK_USER_RIGHTS or WatchedItem::IGNORE_USER_RIGHTS.
3104 */
3105 public function removeWatch( $title, $checkRights = WatchedItem::CHECK_USER_RIGHTS ) {
3106 $this->getWatchedItem( $title, $checkRights )->removeWatch();
3107 $this->invalidateCache();
3108 }
3109
3110 /**
3111 * Clear the user's notification timestamp for the given title.
3112 * If e-notif e-mails are on, they will receive notification mails on
3113 * the next change of the page if it's watched etc.
3114 * @note If the user doesn't have 'editmywatchlist', this will do nothing.
3115 * @param $title Title of the article to look at
3116 * @param int $oldid The revision id being viewed. If not given or 0, latest revision is assumed.
3117 */
3118 public function clearNotification( &$title, $oldid = 0 ) {
3119 global $wgUseEnotif, $wgShowUpdatedMarker;
3120
3121 // Do nothing if the database is locked to writes
3122 if ( wfReadOnly() ) {
3123 return;
3124 }
3125
3126 // Do nothing if not allowed to edit the watchlist
3127 if ( !$this->isAllowed( 'editmywatchlist' ) ) {
3128 return;
3129 }
3130
3131 // If we're working on user's talk page, we should update the talk page message indicator
3132 if ( $title->getNamespace() == NS_USER_TALK && $title->getText() == $this->getName() ) {
3133 if ( !wfRunHooks( 'UserClearNewTalkNotification', array( &$this, $oldid ) ) ) {
3134 return;
3135 }
3136
3137 $nextid = $oldid ? $title->getNextRevisionID( $oldid ) : null;
3138
3139 if ( !$oldid || !$nextid ) {
3140 // If we're looking at the latest revision, we should definitely clear it
3141 $this->setNewtalk( false );
3142 } else {
3143 // Otherwise we should update its revision, if it's present
3144 if ( $this->getNewtalk() ) {
3145 // Naturally the other one won't clear by itself
3146 $this->setNewtalk( false );
3147 $this->setNewtalk( true, Revision::newFromId( $nextid ) );
3148 }
3149 }
3150 }
3151
3152 if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
3153 return;
3154 }
3155
3156 if ( $this->isAnon() ) {
3157 // Nothing else to do...
3158 return;
3159 }
3160
3161 // Only update the timestamp if the page is being watched.
3162 // The query to find out if it is watched is cached both in memcached and per-invocation,
3163 // and when it does have to be executed, it can be on a slave
3164 // If this is the user's newtalk page, we always update the timestamp
3165 $force = '';
3166 if ( $title->getNamespace() == NS_USER_TALK && $title->getText() == $this->getName() ) {
3167 $force = 'force';
3168 }
3169
3170 $this->getWatchedItem( $title )->resetNotificationTimestamp( $force, $oldid );
3171 }
3172
3173 /**
3174 * Resets all of the given user's page-change notification timestamps.
3175 * If e-notif e-mails are on, they will receive notification mails on
3176 * the next change of any watched page.
3177 * @note If the user doesn't have 'editmywatchlist', this will do nothing.
3178 */
3179 public function clearAllNotifications() {
3180 if ( wfReadOnly() ) {
3181 return;
3182 }
3183
3184 // Do nothing if not allowed to edit the watchlist
3185 if ( !$this->isAllowed( 'editmywatchlist' ) ) {
3186 return;
3187 }
3188
3189 global $wgUseEnotif, $wgShowUpdatedMarker;
3190 if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
3191 $this->setNewtalk( false );
3192 return;
3193 }
3194 $id = $this->getId();
3195 if ( $id != 0 ) {
3196 $dbw = wfGetDB( DB_MASTER );
3197 $dbw->update( 'watchlist',
3198 array( /* SET */ 'wl_notificationtimestamp' => null ),
3199 array( /* WHERE */ 'wl_user' => $id ),
3200 __METHOD__
3201 );
3202 // We also need to clear here the "you have new message" notification for the own user_talk page;
3203 // it's cleared one page view later in WikiPage::doViewUpdates().
3204 }
3205 }
3206
3207 /**
3208 * Set this user's options from an encoded string
3209 * @param string $str Encoded options to import
3210 *
3211 * @deprecated in 1.19 due to removal of user_options from the user table
3212 */
3213 private function decodeOptions( $str ) {
3214 wfDeprecated( __METHOD__, '1.19' );
3215 if ( !$str ) {
3216 return;
3217 }
3218
3219 $this->mOptionsLoaded = true;
3220 $this->mOptionOverrides = array();
3221
3222 // If an option is not set in $str, use the default value
3223 $this->mOptions = self::getDefaultOptions();
3224
3225 $a = explode( "\n", $str );
3226 foreach ( $a as $s ) {
3227 $m = array();
3228 if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
3229 $this->mOptions[$m[1]] = $m[2];
3230 $this->mOptionOverrides[$m[1]] = $m[2];
3231 }
3232 }
3233 }
3234
3235 /**
3236 * Set a cookie on the user's client. Wrapper for
3237 * WebResponse::setCookie
3238 * @param string $name Name of the cookie to set
3239 * @param string $value Value to set
3240 * @param int $exp Expiration time, as a UNIX time value;
3241 * if 0 or not specified, use the default $wgCookieExpiration
3242 * @param bool $secure
3243 * true: Force setting the secure attribute when setting the cookie
3244 * false: Force NOT setting the secure attribute when setting the cookie
3245 * null (default): Use the default ($wgCookieSecure) to set the secure attribute
3246 * @param array $params Array of options sent passed to WebResponse::setcookie()
3247 */
3248 protected function setCookie( $name, $value, $exp = 0, $secure = null, $params = array() ) {
3249 $params['secure'] = $secure;
3250 $this->getRequest()->response()->setcookie( $name, $value, $exp, $params );
3251 }
3252
3253 /**
3254 * Clear a cookie on the user's client
3255 * @param string $name Name of the cookie to clear
3256 * @param bool $secure
3257 * true: Force setting the secure attribute when setting the cookie
3258 * false: Force NOT setting the secure attribute when setting the cookie
3259 * null (default): Use the default ($wgCookieSecure) to set the secure attribute
3260 * @param array $params Array of options sent passed to WebResponse::setcookie()
3261 */
3262 protected function clearCookie( $name, $secure = null, $params = array() ) {
3263 $this->setCookie( $name, '', time() - 86400, $secure, $params );
3264 }
3265
3266 /**
3267 * Set the default cookies for this session on the user's client.
3268 *
3269 * @param $request WebRequest object to use; $wgRequest will be used if null
3270 * is passed.
3271 * @param bool $secure Whether to force secure/insecure cookies or use default
3272 */
3273 public function setCookies( $request = null, $secure = null ) {
3274 if ( $request === null ) {
3275 $request = $this->getRequest();
3276 }
3277
3278 $this->load();
3279 if ( 0 == $this->mId ) {
3280 return;
3281 }
3282 if ( !$this->mToken ) {
3283 // When token is empty or NULL generate a new one and then save it to the database
3284 // This allows a wiki to re-secure itself after a leak of it's user table or $wgSecretKey
3285 // Simply by setting every cell in the user_token column to NULL and letting them be
3286 // regenerated as users log back into the wiki.
3287 $this->setToken();
3288 $this->saveSettings();
3289 }
3290 $session = array(
3291 'wsUserID' => $this->mId,
3292 'wsToken' => $this->mToken,
3293 'wsUserName' => $this->getName()
3294 );
3295 $cookies = array(
3296 'UserID' => $this->mId,
3297 'UserName' => $this->getName(),
3298 );
3299 if ( 1 == $this->getOption( 'rememberpassword' ) ) {
3300 $cookies['Token'] = $this->mToken;
3301 } else {
3302 $cookies['Token'] = false;
3303 }
3304
3305 wfRunHooks( 'UserSetCookies', array( $this, &$session, &$cookies ) );
3306
3307 foreach ( $session as $name => $value ) {
3308 $request->setSessionData( $name, $value );
3309 }
3310 foreach ( $cookies as $name => $value ) {
3311 if ( $value === false ) {
3312 $this->clearCookie( $name );
3313 } else {
3314 $this->setCookie( $name, $value, 0, $secure );
3315 }
3316 }
3317
3318 /**
3319 * If wpStickHTTPS was selected, also set an insecure cookie that
3320 * will cause the site to redirect the user to HTTPS, if they access
3321 * it over HTTP. Bug 29898. Use an un-prefixed cookie, so it's the same
3322 * as the one set by centralauth (bug 53538). Also set it to session, or
3323 * standard time setting, based on if rememberme was set.
3324 */
3325 if ( $request->getCheck( 'wpStickHTTPS' ) || $this->requiresHTTPS() ) {
3326 $time = null;
3327 if ( ( 1 == $this->getOption( 'rememberpassword' ) ) ) {
3328 $time = 0; // set to $wgCookieExpiration
3329 }
3330 $this->setCookie(
3331 'forceHTTPS',
3332 'true',
3333 $time,
3334 false,
3335 array( 'prefix' => '' ) // no prefix
3336 );
3337 }
3338 }
3339
3340 /**
3341 * Log this user out.
3342 */
3343 public function logout() {
3344 if ( wfRunHooks( 'UserLogout', array( &$this ) ) ) {
3345 $this->doLogout();
3346 }
3347 }
3348
3349 /**
3350 * Clear the user's cookies and session, and reset the instance cache.
3351 * @see logout()
3352 */
3353 public function doLogout() {
3354 $this->clearInstanceCache( 'defaults' );
3355
3356 $this->getRequest()->setSessionData( 'wsUserID', 0 );
3357
3358 $this->clearCookie( 'UserID' );
3359 $this->clearCookie( 'Token' );
3360 $this->clearCookie( 'forceHTTPS', false, array( 'prefix' => '' ) );
3361
3362 // Remember when user logged out, to prevent seeing cached pages
3363 $this->setCookie( 'LoggedOut', time(), time() + 86400 );
3364 }
3365
3366 /**
3367 * Save this user's settings into the database.
3368 * @todo Only rarely do all these fields need to be set!
3369 */
3370 public function saveSettings() {
3371 global $wgAuth;
3372
3373 $this->load();
3374 if ( wfReadOnly() ) {
3375 return;
3376 }
3377 if ( 0 == $this->mId ) {
3378 return;
3379 }
3380
3381 $this->mTouched = self::newTouchedTimestamp();
3382 if ( !$wgAuth->allowSetLocalPassword() ) {
3383 $this->mPassword = '';
3384 }
3385
3386 $dbw = wfGetDB( DB_MASTER );
3387 $dbw->update( 'user',
3388 array( /* SET */
3389 'user_name' => $this->mName,
3390 'user_password' => $this->mPassword,
3391 'user_newpassword' => $this->mNewpassword,
3392 'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ),
3393 'user_real_name' => $this->mRealName,
3394 'user_email' => $this->mEmail,
3395 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
3396 'user_touched' => $dbw->timestamp( $this->mTouched ),
3397 'user_token' => strval( $this->mToken ),
3398 'user_email_token' => $this->mEmailToken,
3399 'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
3400 'user_password_expires' => $dbw->timestampOrNull( $this->mPasswordExpires ),
3401 ), array( /* WHERE */
3402 'user_id' => $this->mId
3403 ), __METHOD__
3404 );
3405
3406 $this->saveOptions();
3407
3408 wfRunHooks( 'UserSaveSettings', array( $this ) );
3409 $this->clearSharedCache();
3410 $this->getUserPage()->invalidateCache();
3411 }
3412
3413 /**
3414 * If only this user's username is known, and it exists, return the user ID.
3415 * @return int
3416 */
3417 public function idForName() {
3418 $s = trim( $this->getName() );
3419 if ( $s === '' ) {
3420 return 0;
3421 }
3422
3423 $dbr = wfGetDB( DB_SLAVE );
3424 $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), __METHOD__ );
3425 if ( $id === false ) {
3426 $id = 0;
3427 }
3428 return $id;
3429 }
3430
3431 /**
3432 * Add a user to the database, return the user object
3433 *
3434 * @param string $name Username to add
3435 * @param array $params of Strings Non-default parameters to save to the database as user_* fields:
3436 * - password The user's password hash. Password logins will be disabled if this is omitted.
3437 * - newpassword Hash for a temporary password that has been mailed to the user
3438 * - email The user's email address
3439 * - email_authenticated The email authentication timestamp
3440 * - real_name The user's real name
3441 * - options An associative array of non-default options
3442 * - token Random authentication token. Do not set.
3443 * - registration Registration timestamp. Do not set.
3444 *
3445 * @return User object, or null if the username already exists
3446 */
3447 public static function createNew( $name, $params = array() ) {
3448 $user = new User;
3449 $user->load();
3450 $user->setToken(); // init token
3451 if ( isset( $params['options'] ) ) {
3452 $user->mOptions = $params['options'] + (array)$user->mOptions;
3453 unset( $params['options'] );
3454 }
3455 $dbw = wfGetDB( DB_MASTER );
3456 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
3457
3458 $fields = array(
3459 'user_id' => $seqVal,
3460 'user_name' => $name,
3461 'user_password' => $user->mPassword,
3462 'user_newpassword' => $user->mNewpassword,
3463 'user_newpass_time' => $dbw->timestampOrNull( $user->mNewpassTime ),
3464 'user_email' => $user->mEmail,
3465 'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ),
3466 'user_real_name' => $user->mRealName,
3467 'user_token' => strval( $user->mToken ),
3468 'user_registration' => $dbw->timestamp( $user->mRegistration ),
3469 'user_editcount' => 0,
3470 'user_touched' => $dbw->timestamp( self::newTouchedTimestamp() ),
3471 );
3472 foreach ( $params as $name => $value ) {
3473 $fields["user_$name"] = $value;
3474 }
3475 $dbw->insert( 'user', $fields, __METHOD__, array( 'IGNORE' ) );
3476 if ( $dbw->affectedRows() ) {
3477 $newUser = User::newFromId( $dbw->insertId() );
3478 } else {
3479 $newUser = null;
3480 }
3481 return $newUser;
3482 }
3483
3484 /**
3485 * Add this existing user object to the database. If the user already
3486 * exists, a fatal status object is returned, and the user object is
3487 * initialised with the data from the database.
3488 *
3489 * Previously, this function generated a DB error due to a key conflict
3490 * if the user already existed. Many extension callers use this function
3491 * in code along the lines of:
3492 *
3493 * $user = User::newFromName( $name );
3494 * if ( !$user->isLoggedIn() ) {
3495 * $user->addToDatabase();
3496 * }
3497 * // do something with $user...
3498 *
3499 * However, this was vulnerable to a race condition (bug 16020). By
3500 * initialising the user object if the user exists, we aim to support this
3501 * calling sequence as far as possible.
3502 *
3503 * Note that if the user exists, this function will acquire a write lock,
3504 * so it is still advisable to make the call conditional on isLoggedIn(),
3505 * and to commit the transaction after calling.
3506 *
3507 * @throws MWException
3508 * @return Status
3509 */
3510 public function addToDatabase() {
3511 $this->load();
3512 if ( !$this->mToken ) {
3513 $this->setToken(); // init token
3514 }
3515
3516 $this->mTouched = self::newTouchedTimestamp();
3517
3518 $dbw = wfGetDB( DB_MASTER );
3519 $inWrite = $dbw->writesOrCallbacksPending();
3520 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
3521 $dbw->insert( 'user',
3522 array(
3523 'user_id' => $seqVal,
3524 'user_name' => $this->mName,
3525 'user_password' => $this->mPassword,
3526 'user_newpassword' => $this->mNewpassword,
3527 'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ),
3528 'user_email' => $this->mEmail,
3529 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
3530 'user_real_name' => $this->mRealName,
3531 'user_token' => strval( $this->mToken ),
3532 'user_registration' => $dbw->timestamp( $this->mRegistration ),
3533 'user_editcount' => 0,
3534 'user_touched' => $dbw->timestamp( $this->mTouched ),
3535 ), __METHOD__,
3536 array( 'IGNORE' )
3537 );
3538 if ( !$dbw->affectedRows() ) {
3539 if ( !$inWrite ) {
3540 // XXX: Get out of REPEATABLE-READ so the SELECT below works.
3541 // Often this case happens early in views before any writes.
3542 // This shows up at least with CentralAuth.
3543 $dbw->commit( __METHOD__, 'flush' );
3544 }
3545 $this->mId = $dbw->selectField( 'user', 'user_id',
3546 array( 'user_name' => $this->mName ), __METHOD__ );
3547 $loaded = false;
3548 if ( $this->mId ) {
3549 if ( $this->loadFromDatabase() ) {
3550 $loaded = true;
3551 }
3552 }
3553 if ( !$loaded ) {
3554 throw new MWException( __METHOD__ . ": hit a key conflict attempting " .
3555 "to insert user '{$this->mName}' row, but it was not present in select!" );
3556 }
3557 return Status::newFatal( 'userexists' );
3558 }
3559 $this->mId = $dbw->insertId();
3560
3561 // Clear instance cache other than user table data, which is already accurate
3562 $this->clearInstanceCache();
3563
3564 $this->saveOptions();
3565 return Status::newGood();
3566 }
3567
3568 /**
3569 * If this user is logged-in and blocked,
3570 * block any IP address they've successfully logged in from.
3571 * @return bool A block was spread
3572 */
3573 public function spreadAnyEditBlock() {
3574 if ( $this->isLoggedIn() && $this->isBlocked() ) {
3575 return $this->spreadBlock();
3576 }
3577 return false;
3578 }
3579
3580 /**
3581 * If this (non-anonymous) user is blocked,
3582 * block the IP address they've successfully logged in from.
3583 * @return bool A block was spread
3584 */
3585 protected function spreadBlock() {
3586 wfDebug( __METHOD__ . "()\n" );
3587 $this->load();
3588 if ( $this->mId == 0 ) {
3589 return false;
3590 }
3591
3592 $userblock = Block::newFromTarget( $this->getName() );
3593 if ( !$userblock ) {
3594 return false;
3595 }
3596
3597 return (bool)$userblock->doAutoblock( $this->getRequest()->getIP() );
3598 }
3599
3600 /**
3601 * Get whether the user is explicitly blocked from account creation.
3602 * @return bool|Block
3603 */
3604 public function isBlockedFromCreateAccount() {
3605 $this->getBlockedStatus();
3606 if ( $this->mBlock && $this->mBlock->prevents( 'createaccount' ) ) {
3607 return $this->mBlock;
3608 }
3609
3610 # bug 13611: if the IP address the user is trying to create an account from is
3611 # blocked with createaccount disabled, prevent new account creation there even
3612 # when the user is logged in
3613 if ( $this->mBlockedFromCreateAccount === false && !$this->isAllowed( 'ipblock-exempt' ) ) {
3614 $this->mBlockedFromCreateAccount = Block::newFromTarget( null, $this->getRequest()->getIP() );
3615 }
3616 return $this->mBlockedFromCreateAccount instanceof Block && $this->mBlockedFromCreateAccount->prevents( 'createaccount' )
3617 ? $this->mBlockedFromCreateAccount
3618 : false;
3619 }
3620
3621 /**
3622 * Get whether the user is blocked from using Special:Emailuser.
3623 * @return bool
3624 */
3625 public function isBlockedFromEmailuser() {
3626 $this->getBlockedStatus();
3627 return $this->mBlock && $this->mBlock->prevents( 'sendemail' );
3628 }
3629
3630 /**
3631 * Get whether the user is allowed to create an account.
3632 * @return bool
3633 */
3634 public function isAllowedToCreateAccount() {
3635 return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount();
3636 }
3637
3638 /**
3639 * Get this user's personal page title.
3640 *
3641 * @return Title: User's personal page title
3642 */
3643 public function getUserPage() {
3644 return Title::makeTitle( NS_USER, $this->getName() );
3645 }
3646
3647 /**
3648 * Get this user's talk page title.
3649 *
3650 * @return Title: User's talk page title
3651 */
3652 public function getTalkPage() {
3653 $title = $this->getUserPage();
3654 return $title->getTalkPage();
3655 }
3656
3657 /**
3658 * Determine whether the user is a newbie. Newbies are either
3659 * anonymous IPs, or the most recently created accounts.
3660 * @return bool
3661 */
3662 public function isNewbie() {
3663 return !$this->isAllowed( 'autoconfirmed' );
3664 }
3665
3666 /**
3667 * Check to see if the given clear-text password is one of the accepted passwords
3668 * @param string $password user password.
3669 * @return boolean: True if the given password is correct, otherwise False.
3670 */
3671 public function checkPassword( $password ) {
3672 global $wgAuth, $wgLegacyEncoding;
3673 $this->load();
3674
3675 // Even though we stop people from creating passwords that
3676 // are shorter than this, doesn't mean people wont be able
3677 // to. Certain authentication plugins do NOT want to save
3678 // domain passwords in a mysql database, so we should
3679 // check this (in case $wgAuth->strict() is false).
3680 if ( !$this->isValidPassword( $password ) ) {
3681 return false;
3682 }
3683
3684 if ( $wgAuth->authenticate( $this->getName(), $password ) ) {
3685 return true;
3686 } elseif ( $wgAuth->strict() ) {
3687 // Auth plugin doesn't allow local authentication
3688 return false;
3689 } elseif ( $wgAuth->strictUserAuth( $this->getName() ) ) {
3690 // Auth plugin doesn't allow local authentication for this user name
3691 return false;
3692 }
3693 if ( self::comparePasswords( $this->mPassword, $password, $this->mId ) ) {
3694 return true;
3695 } elseif ( $wgLegacyEncoding ) {
3696 // Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
3697 // Check for this with iconv
3698 $cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password );
3699 if ( $cp1252Password != $password
3700 && self::comparePasswords( $this->mPassword, $cp1252Password, $this->mId )
3701 ) {
3702 return true;
3703 }
3704 }
3705 return false;
3706 }
3707
3708 /**
3709 * Check if the given clear-text password matches the temporary password
3710 * sent by e-mail for password reset operations.
3711 *
3712 * @param $plaintext string
3713 *
3714 * @return boolean: True if matches, false otherwise
3715 */
3716 public function checkTemporaryPassword( $plaintext ) {
3717 global $wgNewPasswordExpiry;
3718
3719 $this->load();
3720 if ( self::comparePasswords( $this->mNewpassword, $plaintext, $this->getId() ) ) {
3721 if ( is_null( $this->mNewpassTime ) ) {
3722 return true;
3723 }
3724 $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgNewPasswordExpiry;
3725 return ( time() < $expiry );
3726 } else {
3727 return false;
3728 }
3729 }
3730
3731 /**
3732 * Alias for getEditToken.
3733 * @deprecated since 1.19, use getEditToken instead.
3734 *
3735 * @param string|array $salt of Strings Optional function-specific data for hashing
3736 * @param $request WebRequest object to use or null to use $wgRequest
3737 * @return string The new edit token
3738 */
3739 public function editToken( $salt = '', $request = null ) {
3740 wfDeprecated( __METHOD__, '1.19' );
3741 return $this->getEditToken( $salt, $request );
3742 }
3743
3744 /**
3745 * Initialize (if necessary) and return a session token value
3746 * which can be used in edit forms to show that the user's
3747 * login credentials aren't being hijacked with a foreign form
3748 * submission.
3749 *
3750 * @since 1.19
3751 *
3752 * @param string|array $salt of Strings Optional function-specific data for hashing
3753 * @param $request WebRequest object to use or null to use $wgRequest
3754 * @return string The new edit token
3755 */
3756 public function getEditToken( $salt = '', $request = null ) {
3757 if ( $request == null ) {
3758 $request = $this->getRequest();
3759 }
3760
3761 if ( $this->isAnon() ) {
3762 return EDIT_TOKEN_SUFFIX;
3763 } else {
3764 $token = $request->getSessionData( 'wsEditToken' );
3765 if ( $token === null ) {
3766 $token = MWCryptRand::generateHex( 32 );
3767 $request->setSessionData( 'wsEditToken', $token );
3768 }
3769 if ( is_array( $salt ) ) {
3770 $salt = implode( '|', $salt );
3771 }
3772 return md5( $token . $salt ) . EDIT_TOKEN_SUFFIX;
3773 }
3774 }
3775
3776 /**
3777 * Generate a looking random token for various uses.
3778 *
3779 * @return string The new random token
3780 * @deprecated since 1.20: Use MWCryptRand for secure purposes or wfRandomString for pseudo-randomness
3781 */
3782 public static function generateToken() {
3783 return MWCryptRand::generateHex( 32 );
3784 }
3785
3786 /**
3787 * Check given value against the token value stored in the session.
3788 * A match should confirm that the form was submitted from the
3789 * user's own login session, not a form submission from a third-party
3790 * site.
3791 *
3792 * @param string $val Input value to compare
3793 * @param string $salt Optional function-specific data for hashing
3794 * @param WebRequest $request Object to use or null to use $wgRequest
3795 * @return boolean: Whether the token matches
3796 */
3797 public function matchEditToken( $val, $salt = '', $request = null ) {
3798 $sessionToken = $this->getEditToken( $salt, $request );
3799 if ( $val != $sessionToken ) {
3800 wfDebug( "User::matchEditToken: broken session data\n" );
3801 }
3802 return $val == $sessionToken;
3803 }
3804
3805 /**
3806 * Check given value against the token value stored in the session,
3807 * ignoring the suffix.
3808 *
3809 * @param string $val Input value to compare
3810 * @param string $salt Optional function-specific data for hashing
3811 * @param WebRequest $request object to use or null to use $wgRequest
3812 * @return boolean: Whether the token matches
3813 */
3814 public function matchEditTokenNoSuffix( $val, $salt = '', $request = null ) {
3815 $sessionToken = $this->getEditToken( $salt, $request );
3816 return substr( $sessionToken, 0, 32 ) == substr( $val, 0, 32 );
3817 }
3818
3819 /**
3820 * Generate a new e-mail confirmation token and send a confirmation/invalidation
3821 * mail to the user's given address.
3822 *
3823 * @param string $type message to send, either "created", "changed" or "set"
3824 * @return Status object
3825 */
3826 public function sendConfirmationMail( $type = 'created' ) {
3827 global $wgLang;
3828 $expiration = null; // gets passed-by-ref and defined in next line.
3829 $token = $this->confirmationToken( $expiration );
3830 $url = $this->confirmationTokenUrl( $token );
3831 $invalidateURL = $this->invalidationTokenUrl( $token );
3832 $this->saveSettings();
3833
3834 if ( $type == 'created' || $type === false ) {
3835 $message = 'confirmemail_body';
3836 } elseif ( $type === true ) {
3837 $message = 'confirmemail_body_changed';
3838 } else {
3839 // Messages: confirmemail_body_changed, confirmemail_body_set
3840 $message = 'confirmemail_body_' . $type;
3841 }
3842
3843 return $this->sendMail( wfMessage( 'confirmemail_subject' )->text(),
3844 wfMessage( $message,
3845 $this->getRequest()->getIP(),
3846 $this->getName(),
3847 $url,
3848 $wgLang->timeanddate( $expiration, false ),
3849 $invalidateURL,
3850 $wgLang->date( $expiration, false ),
3851 $wgLang->time( $expiration, false ) )->text() );
3852 }
3853
3854 /**
3855 * Send an e-mail to this user's account. Does not check for
3856 * confirmed status or validity.
3857 *
3858 * @param string $subject Message subject
3859 * @param string $body Message body
3860 * @param string $from Optional From address; if unspecified, default $wgPasswordSender will be used
3861 * @param string $replyto Reply-To address
3862 * @return Status
3863 */
3864 public function sendMail( $subject, $body, $from = null, $replyto = null ) {
3865 if ( is_null( $from ) ) {
3866 global $wgPasswordSender;
3867 $sender = new MailAddress( $wgPasswordSender,
3868 wfMessage( 'emailsender' )->inContentLanguage()->text() );
3869 } else {
3870 $sender = new MailAddress( $from );
3871 }
3872
3873 $to = new MailAddress( $this );
3874 return UserMailer::send( $to, $sender, $subject, $body, $replyto );
3875 }
3876
3877 /**
3878 * Generate, store, and return a new e-mail confirmation code.
3879 * A hash (unsalted, since it's used as a key) is stored.
3880 *
3881 * @note Call saveSettings() after calling this function to commit
3882 * this change to the database.
3883 *
3884 * @param &$expiration \mixed Accepts the expiration time
3885 * @return string New token
3886 */
3887 protected function confirmationToken( &$expiration ) {
3888 global $wgUserEmailConfirmationTokenExpiry;
3889 $now = time();
3890 $expires = $now + $wgUserEmailConfirmationTokenExpiry;
3891 $expiration = wfTimestamp( TS_MW, $expires );
3892 $this->load();
3893 $token = MWCryptRand::generateHex( 32 );
3894 $hash = md5( $token );
3895 $this->mEmailToken = $hash;
3896 $this->mEmailTokenExpires = $expiration;
3897 return $token;
3898 }
3899
3900 /**
3901 * Return a URL the user can use to confirm their email address.
3902 * @param string $token Accepts the email confirmation token
3903 * @return string New token URL
3904 */
3905 protected function confirmationTokenUrl( $token ) {
3906 return $this->getTokenUrl( 'ConfirmEmail', $token );
3907 }
3908
3909 /**
3910 * Return a URL the user can use to invalidate their email address.
3911 * @param string $token Accepts the email confirmation token
3912 * @return string New token URL
3913 */
3914 protected function invalidationTokenUrl( $token ) {
3915 return $this->getTokenUrl( 'InvalidateEmail', $token );
3916 }
3917
3918 /**
3919 * Internal function to format the e-mail validation/invalidation URLs.
3920 * This uses a quickie hack to use the
3921 * hardcoded English names of the Special: pages, for ASCII safety.
3922 *
3923 * @note Since these URLs get dropped directly into emails, using the
3924 * short English names avoids insanely long URL-encoded links, which
3925 * also sometimes can get corrupted in some browsers/mailers
3926 * (bug 6957 with Gmail and Internet Explorer).
3927 *
3928 * @param string $page Special page
3929 * @param string $token Token
3930 * @return string Formatted URL
3931 */
3932 protected function getTokenUrl( $page, $token ) {
3933 // Hack to bypass localization of 'Special:'
3934 $title = Title::makeTitle( NS_MAIN, "Special:$page/$token" );
3935 return $title->getCanonicalURL();
3936 }
3937
3938 /**
3939 * Mark the e-mail address confirmed.
3940 *
3941 * @note Call saveSettings() after calling this function to commit the change.
3942 *
3943 * @return bool
3944 */
3945 public function confirmEmail() {
3946 // Check if it's already confirmed, so we don't touch the database
3947 // and fire the ConfirmEmailComplete hook on redundant confirmations.
3948 if ( !$this->isEmailConfirmed() ) {
3949 $this->setEmailAuthenticationTimestamp( wfTimestampNow() );
3950 wfRunHooks( 'ConfirmEmailComplete', array( $this ) );
3951 }
3952 return true;
3953 }
3954
3955 /**
3956 * Invalidate the user's e-mail confirmation, and unauthenticate the e-mail
3957 * address if it was already confirmed.
3958 *
3959 * @note Call saveSettings() after calling this function to commit the change.
3960 * @return bool Returns true
3961 */
3962 public function invalidateEmail() {
3963 $this->load();
3964 $this->mEmailToken = null;
3965 $this->mEmailTokenExpires = null;
3966 $this->setEmailAuthenticationTimestamp( null );
3967 wfRunHooks( 'InvalidateEmailComplete', array( $this ) );
3968 return true;
3969 }
3970
3971 /**
3972 * Set the e-mail authentication timestamp.
3973 * @param string $timestamp TS_MW timestamp
3974 */
3975 public function setEmailAuthenticationTimestamp( $timestamp ) {
3976 $this->load();
3977 $this->mEmailAuthenticated = $timestamp;
3978 wfRunHooks( 'UserSetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
3979 }
3980
3981 /**
3982 * Is this user allowed to send e-mails within limits of current
3983 * site configuration?
3984 * @return bool
3985 */
3986 public function canSendEmail() {
3987 global $wgEnableEmail, $wgEnableUserEmail;
3988 if ( !$wgEnableEmail || !$wgEnableUserEmail || !$this->isAllowed( 'sendemail' ) ) {
3989 return false;
3990 }
3991 $canSend = $this->isEmailConfirmed();
3992 wfRunHooks( 'UserCanSendEmail', array( &$this, &$canSend ) );
3993 return $canSend;
3994 }
3995
3996 /**
3997 * Is this user allowed to receive e-mails within limits of current
3998 * site configuration?
3999 * @return bool
4000 */
4001 public function canReceiveEmail() {
4002 return $this->isEmailConfirmed() && !$this->getOption( 'disablemail' );
4003 }
4004
4005 /**
4006 * Is this user's e-mail address valid-looking and confirmed within
4007 * limits of the current site configuration?
4008 *
4009 * @note If $wgEmailAuthentication is on, this may require the user to have
4010 * confirmed their address by returning a code or using a password
4011 * sent to the address from the wiki.
4012 *
4013 * @return bool
4014 */
4015 public function isEmailConfirmed() {
4016 global $wgEmailAuthentication;
4017 $this->load();
4018 $confirmed = true;
4019 if ( wfRunHooks( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) {
4020 if ( $this->isAnon() ) {
4021 return false;
4022 }
4023 if ( !Sanitizer::validateEmail( $this->mEmail ) ) {
4024 return false;
4025 }
4026 if ( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() ) {
4027 return false;
4028 }
4029 return true;
4030 } else {
4031 return $confirmed;
4032 }
4033 }
4034
4035 /**
4036 * Check whether there is an outstanding request for e-mail confirmation.
4037 * @return bool
4038 */
4039 public function isEmailConfirmationPending() {
4040 global $wgEmailAuthentication;
4041 return $wgEmailAuthentication &&
4042 !$this->isEmailConfirmed() &&
4043 $this->mEmailToken &&
4044 $this->mEmailTokenExpires > wfTimestamp();
4045 }
4046
4047 /**
4048 * Get the timestamp of account creation.
4049 *
4050 * @return string|bool|null Timestamp of account creation, false for
4051 * non-existent/anonymous user accounts, or null if existing account
4052 * but information is not in database.
4053 */
4054 public function getRegistration() {
4055 if ( $this->isAnon() ) {
4056 return false;
4057 }
4058 $this->load();
4059 return $this->mRegistration;
4060 }
4061
4062 /**
4063 * Get the timestamp of the first edit
4064 *
4065 * @return string|bool Timestamp of first edit, or false for
4066 * non-existent/anonymous user accounts.
4067 */
4068 public function getFirstEditTimestamp() {
4069 if ( $this->getId() == 0 ) {
4070 return false; // anons
4071 }
4072 $dbr = wfGetDB( DB_SLAVE );
4073 $time = $dbr->selectField( 'revision', 'rev_timestamp',
4074 array( 'rev_user' => $this->getId() ),
4075 __METHOD__,
4076 array( 'ORDER BY' => 'rev_timestamp ASC' )
4077 );
4078 if ( !$time ) {
4079 return false; // no edits
4080 }
4081 return wfTimestamp( TS_MW, $time );
4082 }
4083
4084 /**
4085 * Get the permissions associated with a given list of groups
4086 *
4087 * @param array $groups of Strings List of internal group names
4088 * @return Array of Strings List of permission key names for given groups combined
4089 */
4090 public static function getGroupPermissions( $groups ) {
4091 global $wgGroupPermissions, $wgRevokePermissions;
4092 $rights = array();
4093 // grant every granted permission first
4094 foreach ( $groups as $group ) {
4095 if ( isset( $wgGroupPermissions[$group] ) ) {
4096 $rights = array_merge( $rights,
4097 // array_filter removes empty items
4098 array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
4099 }
4100 }
4101 // now revoke the revoked permissions
4102 foreach ( $groups as $group ) {
4103 if ( isset( $wgRevokePermissions[$group] ) ) {
4104 $rights = array_diff( $rights,
4105 array_keys( array_filter( $wgRevokePermissions[$group] ) ) );
4106 }
4107 }
4108 return array_unique( $rights );
4109 }
4110
4111 /**
4112 * Get all the groups who have a given permission
4113 *
4114 * @param string $role Role to check
4115 * @return Array of Strings List of internal group names with the given permission
4116 */
4117 public static function getGroupsWithPermission( $role ) {
4118 global $wgGroupPermissions;
4119 $allowedGroups = array();
4120 foreach ( array_keys( $wgGroupPermissions ) as $group ) {
4121 if ( self::groupHasPermission( $group, $role ) ) {
4122 $allowedGroups[] = $group;
4123 }
4124 }
4125 return $allowedGroups;
4126 }
4127
4128 /**
4129 * Check, if the given group has the given permission
4130 *
4131 * If you're wanting to check whether all users have a permission, use
4132 * User::isEveryoneAllowed() instead. That properly checks if it's revoked
4133 * from anyone.
4134 *
4135 * @since 1.21
4136 * @param string $group Group to check
4137 * @param string $role Role to check
4138 * @return bool
4139 */
4140 public static function groupHasPermission( $group, $role ) {
4141 global $wgGroupPermissions, $wgRevokePermissions;
4142 return isset( $wgGroupPermissions[$group][$role] ) && $wgGroupPermissions[$group][$role]
4143 && !( isset( $wgRevokePermissions[$group][$role] ) && $wgRevokePermissions[$group][$role] );
4144 }
4145
4146 /**
4147 * Check if all users have the given permission
4148 *
4149 * @since 1.22
4150 * @param string $right Right to check
4151 * @return bool
4152 */
4153 public static function isEveryoneAllowed( $right ) {
4154 global $wgGroupPermissions, $wgRevokePermissions;
4155 static $cache = array();
4156
4157 // Use the cached results, except in unit tests which rely on
4158 // being able change the permission mid-request
4159 if ( isset( $cache[$right] ) && !defined( 'MW_PHPUNIT_TEST' ) ) {
4160 return $cache[$right];
4161 }
4162
4163 if ( !isset( $wgGroupPermissions['*'][$right] ) || !$wgGroupPermissions['*'][$right] ) {
4164 $cache[$right] = false;
4165 return false;
4166 }
4167
4168 // If it's revoked anywhere, then everyone doesn't have it
4169 foreach ( $wgRevokePermissions as $rights ) {
4170 if ( isset( $rights[$right] ) && $rights[$right] ) {
4171 $cache[$right] = false;
4172 return false;
4173 }
4174 }
4175
4176 // Allow extensions (e.g. OAuth) to say false
4177 if ( !wfRunHooks( 'UserIsEveryoneAllowed', array( $right ) ) ) {
4178 $cache[$right] = false;
4179 return false;
4180 }
4181
4182 $cache[$right] = true;
4183 return true;
4184 }
4185
4186 /**
4187 * Get the localized descriptive name for a group, if it exists
4188 *
4189 * @param string $group Internal group name
4190 * @return string Localized descriptive group name
4191 */
4192 public static function getGroupName( $group ) {
4193 $msg = wfMessage( "group-$group" );
4194 return $msg->isBlank() ? $group : $msg->text();
4195 }
4196
4197 /**
4198 * Get the localized descriptive name for a member of a group, if it exists
4199 *
4200 * @param string $group Internal group name
4201 * @param string $username Username for gender (since 1.19)
4202 * @return string Localized name for group member
4203 */
4204 public static function getGroupMember( $group, $username = '#' ) {
4205 $msg = wfMessage( "group-$group-member", $username );
4206 return $msg->isBlank() ? $group : $msg->text();
4207 }
4208
4209 /**
4210 * Return the set of defined explicit groups.
4211 * The implicit groups (by default *, 'user' and 'autoconfirmed')
4212 * are not included, as they are defined automatically, not in the database.
4213 * @return Array of internal group names
4214 */
4215 public static function getAllGroups() {
4216 global $wgGroupPermissions, $wgRevokePermissions;
4217 return array_diff(
4218 array_merge( array_keys( $wgGroupPermissions ), array_keys( $wgRevokePermissions ) ),
4219 self::getImplicitGroups()
4220 );
4221 }
4222
4223 /**
4224 * Get a list of all available permissions.
4225 * @return Array of permission names
4226 */
4227 public static function getAllRights() {
4228 if ( self::$mAllRights === false ) {
4229 global $wgAvailableRights;
4230 if ( count( $wgAvailableRights ) ) {
4231 self::$mAllRights = array_unique( array_merge( self::$mCoreRights, $wgAvailableRights ) );
4232 } else {
4233 self::$mAllRights = self::$mCoreRights;
4234 }
4235 wfRunHooks( 'UserGetAllRights', array( &self::$mAllRights ) );
4236 }
4237 return self::$mAllRights;
4238 }
4239
4240 /**
4241 * Get a list of implicit groups
4242 * @return Array of Strings Array of internal group names
4243 */
4244 public static function getImplicitGroups() {
4245 global $wgImplicitGroups;
4246 $groups = $wgImplicitGroups;
4247 wfRunHooks( 'UserGetImplicitGroups', array( &$groups ) ); #deprecated, use $wgImplictGroups instead
4248 return $groups;
4249 }
4250
4251 /**
4252 * Get the title of a page describing a particular group
4253 *
4254 * @param string $group Internal group name
4255 * @return Title|bool Title of the page if it exists, false otherwise
4256 */
4257 public static function getGroupPage( $group ) {
4258 $msg = wfMessage( 'grouppage-' . $group )->inContentLanguage();
4259 if ( $msg->exists() ) {
4260 $title = Title::newFromText( $msg->text() );
4261 if ( is_object( $title ) ) {
4262 return $title;
4263 }
4264 }
4265 return false;
4266 }
4267
4268 /**
4269 * Create a link to the group in HTML, if available;
4270 * else return the group name.
4271 *
4272 * @param string $group Internal name of the group
4273 * @param string $text The text of the link
4274 * @return string HTML link to the group
4275 */
4276 public static function makeGroupLinkHTML( $group, $text = '' ) {
4277 if ( $text == '' ) {
4278 $text = self::getGroupName( $group );
4279 }
4280 $title = self::getGroupPage( $group );
4281 if ( $title ) {
4282 return Linker::link( $title, htmlspecialchars( $text ) );
4283 } else {
4284 return $text;
4285 }
4286 }
4287
4288 /**
4289 * Create a link to the group in Wikitext, if available;
4290 * else return the group name.
4291 *
4292 * @param string $group Internal name of the group
4293 * @param string $text The text of the link
4294 * @return string Wikilink to the group
4295 */
4296 public static function makeGroupLinkWiki( $group, $text = '' ) {
4297 if ( $text == '' ) {
4298 $text = self::getGroupName( $group );
4299 }
4300 $title = self::getGroupPage( $group );
4301 if ( $title ) {
4302 $page = $title->getPrefixedText();
4303 return "[[$page|$text]]";
4304 } else {
4305 return $text;
4306 }
4307 }
4308
4309 /**
4310 * Returns an array of the groups that a particular group can add/remove.
4311 *
4312 * @param string $group the group to check for whether it can add/remove
4313 * @return Array array( 'add' => array( addablegroups ),
4314 * 'remove' => array( removablegroups ),
4315 * 'add-self' => array( addablegroups to self),
4316 * 'remove-self' => array( removable groups from self) )
4317 */
4318 public static function changeableByGroup( $group ) {
4319 global $wgAddGroups, $wgRemoveGroups, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
4320
4321 $groups = array( 'add' => array(), 'remove' => array(), 'add-self' => array(), 'remove-self' => array() );
4322 if ( empty( $wgAddGroups[$group] ) ) {
4323 // Don't add anything to $groups
4324 } elseif ( $wgAddGroups[$group] === true ) {
4325 // You get everything
4326 $groups['add'] = self::getAllGroups();
4327 } elseif ( is_array( $wgAddGroups[$group] ) ) {
4328 $groups['add'] = $wgAddGroups[$group];
4329 }
4330
4331 // Same thing for remove
4332 if ( empty( $wgRemoveGroups[$group] ) ) {
4333 } elseif ( $wgRemoveGroups[$group] === true ) {
4334 $groups['remove'] = self::getAllGroups();
4335 } elseif ( is_array( $wgRemoveGroups[$group] ) ) {
4336 $groups['remove'] = $wgRemoveGroups[$group];
4337 }
4338
4339 // Re-map numeric keys of AddToSelf/RemoveFromSelf to the 'user' key for backwards compatibility
4340 if ( empty( $wgGroupsAddToSelf['user'] ) || $wgGroupsAddToSelf['user'] !== true ) {
4341 foreach ( $wgGroupsAddToSelf as $key => $value ) {
4342 if ( is_int( $key ) ) {
4343 $wgGroupsAddToSelf['user'][] = $value;
4344 }
4345 }
4346 }
4347
4348 if ( empty( $wgGroupsRemoveFromSelf['user'] ) || $wgGroupsRemoveFromSelf['user'] !== true ) {
4349 foreach ( $wgGroupsRemoveFromSelf as $key => $value ) {
4350 if ( is_int( $key ) ) {
4351 $wgGroupsRemoveFromSelf['user'][] = $value;
4352 }
4353 }
4354 }
4355
4356 // Now figure out what groups the user can add to him/herself
4357 if ( empty( $wgGroupsAddToSelf[$group] ) ) {
4358 } elseif ( $wgGroupsAddToSelf[$group] === true ) {
4359 // No idea WHY this would be used, but it's there
4360 $groups['add-self'] = User::getAllGroups();
4361 } elseif ( is_array( $wgGroupsAddToSelf[$group] ) ) {
4362 $groups['add-self'] = $wgGroupsAddToSelf[$group];
4363 }
4364
4365 if ( empty( $wgGroupsRemoveFromSelf[$group] ) ) {
4366 } elseif ( $wgGroupsRemoveFromSelf[$group] === true ) {
4367 $groups['remove-self'] = User::getAllGroups();
4368 } elseif ( is_array( $wgGroupsRemoveFromSelf[$group] ) ) {
4369 $groups['remove-self'] = $wgGroupsRemoveFromSelf[$group];
4370 }
4371
4372 return $groups;
4373 }
4374
4375 /**
4376 * Returns an array of groups that this user can add and remove
4377 * @return Array array( 'add' => array( addablegroups ),
4378 * 'remove' => array( removablegroups ),
4379 * 'add-self' => array( addablegroups to self),
4380 * 'remove-self' => array( removable groups from self) )
4381 */
4382 public function changeableGroups() {
4383 if ( $this->isAllowed( 'userrights' ) ) {
4384 // This group gives the right to modify everything (reverse-
4385 // compatibility with old "userrights lets you change
4386 // everything")
4387 // Using array_merge to make the groups reindexed
4388 $all = array_merge( User::getAllGroups() );
4389 return array(
4390 'add' => $all,
4391 'remove' => $all,
4392 'add-self' => array(),
4393 'remove-self' => array()
4394 );
4395 }
4396
4397 // Okay, it's not so simple, we will have to go through the arrays
4398 $groups = array(
4399 'add' => array(),
4400 'remove' => array(),
4401 'add-self' => array(),
4402 'remove-self' => array()
4403 );
4404 $addergroups = $this->getEffectiveGroups();
4405
4406 foreach ( $addergroups as $addergroup ) {
4407 $groups = array_merge_recursive(
4408 $groups, $this->changeableByGroup( $addergroup )
4409 );
4410 $groups['add'] = array_unique( $groups['add'] );
4411 $groups['remove'] = array_unique( $groups['remove'] );
4412 $groups['add-self'] = array_unique( $groups['add-self'] );
4413 $groups['remove-self'] = array_unique( $groups['remove-self'] );
4414 }
4415 return $groups;
4416 }
4417
4418 /**
4419 * Increment the user's edit-count field.
4420 * Will have no effect for anonymous users.
4421 */
4422 public function incEditCount() {
4423 if ( !$this->isAnon() ) {
4424 $dbw = wfGetDB( DB_MASTER );
4425 $dbw->update(
4426 'user',
4427 array( 'user_editcount=user_editcount+1' ),
4428 array( 'user_id' => $this->getId() ),
4429 __METHOD__
4430 );
4431
4432 // Lazy initialization check...
4433 if ( $dbw->affectedRows() == 0 ) {
4434 // Now here's a goddamn hack...
4435 $dbr = wfGetDB( DB_SLAVE );
4436 if ( $dbr !== $dbw ) {
4437 // If we actually have a slave server, the count is
4438 // at least one behind because the current transaction
4439 // has not been committed and replicated.
4440 $this->initEditCount( 1 );
4441 } else {
4442 // But if DB_SLAVE is selecting the master, then the
4443 // count we just read includes the revision that was
4444 // just added in the working transaction.
4445 $this->initEditCount();
4446 }
4447 }
4448 }
4449 // edit count in user cache too
4450 $this->invalidateCache();
4451 }
4452
4453 /**
4454 * Initialize user_editcount from data out of the revision table
4455 *
4456 * @param $add Integer Edits to add to the count from the revision table
4457 * @return integer Number of edits
4458 */
4459 protected function initEditCount( $add = 0 ) {
4460 // Pull from a slave to be less cruel to servers
4461 // Accuracy isn't the point anyway here
4462 $dbr = wfGetDB( DB_SLAVE );
4463 $count = (int)$dbr->selectField(
4464 'revision',
4465 'COUNT(rev_user)',
4466 array( 'rev_user' => $this->getId() ),
4467 __METHOD__
4468 );
4469 $count = $count + $add;
4470
4471 $dbw = wfGetDB( DB_MASTER );
4472 $dbw->update(
4473 'user',
4474 array( 'user_editcount' => $count ),
4475 array( 'user_id' => $this->getId() ),
4476 __METHOD__
4477 );
4478
4479 return $count;
4480 }
4481
4482 /**
4483 * Get the description of a given right
4484 *
4485 * @param string $right Right to query
4486 * @return string Localized description of the right
4487 */
4488 public static function getRightDescription( $right ) {
4489 $key = "right-$right";
4490 $msg = wfMessage( $key );
4491 return $msg->isBlank() ? $right : $msg->text();
4492 }
4493
4494 /**
4495 * Make an old-style password hash
4496 *
4497 * @param string $password Plain-text password
4498 * @param string $userId User ID
4499 * @return string Password hash
4500 */
4501 public static function oldCrypt( $password, $userId ) {
4502 global $wgPasswordSalt;
4503 if ( $wgPasswordSalt ) {
4504 return md5( $userId . '-' . md5( $password ) );
4505 } else {
4506 return md5( $password );
4507 }
4508 }
4509
4510 /**
4511 * Make a new-style password hash
4512 *
4513 * @param string $password Plain-text password
4514 * @param bool|string $salt Optional salt, may be random or the user ID.
4515 * If unspecified or false, will generate one automatically
4516 * @return string Password hash
4517 */
4518 public static function crypt( $password, $salt = false ) {
4519 global $wgPasswordSalt;
4520
4521 $hash = '';
4522 if ( !wfRunHooks( 'UserCryptPassword', array( &$password, &$salt, &$wgPasswordSalt, &$hash ) ) ) {
4523 return $hash;
4524 }
4525
4526 if ( $wgPasswordSalt ) {
4527 if ( $salt === false ) {
4528 $salt = MWCryptRand::generateHex( 8 );
4529 }
4530 return ':B:' . $salt . ':' . md5( $salt . '-' . md5( $password ) );
4531 } else {
4532 return ':A:' . md5( $password );
4533 }
4534 }
4535
4536 /**
4537 * Compare a password hash with a plain-text password. Requires the user
4538 * ID if there's a chance that the hash is an old-style hash.
4539 *
4540 * @param string $hash Password hash
4541 * @param string $password Plain-text password to compare
4542 * @param string|bool $userId User ID for old-style password salt
4543 *
4544 * @return boolean
4545 */
4546 public static function comparePasswords( $hash, $password, $userId = false ) {
4547 $type = substr( $hash, 0, 3 );
4548
4549 $result = false;
4550 if ( !wfRunHooks( 'UserComparePasswords', array( &$hash, &$password, &$userId, &$result ) ) ) {
4551 return $result;
4552 }
4553
4554 if ( $type == ':A:' ) {
4555 // Unsalted
4556 return md5( $password ) === substr( $hash, 3 );
4557 } elseif ( $type == ':B:' ) {
4558 // Salted
4559 list( $salt, $realHash ) = explode( ':', substr( $hash, 3 ), 2 );
4560 return md5( $salt . '-' . md5( $password ) ) === $realHash;
4561 } else {
4562 // Old-style
4563 return self::oldCrypt( $password, $userId ) === $hash;
4564 }
4565 }
4566
4567 /**
4568 * Add a newuser log entry for this user.
4569 * Before 1.19 the return value was always true.
4570 *
4571 * @param string|bool $action account creation type.
4572 * - String, one of the following values:
4573 * - 'create' for an anonymous user creating an account for himself.
4574 * This will force the action's performer to be the created user itself,
4575 * no matter the value of $wgUser
4576 * - 'create2' for a logged in user creating an account for someone else
4577 * - 'byemail' when the created user will receive its password by e-mail
4578 * - 'autocreate' when the user is automatically created (such as by CentralAuth).
4579 * - Boolean means whether the account was created by e-mail (deprecated):
4580 * - true will be converted to 'byemail'
4581 * - false will be converted to 'create' if this object is the same as
4582 * $wgUser and to 'create2' otherwise
4583 *
4584 * @param string $reason user supplied reason
4585 *
4586 * @return int|bool True if not $wgNewUserLog; otherwise ID of log item or 0 on failure
4587 */
4588 public function addNewUserLogEntry( $action = false, $reason = '' ) {
4589 global $wgUser, $wgNewUserLog;
4590 if ( empty( $wgNewUserLog ) ) {
4591 return true; // disabled
4592 }
4593
4594 if ( $action === true ) {
4595 $action = 'byemail';
4596 } elseif ( $action === false ) {
4597 if ( $this->getName() == $wgUser->getName() ) {
4598 $action = 'create';
4599 } else {
4600 $action = 'create2';
4601 }
4602 }
4603
4604 if ( $action === 'create' || $action === 'autocreate' ) {
4605 $performer = $this;
4606 } else {
4607 $performer = $wgUser;
4608 }
4609
4610 $logEntry = new ManualLogEntry( 'newusers', $action );
4611 $logEntry->setPerformer( $performer );
4612 $logEntry->setTarget( $this->getUserPage() );
4613 $logEntry->setComment( $reason );
4614 $logEntry->setParameters( array(
4615 '4::userid' => $this->getId(),
4616 ) );
4617 $logid = $logEntry->insert();
4618
4619 if ( $action !== 'autocreate' ) {
4620 $logEntry->publish( $logid );
4621 }
4622
4623 return (int)$logid;
4624 }
4625
4626 /**
4627 * Add an autocreate newuser log entry for this user
4628 * Used by things like CentralAuth and perhaps other authplugins.
4629 * Consider calling addNewUserLogEntry() directly instead.
4630 *
4631 * @return bool
4632 */
4633 public function addNewUserLogEntryAutoCreate() {
4634 $this->addNewUserLogEntry( 'autocreate' );
4635
4636 return true;
4637 }
4638
4639 /**
4640 * Load the user options either from cache, the database or an array
4641 *
4642 * @param array $data Rows for the current user out of the user_properties table
4643 */
4644 protected function loadOptions( $data = null ) {
4645 global $wgContLang;
4646
4647 $this->load();
4648
4649 if ( $this->mOptionsLoaded ) {
4650 return;
4651 }
4652
4653 $this->mOptions = self::getDefaultOptions();
4654
4655 if ( !$this->getId() ) {
4656 // For unlogged-in users, load language/variant options from request.
4657 // There's no need to do it for logged-in users: they can set preferences,
4658 // and handling of page content is done by $pageLang->getPreferredVariant() and such,
4659 // so don't override user's choice (especially when the user chooses site default).
4660 $variant = $wgContLang->getDefaultVariant();
4661 $this->mOptions['variant'] = $variant;
4662 $this->mOptions['language'] = $variant;
4663 $this->mOptionsLoaded = true;
4664 return;
4665 }
4666
4667 // Maybe load from the object
4668 if ( !is_null( $this->mOptionOverrides ) ) {
4669 wfDebug( "User: loading options for user " . $this->getId() . " from override cache.\n" );
4670 foreach ( $this->mOptionOverrides as $key => $value ) {
4671 $this->mOptions[$key] = $value;
4672 }
4673 } else {
4674 if ( !is_array( $data ) ) {
4675 wfDebug( "User: loading options for user " . $this->getId() . " from database.\n" );
4676 // Load from database
4677 $dbr = wfGetDB( DB_SLAVE );
4678
4679 $res = $dbr->select(
4680 'user_properties',
4681 array( 'up_property', 'up_value' ),
4682 array( 'up_user' => $this->getId() ),
4683 __METHOD__
4684 );
4685
4686 $this->mOptionOverrides = array();
4687 $data = array();
4688 foreach ( $res as $row ) {
4689 $data[$row->up_property] = $row->up_value;
4690 }
4691 }
4692 foreach ( $data as $property => $value ) {
4693 $this->mOptionOverrides[$property] = $value;
4694 $this->mOptions[$property] = $value;
4695 }
4696 }
4697
4698 $this->mOptionsLoaded = true;
4699
4700 wfRunHooks( 'UserLoadOptions', array( $this, &$this->mOptions ) );
4701 }
4702
4703 /**
4704 * @todo document
4705 */
4706 protected function saveOptions() {
4707 $this->loadOptions();
4708
4709 // Not using getOptions(), to keep hidden preferences in database
4710 $saveOptions = $this->mOptions;
4711
4712 // Allow hooks to abort, for instance to save to a global profile.
4713 // Reset options to default state before saving.
4714 if ( !wfRunHooks( 'UserSaveOptions', array( $this, &$saveOptions ) ) ) {
4715 return;
4716 }
4717
4718 $userId = $this->getId();
4719 $insert_rows = array();
4720 foreach ( $saveOptions as $key => $value ) {
4721 // Don't bother storing default values
4722 $defaultOption = self::getDefaultOption( $key );
4723 if ( ( is_null( $defaultOption ) &&
4724 !( $value === false || is_null( $value ) ) ) ||
4725 $value != $defaultOption ) {
4726 $insert_rows[] = array(
4727 'up_user' => $userId,
4728 'up_property' => $key,
4729 'up_value' => $value,
4730 );
4731 }
4732 }
4733
4734 $dbw = wfGetDB( DB_MASTER );
4735 $hasRows = $dbw->selectField( 'user_properties', '1',
4736 array( 'up_user' => $userId ), __METHOD__ );
4737
4738 if ( $hasRows ) {
4739 // Only do this delete if there is something there. A very large portion of
4740 // calls to this function are for setting 'rememberpassword' for new accounts.
4741 // Doing this delete for new accounts with no rows in the table rougly causes
4742 // gap locks on [max user ID,+infinity) which causes high contention since many
4743 // updates will pile up on each other since they are for higher (newer) user IDs.
4744 $dbw->delete( 'user_properties', array( 'up_user' => $userId ), __METHOD__ );
4745 }
4746 $dbw->insert( 'user_properties', $insert_rows, __METHOD__, array( 'IGNORE' ) );
4747 }
4748
4749 /**
4750 * Provide an array of HTML5 attributes to put on an input element
4751 * intended for the user to enter a new password. This may include
4752 * required, title, and/or pattern, depending on $wgMinimalPasswordLength.
4753 *
4754 * Do *not* use this when asking the user to enter his current password!
4755 * Regardless of configuration, users may have invalid passwords for whatever
4756 * reason (e.g., they were set before requirements were tightened up).
4757 * Only use it when asking for a new password, like on account creation or
4758 * ResetPass.
4759 *
4760 * Obviously, you still need to do server-side checking.
4761 *
4762 * NOTE: A combination of bugs in various browsers means that this function
4763 * actually just returns array() unconditionally at the moment. May as
4764 * well keep it around for when the browser bugs get fixed, though.
4765 *
4766 * @todo FIXME: This does not belong here; put it in Html or Linker or somewhere
4767 *
4768 * @return array Array of HTML attributes suitable for feeding to
4769 * Html::element(), directly or indirectly. (Don't feed to Xml::*()!
4770 * That will get confused by the boolean attribute syntax used.)
4771 */
4772 public static function passwordChangeInputAttribs() {
4773 global $wgMinimalPasswordLength;
4774
4775 if ( $wgMinimalPasswordLength == 0 ) {
4776 return array();
4777 }
4778
4779 # Note that the pattern requirement will always be satisfied if the
4780 # input is empty, so we need required in all cases.
4781 #
4782 # @todo FIXME: Bug 23769: This needs to not claim the password is required
4783 # if e-mail confirmation is being used. Since HTML5 input validation
4784 # is b0rked anyway in some browsers, just return nothing. When it's
4785 # re-enabled, fix this code to not output required for e-mail
4786 # registration.
4787 #$ret = array( 'required' );
4788 $ret = array();
4789
4790 # We can't actually do this right now, because Opera 9.6 will print out
4791 # the entered password visibly in its error message! When other
4792 # browsers add support for this attribute, or Opera fixes its support,
4793 # we can add support with a version check to avoid doing this on Opera
4794 # versions where it will be a problem. Reported to Opera as
4795 # DSK-262266, but they don't have a public bug tracker for us to follow.
4796 /*
4797 if ( $wgMinimalPasswordLength > 1 ) {
4798 $ret['pattern'] = '.{' . intval( $wgMinimalPasswordLength ) . ',}';
4799 $ret['title'] = wfMessage( 'passwordtooshort' )
4800 ->numParams( $wgMinimalPasswordLength )->text();
4801 }
4802 */
4803
4804 return $ret;
4805 }
4806
4807 /**
4808 * Return the list of user fields that should be selected to create
4809 * a new user object.
4810 * @return array
4811 */
4812 public static function selectFields() {
4813 return array(
4814 'user_id',
4815 'user_name',
4816 'user_real_name',
4817 'user_password',
4818 'user_newpassword',
4819 'user_newpass_time',
4820 'user_email',
4821 'user_touched',
4822 'user_token',
4823 'user_email_authenticated',
4824 'user_email_token',
4825 'user_email_token_expires',
4826 'user_password_expires',
4827 'user_registration',
4828 'user_editcount',
4829 );
4830 }
4831
4832 /**
4833 * Factory function for fatal permission-denied errors
4834 *
4835 * @since 1.22
4836 * @param string $permission User right required
4837 * @return Status
4838 */
4839 static function newFatalPermissionDeniedStatus( $permission ) {
4840 global $wgLang;
4841
4842 $groups = array_map(
4843 array( 'User', 'makeGroupLinkWiki' ),
4844 User::getGroupsWithPermission( $permission )
4845 );
4846
4847 if ( $groups ) {
4848 return Status::newFatal( 'badaccess-groups', $wgLang->commaList( $groups ), count( $groups ) );
4849 } else {
4850 return Status::newFatal( 'badaccess-group0' );
4851 }
4852 }
4853 }