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