use MediaWiki\Auth\AuthenticationRequest;
use MediaWiki\User\UserIdentity;
use MediaWiki\Logger\LoggerFactory;
+use Wikimedia\Assert\Assert;
use Wikimedia\IPSet;
use Wikimedia\ScopedCallback;
use Wikimedia\Rdbms\Database;
* - forceChange (bool): if set to true, the user should not be
* allowed to log with this password unless they change it during
* the login process (see ResetPasswordSecondaryAuthenticationProvider).
+ * - suggestChangeOnLogin (bool): if set to true, the user should be prompted for
+ * a password change on login.
*
* @param string $password Desired password
* @return Status
return false;
}
- // Reject various classes of invalid names
- $name = AuthManager::callLegacyAuthPlugin(
- 'getCanonicalName', [ $t->getText() ], $t->getText()
- );
+ $name = $t->getText();
switch ( $validate ) {
case false:
*/
public function trackBlockWithCookie() {
$block = $this->getBlock();
- if ( $block && $this->getRequest()->getCookie( 'BlockID' ) === null ) {
- $config = RequestContext::getMain()->getConfig();
- $shouldSetCookie = false;
-
- if ( $this->isAnon() && $config->get( 'CookieSetOnIpBlock' ) ) {
- // If user is logged-out, set a cookie to track the Block
- $shouldSetCookie = in_array( $block->getType(), [
- Block::TYPE_IP, Block::TYPE_RANGE
- ] );
- if ( $shouldSetCookie ) {
- $block->setCookie( $this->getRequest()->response() );
- // temporary measure the use of cookies on ip blocks
- $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
- $stats->increment( 'block.ipblock.setCookie.success' );
- }
- } elseif ( $this->isLoggedIn() && $config->get( 'CookieSetOnAutoblock' ) ) {
- $shouldSetCookie = $block->getType() === Block::TYPE_USER && $block->isAutoblocking();
- if ( $shouldSetCookie ) {
- $block->setCookie( $this->getRequest()->response() );
- }
- }
+ if ( $block && $this->getRequest()->getCookie( 'BlockID' ) === null
+ && $block->shouldTrackWithCookie( $this->isAnon() )
+ ) {
+ $block->setCookie( $this->getRequest()->response() );
}
}
// update groups in external authentication database
Hooks::run( 'UserGroupsChanged', [ $this, $toPromote, [], false, false, $oldUGMs, $newUGMs ] );
- AuthManager::callLegacyAuthPlugin( 'updateExternalDBGroups', [ $this, $toPromote ] );
$logEntry = new ManualLogEntry( 'rights', 'autopromote' );
$logEntry->setPerformer( $this );
* protected against race conditions using a compare-and-set (CAS) mechanism
* based on comparing $this->mTouched with the user_touched field.
*
- * @param Database $db
+ * @param IDatabase $db
* @param array $conditions WHERE conditions for use with Database::update
* @return array WHERE conditions for use with Database::update
*/
- protected function makeUpdateConditions( Database $db, array $conditions ) {
+ protected function makeUpdateConditions( IDatabase $db, array $conditions ) {
if ( $this->mTouched ) {
// CAS check: only update if the row wasn't changed sicne it was loaded.
$conditions['user_touched'] = $db->timestamp( $this->mTouched );
}
}
+ /** @var array|null */
+ private static $defOpt = null;
+ /** @var string|null */
+ private static $defOptLang = null;
+
+ /**
+ * Reset the process cache of default user options. This is only necessary
+ * if the wiki configuration has changed since defaults were calculated,
+ * and as such should only be performed inside the testing suite that
+ * regularly changes wiki configuration.
+ */
+ public static function resetGetDefaultOptionsForTestsOnly() {
+ Assert::invariant( defined( 'MW_PHPUNIT_TEST' ), 'Unit tests only' );
+ self::$defOpt = null;
+ self::$defOptLang = null;
+ }
+
/**
* Combine the language default options with any site-specific options
* and add the default language variants.
public static function getDefaultOptions() {
global $wgNamespacesToBeSearchedDefault, $wgDefaultUserOptions, $wgDefaultSkin;
- static $defOpt = null;
- static $defOptLang = null;
-
$contLang = MediaWikiServices::getInstance()->getContentLanguage();
- if ( $defOpt !== null && $defOptLang === $contLang->getCode() ) {
+ if ( self::$defOpt !== null && self::$defOptLang === $contLang->getCode() ) {
// The content language does not change (and should not change) mid-request, but the
// unit tests change it anyway, and expect this method to return values relevant to the
// current content language.
- return $defOpt;
+ return self::$defOpt;
}
- $defOpt = $wgDefaultUserOptions;
+ self::$defOpt = $wgDefaultUserOptions;
// Default language setting
- $defOptLang = $contLang->getCode();
- $defOpt['language'] = $defOptLang;
+ self::$defOptLang = $contLang->getCode();
+ self::$defOpt['language'] = self::$defOptLang;
foreach ( LanguageConverter::$languagesWithVariants as $langCode ) {
if ( $langCode === $contLang->getCode() ) {
- $defOpt['variant'] = $langCode;
+ self::$defOpt['variant'] = $langCode;
} else {
- $defOpt["variant-$langCode"] = $langCode;
+ self::$defOpt["variant-$langCode"] = $langCode;
}
}
// since extensions may change the set of searchable namespaces depending
// on user groups/permissions.
foreach ( $wgNamespacesToBeSearchedDefault as $nsnum => $val ) {
- $defOpt['searchNs' . $nsnum] = (bool)$val;
+ self::$defOpt['searchNs' . $nsnum] = (bool)$val;
}
- $defOpt['skin'] = Skin::normalizeKey( $wgDefaultSkin );
+ self::$defOpt['skin'] = Skin::normalizeKey( $wgDefaultSkin );
- Hooks::run( 'UserGetDefaultOptions', [ &$defOpt ] );
+ Hooks::run( 'UserGetDefaultOptions', [ &self::$defOpt ] );
- return $defOpt;
+ return self::$defOpt;
}
/**
/**
* Get blocking information
- * @param bool $bFromReplica Whether to check the replica DB first.
+ * @param bool $fromReplica Whether to check the replica DB first.
* To improve performance, non-critical checks are done against replica DBs.
* Check when actually saving should be done against master.
*/
- private function getBlockedStatus( $bFromReplica = true ) {
- global $wgProxyWhitelist, $wgUser, $wgApplyIpBlocksToXff, $wgSoftBlockRanges;
+ private function getBlockedStatus( $fromReplica = true ) {
+ global $wgProxyWhitelist, $wgApplyIpBlocksToXff, $wgSoftBlockRanges;
if ( $this->mBlockedby != -1 ) {
return;
# user is not immune to autoblocks/hardblocks, and they are the current user so we
# know which IP address they're actually coming from
$ip = null;
- if ( !$this->isAllowed( 'ipblock-exempt' ) ) {
- // $wgUser->getName() only works after the end of Setup.php. Until
- // then, assume it's a logged-out user.
- $globalUserName = $wgUser->isSafeToLoad()
- ? $wgUser->getName()
- : IP::sanitizeIP( $wgUser->getRequest()->getIP() );
- if ( $this->getName() === $globalUserName ) {
- $ip = $this->getRequest()->getIP();
- }
+ $sessionUser = RequestContext::getMain()->getUser();
+ // the session user is set up towards the end of Setup.php. Until then,
+ // assume it's a logged-out user.
+ $globalUserName = $sessionUser->isSafeToLoad()
+ ? $sessionUser->getName()
+ : IP::sanitizeIP( $sessionUser->getRequest()->getIP() );
+ if ( $this->getName() === $globalUserName && !$this->isAllowed( 'ipblock-exempt' ) ) {
+ $ip = $this->getRequest()->getIP();
}
// User/IP blocking
- $block = Block::newFromTarget( $this, $ip, !$bFromReplica );
+ $block = Block::newFromTarget( $this, $ip, !$fromReplica );
// Cookie blocking
if ( !$block instanceof Block ) {
$xff = $this->getRequest()->getHeader( 'X-Forwarded-For' );
$xff = array_map( 'trim', explode( ',', $xff ) );
$xff = array_diff( $xff, [ $ip ] );
- $xffblocks = Block::getBlocksForIPList( $xff, $this->isAnon(), !$bFromReplica );
+ $xffblocks = Block::getBlocksForIPList( $xff, $this->isAnon(), !$fromReplica );
$block = Block::chooseBlock( $xffblocks, $xff );
if ( $block instanceof Block ) {
# Mangle the reason to alert the user that the block
# originated from matching the X-Forwarded-For header.
- $block->mReason = wfMessage( 'xffblockreason', $block->mReason )->plain();
+ $block->setReason( wfMessage( 'xffblockreason', $block->getReason() )->plain() );
}
}
wfDebug( __METHOD__ . ": Found block.\n" );
$this->mBlock = $block;
$this->mBlockedby = $block->getByName();
- $this->mBlockreason = $block->mReason;
- $this->mHideName = $block->mHideName;
+ $this->mBlockreason = $block->getReason();
+ $this->mHideName = $block->getHideName();
$this->mAllowUsertalk = $block->isUsertalkEditAllowed();
} else {
$this->mBlock = null;
}
// Avoid PHP 7.1 warning of passing $this by reference
- $user = $this;
+ $thisUser = $this;
// Extensions
- Hooks::run( 'GetBlockedStatus', [ &$user ] );
+ Hooks::run( 'GetBlockedStatus', [ &$thisUser ] );
}
/**
// Set the user limit key
if ( $userLimit !== false ) {
+ // phan is confused because &can-bypass's value is a bool, so it assumes
+ // that $userLimit is also a bool here.
+ // @phan-suppress-next-line PhanTypeInvalidExpressionArrayDestructuring
list( $max, $period ) = $userLimit;
wfDebug( __METHOD__ . ": effective user limit: $max in {$period}s\n" );
$keys[$cache->makeKey( 'limiter', $action, 'user', $id )] = $userLimit;
$triggered = false;
foreach ( $keys as $key => $limit ) {
+ // phan is confused because &can-bypass's value is a bool, so it assumes
+ // that $userLimit is also a bool here.
+ // @phan-suppress-next-line PhanTypeInvalidExpressionArrayDestructuring
list( $max, $period ) = $limit;
$summary = "(limit $max in {$period}s)";
$count = $cache->get( $key );
/**
* Check if user is blocked
*
- * @param bool $bFromReplica Whether to check the replica DB instead of
+ * @param bool $fromReplica Whether to check the replica DB instead of
* the master. Hacked from false due to horrible probs on site.
* @return bool True if blocked, false otherwise
*/
- public function isBlocked( $bFromReplica = true ) {
- return $this->getBlock( $bFromReplica ) instanceof Block &&
+ public function isBlocked( $fromReplica = true ) {
+ return $this->getBlock( $fromReplica ) instanceof Block &&
$this->getBlock()->appliesToRight( 'edit' );
}
/**
* Get the block affecting the user, or null if the user is not blocked
*
- * @param bool $bFromReplica Whether to check the replica DB instead of the master
+ * @param bool $fromReplica Whether to check the replica DB instead of the master
* @return Block|null
*/
- public function getBlock( $bFromReplica = true ) {
- $this->getBlockedStatus( $bFromReplica );
+ public function getBlock( $fromReplica = true ) {
+ $this->getBlockedStatus( $fromReplica );
return $this->mBlock instanceof Block ? $this->mBlock : null;
}
* @param Title $title Title to check
* @param bool $fromReplica Whether to check the replica DB instead of the master
* @return bool
+ * @throws MWException
+ *
+ * @deprecated since 1.33,
+ * use MediaWikiServices::getInstance()->getPermissionManager()->isBlockedFrom(..)
+ *
*/
public function isBlockedFrom( $title, $fromReplica = false ) {
- $blocked = $this->isHidden();
-
- if ( !$blocked ) {
- $block = $this->getBlock( $fromReplica );
- if ( $block ) {
- // Special handling for a user's own talk page. The block is not aware
- // of the user, so this must be done here.
- if ( $title->equals( $this->getTalkPage() ) ) {
- $blocked = $block->appliesToUsertalk( $title );
- } else {
- $blocked = $block->appliesToTitle( $title );
- }
- }
- }
-
- // only for the purpose of the hook. We really don't need this here.
- $allowUsertalk = $this->mAllowUsertalk;
-
- Hooks::run( 'UserIsBlockedFrom', [ $this, $title, &$blocked, &$allowUsertalk ] );
-
- return $blocked;
+ return MediaWikiServices::getInstance()->getPermissionManager()
+ ->isBlockedFrom( $this, $title, $fromReplica );
}
/**
if ( $this->mLocked !== null ) {
return $this->mLocked;
}
- // Avoid PHP 7.1 warning of passing $this by reference
- $user = $this;
- $authUser = AuthManager::callLegacyAuthPlugin( 'getUserInstance', [ &$user ], null );
- $this->mLocked = $authUser && $authUser->isLocked();
+ // Reset for hook
+ $this->mLocked = false;
Hooks::run( 'UserIsLocked', [ $this, &$this->mLocked ] );
return $this->mLocked;
}
}
$this->getBlockedStatus();
if ( !$this->mHideName ) {
- // Avoid PHP 7.1 warning of passing $this by reference
- $user = $this;
- $authUser = AuthManager::callLegacyAuthPlugin( 'getUserInstance', [ &$user ], null );
- $this->mHideName = $authUser && $authUser->isHidden();
+ // Reset for hook
+ $this->mHideName = false;
Hooks::run( 'UserIsHidden', [ $this, &$this->mHideName ] );
}
return (bool)$this->mHideName;
/**
* Generate a current or new-future timestamp to be stored in the
* user_touched field when we update things.
+ *
* @return string Timestamp in TS_MW format
*/
private function newTouchedTimestamp() {
- global $wgClockSkewFudge;
-
- $time = wfTimestamp( TS_MW, time() + $wgClockSkewFudge );
- if ( $this->mTouched && $time <= $this->mTouched ) {
- $time = wfTimestamp( TS_MW, wfTimestamp( TS_UNIX, $this->mTouched ) + 1 );
+ $time = time();
+ if ( $this->mTouched ) {
+ $time = max( $time, wfTimestamp( TS_UNIX, $this->mTouched ) + 1 );
}
- return $time;
+ return wfTimestamp( TS_MW, $time );
}
/**
Hooks::run( 'UserSaveSettings', [ $this ] );
$this->clearSharedCache();
- $this->getUserPage()->invalidateCache();
+ $this->getUserPage()->purgeSquid();
}
/**
[ 'LOCK IN SHARE MODE' ]
);
$loaded = false;
- if ( $this->mId ) {
- if ( $this->loadFromDatabase( self::READ_LOCKING ) ) {
- $loaded = true;
- }
+ if ( $this->mId && $this->loadFromDatabase( self::READ_LOCKING ) ) {
+ $loaded = true;
}
if ( !$loaded ) {
throw new MWException( $fname . ": hit a key conflict attempting " .
if ( $type == 'created' || $type === false ) {
$message = 'confirmemail_body';
+ $type = 'created';
} elseif ( $type === true ) {
$message = 'confirmemail_body_changed';
+ $type = 'changed';
} else {
// Messages: confirmemail_body_changed, confirmemail_body_set
$message = 'confirmemail_body_' . $type;
}
- return $this->sendMail( wfMessage( 'confirmemail_subject' )->text(),
- wfMessage( $message,
+ $mail = [
+ 'subject' => wfMessage( 'confirmemail_subject' )->text(),
+ 'body' => wfMessage( $message,
$this->getRequest()->getIP(),
$this->getName(),
$url,
$wgLang->userTimeAndDate( $expiration, $this ),
$invalidateURL,
$wgLang->userDate( $expiration, $this ),
- $wgLang->userTime( $expiration, $this ) )->text() );
+ $wgLang->userTime( $expiration, $this ) )->text(),
+ 'from' => null,
+ 'replyTo' => null,
+ ];
+ $info = [
+ 'type' => $type,
+ 'ip' => $this->getRequest()->getIP(),
+ 'confirmURL' => $url,
+ 'invalidateURL' => $invalidateURL,
+ 'expiration' => $expiration
+ ];
+
+ Hooks::run( 'UserSendConfirmationMail', [ $this, &$mail, $info ] );
+ return $this->sendMail( $mail['subject'], $mail['body'], $mail['from'], $mail['replyTo'] );
}
/**
* @param string $body Message body
* @param User|null $from Optional sending user; if unspecified, default
* $wgPasswordSender will be used.
- * @param string|null $replyto Reply-To address
+ * @param MailAddress|null $replyto Reply-To address
* @return Status
*/
public function sendMail( $subject, $body, $from = null, $replyto = null ) {
* non-existent/anonymous user accounts.
*/
public function getFirstEditTimestamp() {
+ return $this->getEditTimestamp( true );
+ }
+
+ /**
+ * Get the timestamp of the latest edit
+ *
+ * @since 1.33
+ * @return string|bool Timestamp of first edit, or false for
+ * non-existent/anonymous user accounts.
+ */
+ public function getLatestEditTimestamp() {
+ return $this->getEditTimestamp( false );
+ }
+
+ /**
+ * Get the timestamp of the first or latest edit
+ *
+ * @param bool $first True for the first edit, false for the latest one
+ * @return string|bool Timestamp of first or latest edit, or false for
+ * non-existent/anonymous user accounts.
+ */
+ private function getEditTimestamp( $first ) {
if ( $this->getId() == 0 ) {
return false; // anons
}
$actorWhere = ActorMigration::newMigration()->getWhere( $dbr, 'rev_user', $this );
$tsField = isset( $actorWhere['tables']['temp_rev_user'] )
? 'revactor_timestamp' : 'rev_timestamp';
+ $sortOrder = $first ? 'ASC' : 'DESC';
$time = $dbr->selectField(
[ 'revision' ] + $actorWhere['tables'],
$tsField,
[ $actorWhere['conds'] ],
__METHOD__,
- [ 'ORDER BY' => "$tsField ASC" ],
+ [ 'ORDER BY' => "$tsField $sortOrder" ],
$actorWhere['joins']
);
if ( !$time ) {
// XXX it's not clear whether central ID providers are supposed to obey this
return $this->getName() === $user->getName();
}
+
+ /**
+ * Checks if usertalk is allowed
+ *
+ * @return bool
+ */
+ public function isAllowUsertalk() {
+ return $this->mAllowUsertalk;
+ }
+
}