From 63601400e476c6cf43d985f3e7b9864681695ed4 Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Fri, 18 Jan 2013 16:46:04 +0100 Subject: Update to MediaWiki 1.20.2 this update includes: * adjusted Arch Linux skin * updated FluxBBAuthPlugin * patch for https://bugzilla.wikimedia.org/show_bug.cgi?id=44024 --- includes/User.php | 326 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 218 insertions(+), 108 deletions(-) (limited to 'includes/User.php') diff --git a/includes/User.php b/includes/User.php index 1529da1e..0a3db4c0 100644 --- a/includes/User.php +++ b/includes/User.php @@ -65,6 +65,11 @@ class User { const MW_USER_VERSION = MW_USER_VERSION; const EDIT_TOKEN_SUFFIX = EDIT_TOKEN_SUFFIX; + /** + * Maximum items in $mWatchedItems + */ + const MAX_WATCHED_ITEMS_CACHE = 100; + /** * Array of Strings List of member variables which are saved to the * shared cache (memcached). Any operation which changes the @@ -114,9 +119,11 @@ class User { 'delete', 'deletedhistory', 'deletedtext', + 'deletelogentry', 'deleterevision', 'edit', 'editinterface', + 'editprotected', 'editusercssjs', #deprecated 'editusercss', 'edituserjs', @@ -134,12 +141,15 @@ class User { 'nominornewtalk', 'noratelimit', 'override-export-depth', + 'passwordreset', 'patrol', + 'patrolmarks', 'protect', 'proxyunbannable', 'purge', 'read', 'reupload', + 'reupload-own', 'reupload-shared', 'rollback', 'sendemail', @@ -165,8 +175,8 @@ class User { //@{ var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime, $mEmail, $mTouched, $mToken, $mEmailAuthenticated, - $mEmailToken, $mEmailTokenExpires, $mRegistration, $mGroups, $mOptionOverrides, - $mCookiePassword, $mEditCount, $mAllowUsertalk; + $mEmailToken, $mEmailTokenExpires, $mRegistration, $mEditCount, + $mGroups, $mOptionOverrides; //@} /** @@ -209,11 +219,21 @@ class User { */ var $mBlock; + /** + * @var bool + */ + var $mAllowUsertalk; + /** * @var Block */ private $mBlockedFromCreateAccount = false; + /** + * @var Array + */ + private $mWatchedItems = array(); + static $idCacheByName = array(); /** @@ -445,22 +465,20 @@ class User { /** * Get the username corresponding to a given user ID * @param $id Int User ID - * @return String|false The corresponding username + * @return String|bool The corresponding username */ public static function whoIs( $id ) { - $dbr = wfGetDB( DB_SLAVE ); - return $dbr->selectField( 'user', 'user_name', array( 'user_id' => $id ), __METHOD__ ); + return UserCache::singleton()->getProp( $id, 'name' ); } /** * Get the real name of a user given their user ID * * @param $id Int User ID - * @return String|false The corresponding user's real name + * @return String|bool The corresponding user's real name */ public static function whoIsReal( $id ) { - $dbr = wfGetDB( DB_SLAVE ); - return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ), __METHOD__ ); + return UserCache::singleton()->getProp( $id, 'real_name' ); } /** @@ -512,7 +530,7 @@ class User { * as 300.300.300.300 will return true because it looks like an IP * address, despite not being strictly valid. * - * We match \d{1,3}\.\d{1,3}\.\d{1,3}\.xxx as an anonymous IP + * We match "\d{1,3}\.\d{1,3}\.\d{1,3}\.xxx" as an anonymous IP * address because the usemod software would "cloak" anonymous IP * addresses like this, if we allowed accounts like this to be created * new users could get the old edits of these anonymous users. @@ -606,7 +624,7 @@ class User { // Certain names may be reserved for batch processes. foreach ( $reservedUsernames as $reserved ) { if ( substr( $reserved, 0, 4 ) == 'msg:' ) { - $reserved = wfMsgForContent( substr( $reserved, 4 ) ); + $reserved = wfMessage( substr( $reserved, 4 ) )->inContentLanguage()->text(); } if ( $reserved == $name ) { return false; @@ -1023,7 +1041,7 @@ class User { } $dbr = wfGetDB( DB_MASTER ); - $s = $dbr->selectRow( 'user', '*', array( 'user_id' => $this->mId ), __METHOD__ ); + $s = $dbr->selectRow( 'user', self::selectFields(), array( 'user_id' => $this->mId ), __METHOD__ ); wfRunHooks( 'UserLoadFromDatabase', array( $this, &$s ) ); @@ -1278,22 +1296,22 @@ class User { } # User/IP blocking - $block = Block::newFromTarget( $this->getName(), $ip, !$bFromSlave ); + $block = Block::newFromTarget( $this, $ip, !$bFromSlave ); # Proxy blocking if ( !$block instanceof Block && $ip !== null && !$this->isAllowed( 'proxyunbannable' ) - && !in_array( $ip, $wgProxyWhitelist ) ) + && !in_array( $ip, $wgProxyWhitelist ) ) { # Local list if ( self::isLocallyBlockedProxy( $ip ) ) { $block = new Block; - $block->setBlocker( wfMsg( 'proxyblocker' ) ); - $block->mReason = wfMsg( 'proxyblockreason' ); + $block->setBlocker( wfMessage( 'proxyblocker' )->text() ); + $block->mReason = wfMessage( 'proxyblockreason' )->text(); $block->setTarget( $ip ); } elseif ( $this->isAnon() && $this->isDnsBlacklisted( $ip ) ) { $block = new Block; - $block->setBlocker( wfMsg( 'sorbs' ) ); - $block->mReason = wfMsg( 'sorbsreason' ); + $block->setBlocker( wfMessage( 'sorbs' )->text() ); + $block->mReason = wfMessage( 'sorbsreason' )->text(); $block->setTarget( $ip ); } } @@ -1372,11 +1390,11 @@ class User { $ipList = gethostbynamel( $host ); if( $ipList ) { - wfDebug( "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" ); + wfDebugLog( 'dnsblacklist', "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" ); $found = true; break; } else { - wfDebug( "Requested $host, not found in $base.\n" ); + wfDebugLog( 'dnsblacklist', "Requested $host, not found in $base.\n" ); } } } @@ -1512,7 +1530,7 @@ class User { $count = $wgMemc->get( $key ); // Already pinged? if( $count ) { - if( $count > $max ) { + if( $count >= $max ) { wfDebug( __METHOD__ . ": tripped! $key at $count $summary\n" ); if( $wgRateLimitLog ) { wfSuppressWarnings(); @@ -1748,16 +1766,22 @@ class User { # Check memcached separately for anons, who have no # entire User object stored in there. if( !$this->mId ) { - global $wgMemc; - $key = wfMemcKey( 'newtalk', 'ip', $this->getName() ); - $newtalk = $wgMemc->get( $key ); - if( strval( $newtalk ) !== '' ) { - $this->mNewtalk = (bool)$newtalk; + global $wgDisableAnonTalk; + if( $wgDisableAnonTalk ) { + // Anon newtalk disabled by configuration. + $this->mNewtalk = false; } else { - // Since we are caching this, make sure it is up to date by getting it - // from the master - $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName(), true ); - $wgMemc->set( $key, (int)$this->mNewtalk, 1800 ); + global $wgMemc; + $key = wfMemcKey( 'newtalk', 'ip', $this->getName() ); + $newtalk = $wgMemc->get( $key ); + if( strval( $newtalk ) !== '' ) { + $this->mNewtalk = (bool)$newtalk; + } else { + // Since we are caching this, make sure it is up to date by getting it + // from the master + $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName(), true ); + $wgMemc->set( $key, (int)$this->mNewtalk, 1800 ); + } } } else { $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId ); @@ -1773,14 +1797,20 @@ class User { */ public function getNewMessageLinks() { $talks = array(); - if( !wfRunHooks( 'UserRetrieveNewTalks', array( &$this, &$talks ) ) ) + if( !wfRunHooks( 'UserRetrieveNewTalks', array( &$this, &$talks ) ) ) { return $talks; - - if( !$this->getNewtalk() ) + } elseif( !$this->getNewtalk() ) { return array(); - $up = $this->getUserPage(); - $utp = $up->getTalkPage(); - return array( array( 'wiki' => wfWikiID(), 'link' => $utp->getLocalURL() ) ); + } + $utp = $this->getTalkPage(); + $dbr = wfGetDB( DB_SLAVE ); + // Get the "last viewed rev" timestamp from the oldest message notification + $timestamp = $dbr->selectField( 'user_newtalk', + 'MIN(user_last_timestamp)', + $this->isAnon() ? array( 'user_ip' => $this->getName() ) : array( 'user_id' => $this->getID() ), + __METHOD__ ); + $rev = $timestamp ? Revision::loadFromTimestamp( $dbr, $utp, $timestamp ) : null; + return array( array( 'wiki' => wfWikiID(), 'link' => $utp->getLocalURL(), 'rev' => $rev ) ); } /** @@ -1807,12 +1837,17 @@ class User { * Add or update the new messages flag * @param $field String 'user_ip' for anonymous users, 'user_id' otherwise * @param $id String|Int User's IP address for anonymous users, User ID otherwise + * @param $curRev Revision new, as yet unseen revision of the user talk page. Ignored if null. * @return Bool True if successful, false otherwise */ - protected function updateNewtalk( $field, $id ) { + protected function updateNewtalk( $field, $id, $curRev = null ) { + // Get timestamp of the talk page revision prior to the current one + $prevRev = $curRev ? $curRev->getPrevious() : false; + $ts = $prevRev ? $prevRev->getTimestamp() : null; + // Mark the user as having new messages since this revision $dbw = wfGetDB( DB_MASTER ); $dbw->insert( 'user_newtalk', - array( $field => $id ), + array( $field => $id, 'user_last_timestamp' => $dbw->timestampOrNull( $ts ) ), __METHOD__, 'IGNORE' ); if ( $dbw->affectedRows() ) { @@ -1847,8 +1882,9 @@ class User { /** * Update the 'You have new messages!' status. * @param $val Bool Whether the user has new messages + * @param $curRev Revision new, as yet unseen revision of the user talk page. Ignored if null or !$val. */ - public function setNewtalk( $val ) { + public function setNewtalk( $val, $curRev = null ) { if( wfReadOnly() ) { return; } @@ -1866,7 +1902,7 @@ class User { global $wgMemc; if( $val ) { - $changed = $this->updateNewtalk( $field, $id ); + $changed = $this->updateNewtalk( $field, $id, $curRev ); } else { $changed = $this->deleteNewtalk( $field, $id ); } @@ -1921,10 +1957,19 @@ class User { $this->mTouched = self::newTouchedTimestamp(); $dbw = wfGetDB( DB_MASTER ); - $dbw->update( 'user', - array( 'user_touched' => $dbw->timestamp( $this->mTouched ) ), - array( 'user_id' => $this->mId ), - __METHOD__ ); + + // Prevent contention slams by checking user_touched first + $now = $dbw->timestamp( $this->mTouched ); + $needsPurge = $dbw->selectField( 'user', '1', + array( 'user_id' => $this->mId, 'user_touched < ' . $dbw->addQuotes( $now ) ) + ); + if ( $needsPurge ) { + $dbw->update( 'user', + array( 'user_touched' => $now ), + array( 'user_id' => $this->mId, 'user_touched < ' . $dbw->addQuotes( $now ) ), + __METHOD__ + ); + } $this->clearSharedCache(); } @@ -1971,7 +2016,7 @@ class User { if( $str !== null ) { if( !$wgAuth->allowPasswordChange() ) { - throw new PasswordError( wfMsg( 'password-change-forbidden' ) ); + throw new PasswordError( wfMessage( 'password-change-forbidden' )->text() ); } if( !$this->isValidPassword( $str ) ) { @@ -1984,12 +2029,12 @@ class User { $message = $valid; $params = array( $wgMinimalPasswordLength ); } - throw new PasswordError( wfMsgExt( $message, array( 'parsemag' ), $params ) ); + throw new PasswordError( wfMessage( $message, $params )->text() ); } } if( !$wgAuth->setPassword( $this, $str ) ) { - throw new PasswordError( wfMsg( 'externaldberror' ) ); + throw new PasswordError( wfMessage( 'externaldberror' )->text() ); } $this->setInternalPassword( $str ); @@ -2036,7 +2081,6 @@ class User { * @param $token String|bool If specified, set the token to this value */ public function setToken( $token = false ) { - global $wgSecretKey, $wgProxyKey; $this->load(); if ( !$token ) { $this->mToken = MWCryptRand::generateHex( USER_TOKEN_LENGTH ); @@ -2045,16 +2089,6 @@ class User { } } - /** - * Set the cookie password - * - * @param $str String New cookie password - */ - private function setCookiePassword( $str ) { - $this->load(); - $this->mCookiePassword = md5( $str ); - } - /** * Set the password for a password reminder or new account email * @@ -2118,6 +2152,42 @@ class User { wfRunHooks( 'UserSetEmail', array( $this, &$this->mEmail ) ); } + /** + * Set the user's e-mail address and a confirmation mail if needed. + * + * @since 1.20 + * @param $str String New e-mail address + * @return Status + */ + public function setEmailWithConfirmation( $str ) { + global $wgEnableEmail, $wgEmailAuthentication; + + if ( !$wgEnableEmail ) { + return Status::newFatal( 'emaildisabled' ); + } + + $oldaddr = $this->getEmail(); + if ( $str === $oldaddr ) { + return Status::newGood( true ); + } + + $this->setEmail( $str ); + + if ( $str !== '' && $wgEmailAuthentication ) { + # Send a confirmation request to the new address if needed + $type = $oldaddr != '' ? 'changed' : 'set'; + $result = $this->sendConfirmationMail( $type ); + if ( $result->isGood() ) { + # Say the the caller that a confirmation mail has been sent + $result->value = 'eauth'; + } + } else { + $result = Status::newGood( true ); + } + + return $result; + } + /** * Get the user's real name * @return String User's real name @@ -2239,9 +2309,11 @@ class User { $this->loadOptions(); // Explicitly NULL values should refer to defaults - global $wgDefaultUserOptions; - if( is_null( $val ) && isset( $wgDefaultUserOptions[$oname] ) ) { - $val = $wgDefaultUserOptions[$oname]; + if( is_null( $val ) ) { + $defaultOption = self::getDefaultOption( $oname ); + if( !is_null( $defaultOption ) ) { + $val = $defaultOption; + } } $this->mOptions[$oname] = $val; @@ -2251,7 +2323,10 @@ class User { * Reset all options to the site defaults */ public function resetOptions() { + $this->load(); + $this->mOptions = self::getDefaultOptions(); + $this->mOptionsLoaded = true; } /** @@ -2571,14 +2646,34 @@ class User { return RequestContext::getMain()->getSkin(); } + /** + * Get a WatchedItem for this user and $title. + * + * @param $title Title + * @return WatchedItem + */ + public function getWatchedItem( $title ) { + $key = $title->getNamespace() . ':' . $title->getDBkey(); + + if ( isset( $this->mWatchedItems[$key] ) ) { + return $this->mWatchedItems[$key]; + } + + if ( count( $this->mWatchedItems ) >= self::MAX_WATCHED_ITEMS_CACHE ) { + $this->mWatchedItems = array(); + } + + $this->mWatchedItems[$key] = WatchedItem::fromUserTitle( $this, $title ); + return $this->mWatchedItems[$key]; + } + /** * Check the watched status of an article. * @param $title Title of the article to look at * @return Bool */ public function isWatched( $title ) { - $wl = WatchedItem::fromUserTitle( $this, $title ); - return $wl->isWatched(); + return $this->getWatchedItem( $title )->isWatched(); } /** @@ -2586,8 +2681,7 @@ class User { * @param $title Title of the article to look at */ public function addWatch( $title ) { - $wl = WatchedItem::fromUserTitle( $this, $title ); - $wl->addWatch(); + $this->getWatchedItem( $title )->addWatch(); $this->invalidateCache(); } @@ -2596,8 +2690,7 @@ class User { * @param $title Title of the article to look at */ public function removeWatch( $title ) { - $wl = WatchedItem::fromUserTitle( $this, $title ); - $wl->removeWatch(); + $this->getWatchedItem( $title )->removeWatch(); $this->invalidateCache(); } @@ -2635,28 +2728,14 @@ class User { // The query to find out if it is watched is cached both in memcached and per-invocation, // and when it does have to be executed, it can be on a slave // If this is the user's newtalk page, we always update the timestamp - if( $title->getNamespace() == NS_USER_TALK && + $force = ''; + if ( $title->getNamespace() == NS_USER_TALK && $title->getText() == $this->getName() ) { - $watched = true; - } else { - $watched = $this->isWatched( $title ); + $force = 'force'; } - // If the page is watched by the user (or may be watched), update the timestamp on any - // any matching rows - if ( $watched ) { - $dbw = wfGetDB( DB_MASTER ); - $dbw->update( 'watchlist', - array( /* SET */ - 'wl_notificationtimestamp' => null - ), array( /* WHERE */ - 'wl_title' => $title->getDBkey(), - 'wl_namespace' => $title->getNamespace(), - 'wl_user' => $this->getID() - ), __METHOD__ - ); - } + $this->getWatchedItem( $title )->resetNotificationTimestamp( $force ); } /** @@ -2903,6 +2982,7 @@ class User { 'user_token' => strval( $user->mToken ), 'user_registration' => $dbw->timestamp( $user->mRegistration ), 'user_editcount' => 0, + 'user_touched' => $dbw->timestamp( self::newTouchedTimestamp() ), ); foreach ( $params as $name => $value ) { $fields["user_$name"] = $value; @@ -2921,6 +3001,9 @@ class User { */ public function addToDatabase() { $this->load(); + + $this->mTouched = self::newTouchedTimestamp(); + $dbw = wfGetDB( DB_MASTER ); $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' ); $dbw->insert( 'user', @@ -2936,6 +3019,7 @@ class User { 'user_token' => strval( $this->mToken ), 'user_registration' => $dbw->timestamp( $this->mRegistration ), 'user_editcount' => 0, + 'user_touched' => $dbw->timestamp( $this->mTouched ), ), __METHOD__ ); $this->mId = $dbw->insertId(); @@ -2994,7 +3078,7 @@ class User { */ public function getPageRenderingHash() { wfDeprecated( __METHOD__, '1.17' ); - + global $wgUseDynamicDates, $wgRenderHashAppend, $wgLang, $wgContLang; if( $this->mHash ){ return $this->mHash; @@ -3212,6 +3296,7 @@ class User { * * @param $salt String Optional salt value * @return String The new random token + * @deprecated since 1.20; Use MWCryptRand for secure purposes or wfRandomString for pesudo-randomness */ public static function generateToken( $salt = '' ) { return MWCryptRand::generateHex( 32 ); @@ -3273,15 +3358,15 @@ class User { $message = 'confirmemail_body_' . $type; } - return $this->sendMail( wfMsg( 'confirmemail_subject' ), - wfMsg( $message, + return $this->sendMail( wfMessage( 'confirmemail_subject' )->text(), + wfMessage( $message, $this->getRequest()->getIP(), $this->getName(), $url, $wgLang->timeanddate( $expiration, false ), $invalidateURL, $wgLang->date( $expiration, false ), - $wgLang->time( $expiration, false ) ) ); + $wgLang->time( $expiration, false ) )->text() ); } /** @@ -3344,7 +3429,7 @@ class User { * @return String New token URL */ private function invalidationTokenUrl( $token ) { - return $this->getTokenUrl( 'Invalidateemail', $token ); + return $this->getTokenUrl( 'InvalidateEmail', $token ); } /** @@ -3372,7 +3457,7 @@ class User { * * @note Call saveSettings() after calling this function to commit the change. * - * @return true + * @return bool */ public function confirmEmail() { $this->setEmailAuthenticationTimestamp( wfTimestampNow() ); @@ -3385,7 +3470,7 @@ class User { * address if it was already confirmed. * * @note Call saveSettings() after calling this function to commit the change. - * @return true + * @return bool Returns true */ function invalidateEmail() { $this->load(); @@ -3933,10 +4018,10 @@ class User { $action = 'create2'; if ( $byEmail ) { if ( $reason === '' ) { - $reason = wfMsgForContent( 'newuserlog-byemail' ); + $reason = wfMessage( 'newuserlog-byemail' )->inContentLanguage()->text(); } else { $reason = $wgContLang->commaList( array( - $reason, wfMsgForContent( 'newuserlog-byemail' ) ) ); + $reason, wfMessage( 'newuserlog-byemail' )->inContentLanguage()->text() ) ); } } } @@ -3953,7 +4038,7 @@ class User { * Add an autocreate newuser log entry for this user * Used by things like CentralAuth and perhaps other authplugins. * - * @return true + * @return bool */ public function addNewUserLogEntryAutoCreate() { global $wgNewUserLog; @@ -3961,7 +4046,7 @@ class User { return true; // disabled } $log = new LogPage( 'newusers', false ); - $log->addEntry( 'autocreate', $this->getUserPage(), '', array( $this->getId() ) ); + $log->addEntry( 'autocreate', $this->getUserPage(), '', array( $this->getId() ), $this ); return true; } @@ -3988,7 +4073,7 @@ class User { $res = $dbr->select( 'user_properties', - '*', + array( 'up_property', 'up_value' ), array( 'up_user' => $this->getId() ), __METHOD__ ); @@ -4011,13 +4096,9 @@ class User { protected function saveOptions() { global $wgAllowPrefChange; - $extuser = ExternalUser::newFromUser( $this ); - $this->loadOptions(); - $dbw = wfGetDB( DB_MASTER ); - - $insert_rows = array(); + // Not using getOptions(), to keep hidden preferences in database $saveOptions = $this->mOptions; // Allow hooks to abort, for instance to save to a global profile. @@ -4026,13 +4107,17 @@ class User { return; } + $extuser = ExternalUser::newFromUser( $this ); + $userId = $this->getId(); + $insert_rows = array(); foreach( $saveOptions as $key => $value ) { # Don't bother storing default values - if ( ( is_null( self::getDefaultOption( $key ) ) && - !( $value === false || is_null($value) ) ) || - $value != self::getDefaultOption( $key ) ) { + $defaultOption = self::getDefaultOption( $key ); + if ( ( is_null( $defaultOption ) && + !( $value === false || is_null( $value ) ) ) || + $value != $defaultOption ) { $insert_rows[] = array( - 'up_user' => $this->getId(), + 'up_user' => $userId, 'up_property' => $key, 'up_value' => $value, ); @@ -4049,7 +4134,8 @@ class User { } } - $dbw->delete( 'user_properties', array( 'up_user' => $this->getId() ), __METHOD__ ); + $dbw = wfGetDB( DB_MASTER ); + $dbw->delete( 'user_properties', array( 'up_user' => $userId ), __METHOD__ ); $dbw->insert( 'user_properties', $insert_rows, __METHOD__ ); } @@ -4104,11 +4190,35 @@ class User { /* if ( $wgMinimalPasswordLength > 1 ) { $ret['pattern'] = '.{' . intval( $wgMinimalPasswordLength ) . ',}'; - $ret['title'] = wfMsgExt( 'passwordtooshort', 'parsemag', - $wgMinimalPasswordLength ); + $ret['title'] = wfMessage( 'passwordtooshort' ) + ->numParams( $wgMinimalPasswordLength )->text(); } */ return $ret; } + + /** + * Return the list of user fields that should be selected to create + * a new user object. + * @return array + */ + public static function selectFields() { + return array( + 'user_id', + 'user_name', + 'user_real_name', + 'user_password', + 'user_newpassword', + 'user_newpass_time', + 'user_email', + 'user_touched', + 'user_token', + 'user_email_authenticated', + 'user_email_token', + 'user_email_token_expires', + 'user_registration', + 'user_editcount', + ); + } } -- cgit v1.2.2