Merge "Update list item newline handling to follow Parsoid's model"
[lhc/web/wiklou.git] / includes / filerepo / file / LocalFile.php
index 9b9f0a9..300e68e 100644 (file)
@@ -496,6 +496,8 @@ class LocalFile extends File {
 
                $decoded['timestamp'] = wfTimestamp( TS_MW, $decoded['timestamp'] );
 
+               $decoded['metadata'] = $this->repo->getSlaveDB()->decodeBlob( $decoded['metadata'] );
+
                if ( empty( $decoded['major_mime'] ) ) {
                        $decoded['mime'] = 'unknown/unknown';
                } else {
@@ -1246,8 +1248,13 @@ class LocalFile extends File {
                        wfProfileOut( __METHOD__ . '-getProps' );
                }
 
+               # Imports or such might force a certain timestamp; otherwise we generate
+               # it and can fudge it slightly to keep (name,timestamp) unique on re-upload.
                if ( $timestamp === false ) {
                        $timestamp = $dbw->timestamp();
+                       $allowTimeKludge = true;
+               } else {
+                       $allowTimeKludge = false;
                }
 
                $props['description'] = $comment;
@@ -1290,6 +1297,20 @@ class LocalFile extends File {
                        'IGNORE'
                );
                if ( $dbw->affectedRows() == 0 ) {
+                       if ( $allowTimeKludge ) {
+                               # Use FOR UPDATE to ignore any transaction snapshotting
+                               $ltimestamp = $dbw->selectField( 'image', 'img_timestamp',
+                                       array( 'img_name' => $this->getName() ), __METHOD__, array( 'FOR UPDATE' ) );
+                               $lUnixtime = $ltimestamp ? wfTimestamp( TS_UNIX, $ltimestamp ) : false;
+                               # Avoid a timestamp that is not newer than the last version
+                               # TODO: the image/oldimage tables should be like page/revision with an ID field
+                               if ( $lUnixtime && wfTimestamp( TS_UNIX, $timestamp ) <= $lUnixtime ) {
+                                       sleep( 1 ); // fast enough re-uploads would go far in the future otherwise
+                                       $timestamp = $dbw->timestamp( $lUnixtime + 1 );
+                                       $this->timestamp = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
+                               }
+                       }
+
                        # (bug 34993) Note: $oldver can be empty here, if the previous
                        # version of the file was broken. Allow registration of the new
                        # version to continue anyway, because that's better than having
@@ -1597,14 +1618,15 @@ class LocalFile extends File {
         *
         * @param string $reason
         * @param bool $suppress
+        * @param User|null $user
         * @return FileRepoStatus
         */
-       function delete( $reason, $suppress = false ) {
+       function delete( $reason, $suppress = false, $user = null ) {
                if ( $this->getRepo()->getReadOnlyReason() !== false ) {
                        return $this->readOnlyFatalStatus();
                }
 
-               $batch = new LocalFileDeleteBatch( $this, $reason, $suppress );
+               $batch = new LocalFileDeleteBatch( $this, $reason, $suppress, $user );
 
                $this->lock(); // begin
                $batch->addCurrent();
@@ -1654,16 +1676,17 @@ class LocalFile extends File {
         * @param string $archiveName
         * @param string $reason
         * @param bool $suppress
+        * @param User|null $user
         * @throws MWException Exception on database or file store failure
         * @return FileRepoStatus
         */
-       function deleteOld( $archiveName, $reason, $suppress = false ) {
+       function deleteOld( $archiveName, $reason, $suppress = false, $user = null ) {
                global $wgUseSquid;
                if ( $this->getRepo()->getReadOnlyReason() !== false ) {
                        return $this->readOnlyFatalStatus();
                }
 
-               $batch = new LocalFileDeleteBatch( $this, $reason, $suppress );
+               $batch = new LocalFileDeleteBatch( $this, $reason, $suppress, $user );
 
                $this->lock(); // begin
                $batch->addOld( $archiveName );
@@ -1821,8 +1844,8 @@ class LocalFile extends File {
        /**
         * Start a transaction and lock the image for update
         * Increments a reference counter if the lock is already held
-        * @throws MWException
-        * @return bool True if the image exists, false otherwise
+        * @throws MWException Throws an error if the lock was not acquired
+        * @return bool success
         */
        function lock() {
                $dbw = $this->repo->getMasterDB();
@@ -1834,21 +1857,22 @@ class LocalFile extends File {
                        }
                        $this->locked++;
                        // Bug 54736: use simple lock to handle when the file does not exist.
-                       // SELECT FOR UPDATE only locks records not the gaps where there are none.
-                       $cache = wfGetMainCache();
-                       $key = $this->getCacheKey();
-                       if ( !$cache->lock( $key, 5 ) ) {
+                       // SELECT FOR UPDATE prevents changes, not other SELECTs with FOR UPDATE.
+                       // Also, that would cause contention on INSERT of similarly named rows.
+                       $backend = $this->getRepo()->getBackend();
+                       $lockPaths = array( $this->getPath() ); // represents all versions of the file
+                       $status = $backend->lockFiles( $lockPaths, LockManager::LOCK_EX, 5 );
+                       if ( !$status->isGood() ) {
                                throw new MWException( "Could not acquire lock for '{$this->getName()}.'" );
                        }
-                       $dbw->onTransactionIdle( function () use ( $cache, $key ) {
-                               $cache->unlock( $key ); // release on commit
+                       $dbw->onTransactionIdle( function () use ( $backend, $lockPaths ) {
+                               $backend->unlockFiles( $lockPaths, LockManager::LOCK_EX ); // release on commit
                        } );
                }
 
                $this->markVolatile(); // file may change soon
 
-               return $dbw->selectField( 'image', '1',
-                       array( 'img_name' => $this->getName() ), __METHOD__, array( 'FOR UPDATE' ) );
+               return true;
        }
 
        /**
@@ -1962,15 +1986,25 @@ class LocalFileDeleteBatch {
        /** @var FileRepoStatus */
        private $status;
 
+       /** @var User */
+       private $user;
+
        /**
         * @param File $file
         * @param string $reason
         * @param bool $suppress
+        * @param User|null $user
         */
-       function __construct( File $file, $reason = '', $suppress = false ) {
+       function __construct( File $file, $reason = '', $suppress = false, $user = null ) {
                $this->file = $file;
                $this->reason = $reason;
                $this->suppress = $suppress;
+               if ( $user ) {
+                       $this->user = $user;
+               } else {
+                       global $wgUser;
+                       $this->user = $wgUser;
+               }
                $this->status = $file->repo->newGood();
        }
 
@@ -2084,11 +2118,9 @@ class LocalFileDeleteBatch {
        }
 
        function doDBInserts() {
-               global $wgUser;
-
                $dbw = $this->file->repo->getMasterDB();
                $encTimestamp = $dbw->addQuotes( $dbw->timestamp() );
-               $encUserId = $dbw->addQuotes( $wgUser->getId() );
+               $encUserId = $dbw->addQuotes( $this->user->getId() );
                $encReason = $dbw->addQuotes( $this->reason );
                $encGroup = $dbw->addQuotes( 'deleted' );
                $ext = $this->file->getExtension();
@@ -2114,7 +2146,11 @@ class LocalFileDeleteBatch {
                        $dbw->insertSelect( 'filearchive', 'image',
                                array(
                                        'fa_storage_group' => $encGroup,
-                                       'fa_storage_key' => "CASE WHEN img_sha1='' THEN '' ELSE $concat END",
+                                       'fa_storage_key' => $dbw->conditional(
+                                               array( 'img_sha1' => '' ),
+                                               $dbw->addQuotes( '' ),
+                                               $concat
+                                       ),
                                        'fa_deleted_user' => $encUserId,
                                        'fa_deleted_timestamp' => $encTimestamp,
                                        'fa_deleted_reason' => $encReason,
@@ -2146,7 +2182,11 @@ class LocalFileDeleteBatch {
                        $dbw->insertSelect( 'filearchive', 'oldimage',
                                array(
                                        'fa_storage_group' => $encGroup,
-                                       'fa_storage_key' => "CASE WHEN oi_sha1='' THEN '' ELSE $concat END",
+                                       'fa_storage_key' => $dbw->conditional(
+                                               array( 'oi_sha1' => '' ),
+                                               $dbw->addQuotes( '' ),
+                                               $concat
+                                       ),
                                        'fa_deleted_user' => $encUserId,
                                        'fa_deleted_timestamp' => $encTimestamp,
                                        'fa_deleted_reason' => $encReason,
@@ -2363,10 +2403,14 @@ class LocalFileRestoreBatch {
                        return $this->file->repo->newGood();
                }
 
-               $exists = $this->file->lock();
+               $this->file->lock();
+
                $dbw = $this->file->repo->getMasterDB();
                $status = $this->file->repo->newGood();
 
+               $exists = (bool)$dbw->selectField( 'image', '1',
+                       array( 'img_name' => $this->file->getName() ), __METHOD__, array( 'FOR UPDATE' ) );
+
                // Fetch all or selected archived revisions for the file,
                // sorted from the most recent to the oldest.
                $conditions = array( 'fa_name' => $this->file->getName() );
@@ -2727,7 +2771,8 @@ class LocalFileMoveBatch {
                $result = $this->db->select( 'oldimage',
                        array( 'oi_archive_name', 'oi_deleted' ),
                        array( 'oi_name' => $this->oldName ),
-                       __METHOD__
+                       __METHOD__,
+                       array( 'FOR UPDATE' ) // ignore snapshot
                );
 
                foreach ( $result as $row ) {