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