Merge "Move EnqueueableDataUpdate to a separate file"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Tue, 23 Aug 2016 04:57:41 +0000 (04:57 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Tue, 23 Aug 2016 04:57:41 +0000 (04:57 +0000)
RELEASE-NOTES-1.28
includes/OutputPage.php
includes/db/Database.php
includes/db/DatabaseMysqlBase.php
includes/db/loadbalancer/LoadBalancer.php
includes/jobqueue/JobRunner.php
includes/user/User.php
tests/phpunit/includes/user/UserTest.php

index 5d88fbf..865e300 100644 (file)
@@ -52,6 +52,16 @@ production.
 ==== Removed and replaced external libraries ====
 
 === Bug fixes in 1.28 ===
+* (T137264) SECURITY: XSS in unclosed internal links
+* (T133147) SECURITY: Escape '<' and ']]>' in inline <style> blocks
+* (T133147) SECURITY: Require login to preview user CSS pages
+* (T132926) SECURITY: Do not allow undeleting a revision deleted file if it is
+  the top file
+* (T129738) SECURITY: Make $wgBlockDisablesLogin also restrict logged in
+  permissions
+* (T129738) SECURITY: Make blocks log users out if $wgBlockDisablesLogin is true
+* (T139670) Move 'UserGetRights' call before application of
+  Session::getAllowedUserRights()
 
 === Action API changes in 1.28 ===
 * Added 'maxarticlesize' property to action=query&meta=siteinfo which contains
@@ -72,6 +82,8 @@ production.
 === Action API internal changes in 1.28 ===
 * Added a new hook, 'ApiMakeParserOptions', to allow extensions to better
   interact with ApiParse and ApiExpandTemplates.
+* (T139565) SECURITY: API: Generate head items in the context of the given title
+* (T115333) SECURITY: Check read permission when loading page content in ApiParse
 
 === Languages updated in 1.28 ===
 
index 8fb3bc2..852a4ed 100644 (file)
@@ -2859,7 +2859,6 @@ class OutputPage extends ContextSource {
 
        private function isUserCssPreview() {
                return $this->getConfig()->get( 'AllowUserCss' )
-                       && $this->getUser()->isLoggedIn()
                        && $this->getTitle()
                        && $this->getTitle()->isCssSubpage()
                        && $this->userCanPreview();
index 186a87b..6ddc9f7 100644 (file)
 abstract class DatabaseBase implements IDatabase {
        /** Number of times to re-try an operation in case of deadlock */
        const DEADLOCK_TRIES = 4;
-
        /** Minimum time to wait before retry, in microseconds */
        const DEADLOCK_DELAY_MIN = 500000;
-
        /** Maximum time to wait before retry */
        const DEADLOCK_DELAY_MAX = 1500000;
 
+       /** How long before it is worth doing a dummy query to test the connection */
+       const PING_TTL = 1.0;
+
+       /** @var string SQL query */
        protected $mLastQuery = '';
+       /** @var bool */
        protected $mDoneWrites = false;
+       /** @var string|bool */
        protected $mPHPError = false;
-
-       protected $mServer, $mUser, $mPassword, $mDBname;
+       /** @var string */
+       protected $mServer;
+       /** @var string */
+       protected $mUser;
+       /** @var string */
+       protected $mPassword;
+       /** @var string */
+       protected $mDBname;
 
        /** @var BagOStuff APC cache */
        protected $srvCache;
 
        /** @var resource Database connection */
        protected $mConn = null;
+       /** @var bool */
        protected $mOpened = false;
 
        /** @var array[] List of (callable, method name) */
@@ -61,20 +72,27 @@ abstract class DatabaseBase implements IDatabase {
        /** @var bool Whether to suppress triggering of post-commit callbacks */
        protected $suppressPostCommitCallbacks = false;
 
+       /** @var string */
        protected $mTablePrefix;
+       /** @var string */
        protected $mSchema;
+       /** @var integer */
        protected $mFlags;
+       /** @var bool */
        protected $mForeign;
+       /** @var array */
        protected $mLBInfo = [];
+       /** @var bool|null */
        protected $mDefaultBigSelects = null;
+       /** @var array|bool */
        protected $mSchemaVars = false;
        /** @var array */
        protected $mSessionVars = [];
-
+       /** @var array|null */
        protected $preparedArgs;
-
+       /** @var string|bool|null Stashed value of html_errors INI setting */
        protected $htmlErrors;
-
+       /** @var string */
        protected $delimiter = ';';
 
        /**
@@ -177,6 +195,9 @@ abstract class DatabaseBase implements IDatabase {
         */
        protected $allViews = null;
 
+       /** @var float UNIX timestamp */
+       protected $lastPing = 0.0;
+
        /** @var TransactionProfiler */
        protected $trxProfiler;
 
@@ -786,8 +807,8 @@ abstract class DatabaseBase implements IDatabase {
                $priorWritesPending = $this->writesOrCallbacksPending();
                $this->mLastQuery = $sql;
 
-               $isWriteQuery = $this->isWriteQuery( $sql );
-               if ( $isWriteQuery ) {
+               $isWrite = $this->isWriteQuery( $sql );
+               if ( $isWrite ) {
                        $reason = $this->getReadOnlyReason();
                        if ( $reason !== false ) {
                                throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
@@ -820,31 +841,12 @@ abstract class DatabaseBase implements IDatabase {
                }
 
                # Keep track of whether the transaction has write queries pending
-               if ( $this->mTrxLevel && !$this->mTrxDoneWrites && $isWriteQuery ) {
+               if ( $this->mTrxLevel && !$this->mTrxDoneWrites && $isWrite ) {
                        $this->mTrxDoneWrites = true;
                        $this->getTransactionProfiler()->transactionWritingIn(
                                $this->mServer, $this->mDBname, $this->mTrxShortId );
                }
 
-               $isMaster = !is_null( $this->getLBInfo( 'master' ) );
-               # generalizeSQL will probably cut down the query to reasonable
-               # logging size most of the time. The substr is really just a sanity check.
-               if ( $isMaster ) {
-                       $queryProf = 'query-m: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
-                       $totalProf = 'DatabaseBase::query-master';
-               } else {
-                       $queryProf = 'query: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
-                       $totalProf = 'DatabaseBase::query';
-               }
-               # Include query transaction state
-               $queryProf .= $this->mTrxShortId ? " [TRX#{$this->mTrxShortId}]" : "";
-
-               $profiler = Profiler::instance();
-               if ( !$profiler instanceof ProfilerStub ) {
-                       $totalProfSection = $profiler->scopedProfileIn( $totalProf );
-                       $queryProfSection = $profiler->scopedProfileIn( $queryProf );
-               }
-
                if ( $this->debug() ) {
                        wfDebugLog( 'queries', sprintf( "%s: %s", $this->mDBname, $commentedSql ) );
                }
@@ -852,15 +854,8 @@ abstract class DatabaseBase implements IDatabase {
                # Avoid fatals if close() was called
                $this->assertOpen();
 
-               # Do the query and handle errors
-               $startTime = microtime( true );
-               $ret = $this->doQuery( $commentedSql );
-               $queryRuntime = microtime( true ) - $startTime;
-               # Log the query time and feed it into the DB trx profiler
-               $this->getTransactionProfiler()->recordQueryCompletion(
-                       $queryProf, $startTime, $isWriteQuery, $this->affectedRows() );
-
-               MWDebug::query( $sql, $fname, $isMaster, $queryRuntime );
+               # Send the query to the server
+               $ret = $this->doProfiledQuery( $sql, $commentedSql, $isWrite, $fname );
 
                # Try reconnecting if the connection was lost
                if ( false === $ret && $this->wasErrorReissuable() ) {
@@ -881,12 +876,7 @@ abstract class DatabaseBase implements IDatabase {
                                        $this->reportQueryError( $lastError, $lastErrno, $sql, $fname );
                                } else {
                                        # Should be safe to silently retry the query
-                                       $startTime = microtime( true );
-                                       $ret = $this->doQuery( $commentedSql );
-                                       $queryRuntime = microtime( true ) - $startTime;
-                                       # Log the query time and feed it into the DB trx profiler
-                                       $this->getTransactionProfiler()->recordQueryCompletion(
-                                               $queryProf, $startTime, $isWriteQuery, $this->affectedRows() );
+                                       $ret = $this->doProfiledQuery( $sql, $commentedSql, $isWrite, $fname );
                                }
                        } else {
                                wfDebug( "Failed\n" );
@@ -911,16 +901,47 @@ abstract class DatabaseBase implements IDatabase {
 
                $res = $this->resultObject( $ret );
 
-               // Destroy profile sections in the opposite order to their creation
-               ScopedCallback::consume( $queryProfSection );
-               ScopedCallback::consume( $totalProfSection );
+               return $res;
+       }
 
-               if ( $isWriteQuery && $this->mTrxLevel ) {
-                       $this->mTrxWriteDuration += $queryRuntime;
-                       $this->mTrxWriteCallers[] = $fname;
+       private function doProfiledQuery( $sql, $commentedSql, $isWrite, $fname ) {
+               $isMaster = !is_null( $this->getLBInfo( 'master' ) );
+               # generalizeSQL() will probably cut down the query to reasonable
+               # logging size most of the time. The substr is really just a sanity check.
+               if ( $isMaster ) {
+                       $queryProf = 'query-m: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
+               } else {
+                       $queryProf = 'query: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
                }
 
-               return $res;
+               # Include query transaction state
+               $queryProf .= $this->mTrxShortId ? " [TRX#{$this->mTrxShortId}]" : "";
+
+               $profiler = Profiler::instance();
+               if ( !( $profiler instanceof ProfilerStub ) ) {
+                       $queryProfSection = $profiler->scopedProfileIn( $queryProf );
+               }
+
+               $startTime = microtime( true );
+               $ret = $this->doQuery( $commentedSql );
+               $queryRuntime = microtime( true ) - $startTime;
+
+               unset( $queryProfSection ); // profile out (if set)
+
+               if ( $ret !== false ) {
+                       $this->lastPing = $startTime;
+                       if ( $isWrite && $this->mTrxLevel ) {
+                               $this->mTrxWriteDuration += $queryRuntime;
+                               $this->mTrxWriteCallers[] = $fname;
+                       }
+               }
+
+               $this->getTransactionProfiler()->recordQueryCompletion(
+                       $queryProf, $startTime, $isWrite, $this->affectedRows()
+               );
+               MWDebug::query( $sql, $fname, $isMaster, $queryRuntime );
+
+               return $ret;
        }
 
        private function canRecoverFromDisconnect( $sql, $priorWritesPending ) {
@@ -2926,6 +2947,9 @@ abstract class DatabaseBase implements IDatabase {
        }
 
        public function ping() {
+               if ( $this->isOpen() && ( microtime( true ) - $this->lastPing ) < self::PING_TTL ) {
+                       return true;
+               }
                try {
                        // This will reconnect if possible, or error out if not
                        $this->query( "SELECT 1 AS ping", __METHOD__ );
@@ -2939,8 +2963,18 @@ abstract class DatabaseBase implements IDatabase {
         * @return bool
         */
        protected function reconnect() {
-               # Stub. Not essential to override.
-               return true;
+               $this->closeConnection();
+               $this->mOpened = false;
+               $this->mConn = false;
+               try {
+                       $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
+                       $this->lastPing = microtime( true );
+                       $ok = true;
+               } catch ( DBConnectionError $e ) {
+                       $ok = false;
+               }
+
+               return $ok;
        }
 
        public function getSessionLagStatus() {
index d1ebe62..fa3756c 100644 (file)
@@ -599,15 +599,6 @@ abstract class DatabaseMysqlBase extends Database {
                return strlen( $name ) && $name[0] == '`' && substr( $name, -1, 1 ) == '`';
        }
 
-       function reconnect() {
-               $this->closeConnection();
-               $this->mOpened = false;
-               $this->mConn = false;
-               $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
-
-               return true;
-       }
-
        function getLag() {
                if ( $this->getLagDetectionMethod() === 'pt-heartbeat' ) {
                        return $this->getLagFromPtHeartbeat();
index f045acf..13a0879 100644 (file)
@@ -1090,7 +1090,7 @@ class LoadBalancer {
        public function approveMasterChanges( array $options ) {
                $limit = isset( $options['maxWriteDuration'] ) ? $options['maxWriteDuration'] : 0;
                $this->forEachOpenMasterConnection( function ( DatabaseBase $conn ) use ( $limit ) {
-                       // If atomic section or explicit transactions are still open, some caller must have
+                       // If atomic sections or explicit transactions are still open, some caller must have
                        // caught an exception but failed to properly rollback any changes. Detect that and
                        // throw and error (causing rollback).
                        if ( $conn->explicitTrxActive() ) {
@@ -1108,6 +1108,14 @@ class LoadBalancer {
                                        wfMessage( 'transaction-duration-limit-exceeded', $time, $limit )->text()
                                );
                        }
+                       // If a connection sits idle while slow queries execute on another, that connection
+                       // may end up dropped before the commit round is reached. Ping servers to detect this.
+                       if ( $conn->writesOrCallbacksPending() && !$conn->ping() ) {
+                               throw new DBTransactionError(
+                                       $conn,
+                                       "A connection to the {$conn->getDBname()} database was lost before commit."
+                               );
+                       }
                } );
        }
 
index 4b906a7..ad0abf2 100644 (file)
@@ -528,17 +528,6 @@ class JobRunner implements LoggerAwareInterface {
                        $lb->waitForAll( $pos );
                }
 
-               $fname = __METHOD__;
-               // Re-ping all masters with transactions. This throws DBError if some
-               // connection died while waiting on locks/slaves, triggering a rollback.
-               wfGetLBFactory()->forEachLB( function( LoadBalancer $lb ) use ( $fname ) {
-                       $lb->forEachOpenConnection( function( IDatabase $conn ) use ( $fname ) {
-                               if ( $conn->writesOrCallbacksPending() ) {
-                                       $conn->ping();
-                               }
-                       } );
-               } );
-
                // Actually commit the DB master changes
                wfGetLBFactory()->commitMasterChanges( __METHOD__ );
 
index 8859609..a9ccc4e 100644 (file)
@@ -3134,6 +3134,7 @@ class User implements IDBAccessObject {
        public function getRights() {
                if ( is_null( $this->mRights ) ) {
                        $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() );
+                       Hooks::run( 'UserGetRights', [ $this, &$this->mRights ] );
 
                        // Deny any rights denied by the user's session, unless this
                        // endpoint has no sessions.
@@ -3144,7 +3145,6 @@ class User implements IDBAccessObject {
                                }
                        }
 
-                       Hooks::run( 'UserGetRights', [ $this, &$this->mRights ] );
                        // Force reindexation of rights when a hook has unset one of them
                        $this->mRights = array_values( array_unique( $this->mRights ) );
 
@@ -3977,7 +3977,6 @@ class User implements IDBAccessObject {
                $noPass = PasswordFactory::newInvalidPassword()->toString();
 
                $dbw = wfGetDB( DB_MASTER );
-               $inWrite = $dbw->writesOrCallbacksPending();
                $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
                $dbw->insert( 'user',
                        [
@@ -3996,25 +3995,17 @@ class User implements IDBAccessObject {
                        [ 'IGNORE' ]
                );
                if ( !$dbw->affectedRows() ) {
-                       // The queries below cannot happen in the same REPEATABLE-READ snapshot.
-                       // Handle this by COMMIT, if possible, or by LOCK IN SHARE MODE otherwise.
-                       if ( $inWrite ) {
-                               // Can't commit due to pending writes that may need atomicity.
-                               // This may cause some lock contention unlike the case below.
-                               $options = [ 'LOCK IN SHARE MODE' ];
-                               $flags = self::READ_LOCKING;
-                       } else {
-                               // Often, this case happens early in views before any writes when
-                               // using CentralAuth. It's should be OK to commit and break the snapshot.
-                               $dbw->commit( __METHOD__, 'flush' );
-                               $options = [];
-                               $flags = self::READ_LATEST;
-                       }
-                       $this->mId = $dbw->selectField( 'user', 'user_id',
-                               [ 'user_name' => $this->mName ], __METHOD__, $options );
+                       // Use locking reads to bypass any REPEATABLE-READ snapshot.
+                       $this->mId = $dbw->selectField(
+                               'user',
+                               'user_id',
+                               [ 'user_name' => $this->mName ],
+                               __METHOD__,
+                               [ 'LOCK IN SHARE MODE' ]
+                       );
                        $loaded = false;
                        if ( $this->mId ) {
-                               if ( $this->loadFromDatabase( $flags ) ) {
+                               if ( $this->loadFromDatabase( self::READ_LOCKING ) ) {
                                        $loaded = true;
                                }
                        }
index bd076ba..985554b 100644 (file)
@@ -92,6 +92,57 @@ class UserTest extends MediaWikiTestCase {
                $this->assertNotContains( 'nukeworld', $rights );
        }
 
+       /**
+        * @covers User::getRights
+        */
+       public function testUserGetRightsHooks() {
+               $user = new User;
+               $user->addGroup( 'unittesters' );
+               $user->addGroup( 'testwriters' );
+               $userWrapper = TestingAccessWrapper::newFromObject( $user );
+
+               $rights = $user->getRights();
+               $this->assertContains( 'test', $rights, 'sanity check' );
+               $this->assertContains( 'runtest', $rights, 'sanity check' );
+               $this->assertContains( 'writetest', $rights, 'sanity check' );
+               $this->assertNotContains( 'nukeworld', $rights, 'sanity check' );
+
+               // Add a hook manipluating the rights
+               $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'UserGetRights' => [ function ( $user, &$rights ) {
+                       $rights[] = 'nukeworld';
+                       $rights = array_diff( $rights, [ 'writetest' ] );
+               } ] ] );
+
+               $userWrapper->mRights = null;
+               $rights = $user->getRights();
+               $this->assertContains( 'test', $rights );
+               $this->assertContains( 'runtest', $rights );
+               $this->assertNotContains( 'writetest', $rights );
+               $this->assertContains( 'nukeworld', $rights );
+
+               // Add a Session that limits rights
+               $mock = $this->getMockBuilder( stdclass::class )
+                       ->setMethods( [ 'getAllowedUserRights', 'deregisterSession', 'getSessionId' ] )
+                       ->getMock();
+               $mock->method( 'getAllowedUserRights' )->willReturn( [ 'test', 'writetest' ] );
+               $mock->method( 'getSessionId' )->willReturn(
+                       new MediaWiki\Session\SessionId( str_repeat( 'X', 32 ) )
+               );
+               $session = MediaWiki\Session\TestUtils::getDummySession( $mock );
+               $mockRequest = $this->getMockBuilder( FauxRequest::class )
+                       ->setMethods( [ 'getSession' ] )
+                       ->getMock();
+               $mockRequest->method( 'getSession' )->willReturn( $session );
+               $userWrapper->mRequest = $mockRequest;
+
+               $userWrapper->mRights = null;
+               $rights = $user->getRights();
+               $this->assertContains( 'test', $rights );
+               $this->assertNotContains( 'runtest', $rights );
+               $this->assertNotContains( 'writetest', $rights );
+               $this->assertNotContains( 'nukeworld', $rights );
+       }
+
        /**
         * @dataProvider provideGetGroupsWithPermission
         * @covers User::getGroupsWithPermission