FU r102073:
[lhc/web/wiklou.git] / includes / filerepo / LocalFile.php
index 8a61a25..6cd30e1 100644 (file)
@@ -33,7 +33,7 @@ class LocalFile extends File {
         * @private
         */
        var
-               $fileExists,       # does the file file exist on disk? (loadFromXxx)
+               $fileExists,       # does the file exist on disk? (loadFromXxx)
                $historyLine,      # Number of line to return by nextHistoryLine() (constructor)
                $historyRes,       # result of the query for the file's history (nextHistoryLine)
                $width,            # \
@@ -58,11 +58,19 @@ class LocalFile extends File {
 
        /**#@-*/
 
+       protected $repoClass = 'LocalRepo';
+
        /**
         * Create a LocalFile from a title
         * Do not call this except from inside a repo class.
         *
         * Note: $unused param is only here to avoid an E_STRICT
+        *
+        * @param $title
+        * @param $repo
+        * @param $unused
+        *
+        * @return LocalFile
         */
        static function newFromTitle( $title, $repo, $unused = null ) {
                return new self( $title, $repo );
@@ -71,6 +79,11 @@ class LocalFile extends File {
        /**
         * Create a LocalFile from a title
         * Do not call this except from inside a repo class.
+        *
+        * @param $row
+        * @param $repo
+        *
+        * @return LocalFile
         */
        static function newFromRow( $row, $repo ) {
                $title = Title::makeTitle( NS_FILE, $row->img_name );
@@ -83,17 +96,22 @@ class LocalFile extends File {
        /**
         * Create a LocalFile from a SHA-1 key
         * Do not call this except from inside a repo class.
+        *
+        * @param $sha1 string base-36 SHA-1
+        * @param $repo LocalRepo
+        * @param string|bool $timestamp MW_timestamp (optional)
+        *
+        * @return bool|LocalFile
         */
        static function newFromKey( $sha1, $repo, $timestamp = false ) {
-               $conds = array( 'img_sha1' => $sha1 );
+               $dbr = $repo->getSlaveDB();
 
+               $conds = array( 'img_sha1' => $sha1 );
                if ( $timestamp ) {
-                       $conds['img_timestamp'] = $timestamp;
+                       $conds['img_timestamp'] = $dbr->timestamp( $timestamp );
                }
 
-               $dbr = $repo->getSlaveDB();
                $row = $dbr->selectRow( 'image', self::selectFields(), $conds, __METHOD__ );
-
                if ( $row ) {
                        return self::newFromRow( $row, $repo );
                } else {
@@ -128,16 +146,15 @@ class LocalFile extends File {
         * Do not call this except from inside a repo class.
         */
        function __construct( $title, $repo ) {
-               if ( !is_object( $title ) ) {
-                       throw new MWException( __CLASS__ . ' constructor given bogus title.' );
-               }
-
                parent::__construct( $title, $repo );
 
                $this->metadata = '';
                $this->historyLine = 0;
                $this->historyRes = null;
                $this->dataLoaded = false;
+
+               $this->assertRepoDefined();
+               $this->assertTitleDefined();
        }
 
        /**
@@ -333,6 +350,7 @@ class LocalFile extends File {
         * Upgrade a row if it needs it
         */
        function maybeUpgradeRow() {
+               global $wgUpdateCompatibleMetadata;
                if ( wfReadOnly() ) {
                        return;
                }
@@ -344,9 +362,14 @@ class LocalFile extends File {
                        $this->upgraded = true;
                } else {
                        $handler = $this->getHandler();
-                       if ( $handler && !$handler->isMetadataValid( $this, $this->metadata ) ) {
-                               $this->upgradeRow();
-                               $this->upgraded = true;
+                       if ( $handler ) {
+                               $validity = $handler->isMetadataValid( $this, $this->metadata );
+                               if ( $validity === MediaHandler::METADATA_BAD
+                                       || ( $validity === MediaHandler::METADATA_COMPATIBLE && $wgUpdateCompatibleMetadata )
+                               ) {
+                                       $this->upgradeRow();
+                                       $this->upgraded = true;
+                               }
                        }
                }
        }
@@ -540,8 +563,8 @@ class LocalFile extends File {
        /** isTrustedFile inherited */
 
        /**
-        * Returns true if the file file exists on disk.
-        * @return boolean Whether file file exist on disk.
+        * Returns true if the file exists on disk.
+        * @return boolean Whether file exist on disk.
         */
        public function exists() {
                $this->load();
@@ -552,7 +575,6 @@ class LocalFile extends File {
        /** getUnscaledThumb inherited */
        /** thumbName inherited */
        /** createThumb inherited */
-       /** getThumbnail inherited */
        /** transform inherited */
 
        /**
@@ -591,12 +613,19 @@ class LocalFile extends File {
 
        /**
         * Get all thumbnail names previously generated for this file
+        * @param $archiveName string|false Name of an archive file
+        * @return array first element is the base dir, then files in that base dir.
         */
-       function getThumbnails() {
+       function getThumbnails( $archiveName = false ) {
                $this->load();
 
+               if ( $archiveName ) {
+                       $dir = $this->getArchiveThumbPath( $archiveName );
+               } else {
+                       $dir = $this->getThumbPath();
+               }
                $files = array();
-               $dir = $this->getThumbPath();
+               $files[] = $dir;
 
                if ( is_dir( $dir ) ) {
                        $handle = opendir( $dir );
@@ -633,6 +662,11 @@ class LocalFile extends File {
                $hashedName = md5( $this->getName() );
                $oldKey = $this->repo->getSharedCacheKey( 'oldfile', $hashedName );
 
+               // Must purge thumbnails for old versions too! bug 30192
+               foreach( $this->getHistory() as $oldFile ) {
+                       $oldFile->purgeThumbnails();
+               }
+
                if ( $oldKey ) {
                        $wgMemc->delete( $oldKey );
                }
@@ -653,30 +687,79 @@ class LocalFile extends File {
        }
 
        /**
-        * Delete cached transformed files
+        * Delete cached transformed files for archived files
+        * @param $archiveName string name of the archived file
+        */
+       function purgeOldThumbnails( $archiveName ) {
+               global $wgUseSquid;
+               // Get a list of old thumbnails and URLs
+               $files = $this->getThumbnails( $archiveName );
+               $dir = array_shift( $files );
+               $this->purgeThumbList( $dir, $files );
+
+               // Directory should be empty, delete it too. This will probably suck on
+               // something like NFS or if the directory isn't actually empty, so hide
+               // the warnings :D
+               wfSuppressWarnings();
+               if( !rmdir( $dir ) ) {
+                       wfDebug( __METHOD__ . ": unable to remove archive directory: $dir\n" );
+               }
+               wfRestoreWarnings();
+
+               // Purge any custom thumbnail caches
+               wfRunHooks( 'LocalFilePurgeThumbnails', array( $this, 'archive' ) );
+
+               // Purge the squid
+               if ( $wgUseSquid ) {
+                       $urls = array();
+                       foreach( $files as $file ) {
+                               $urls[] = $this->getArchiveThumbUrl( $archiveName, $file );
+                       }
+                       SquidUpdate::purge( $urls );
+               }
+       }
+
+
+       /**
+        * Delete cached transformed files for the current version only.
         */
        function purgeThumbnails() {
                global $wgUseSquid;
 
                // Delete thumbnails
                $files = $this->getThumbnails();
-               $dir = $this->getThumbPath();
-               $urls = array();
+               $dir = array_shift( $files );
+               $this->purgeThumbList( $dir, $files );
+
+               // Purge any custom thumbnail caches
+               wfRunHooks( 'LocalFilePurgeThumbnails', array( $this, 'current' ) );
+
+               // Purge the squid
+               if ( $wgUseSquid ) {
+                       $urls = array();
+                       foreach( $files as $file ) {
+                               $urls[] = $this->getThumbUrl( $file );
+                       }
+                       SquidUpdate::purge( $urls );
+               }
+       }
 
+       /**
+        * Delete a list of thumbnails visible at urls
+        * @param $dir string base dir of the files.
+        * @param $files array of strings: relative filenames (to $dir)
+        */
+       protected function purgeThumbList($dir, $files) {
+               wfDebug( __METHOD__ . ": " . var_export( $files, true ) . "\n" );
                foreach ( $files as $file ) {
                        # Check that the base file name is part of the thumb name
                        # This is a basic sanity check to avoid erasing unrelated directories
                        if ( strpos( $file, $this->getName() ) !== false ) {
-                               $url = $this->getThumbUrl( $file );
-                               $urls[] = $url;
-                               @unlink( "$dir/$file" );
+                               wfSuppressWarnings();
+                               unlink( "$dir/$file" );
+                               wfRestoreWarnings();
                        }
                }
-
-               // Purge the squid
-               if ( $wgUseSquid ) {
-                       SquidUpdate::purge( $urls );
-               }
        }
 
        /** purgeDescription inherited */
@@ -781,12 +864,10 @@ class LocalFile extends File {
                }
        }
 
-       /** getFullPath inherited */
        /** getHashPath inherited */
        /** getRel inherited */
        /** getUrlRel inherited */
        /** getArchiveRel inherited */
-       /** getThumbRel inherited */
        /** getArchivePath inherited */
        /** getThumbPath inherited */
        /** getArchiveUrl inherited */
@@ -828,7 +909,6 @@ class LocalFile extends File {
 
        /**
         * Record a file upload in the upload log and the image table
-        * @deprecated use upload()
         */
        function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
                $watch = false, $timestamp = false )
@@ -844,14 +924,14 @@ class LocalFile extends File {
                        $wgUser->addWatch( $this->getTitle() );
                }
                return true;
-
        }
 
        /**
         * Record a file upload in the upload log and the image table
         */
-       function recordUpload2( $oldver, $comment, $pageText, $props = false, $timestamp = false, $user = null )
-       {
+       function recordUpload2(
+               $oldver, $comment, $pageText, $props = false, $timestamp = false, $user = null
+       ) {
                if ( is_null( $user ) ) {
                        global $wgUser;
                        $user = $wgUser;
@@ -864,10 +944,14 @@ class LocalFile extends File {
                        $props = $this->repo->getFileProps( $this->getVirtualUrl() );
                }
 
+               if ( $timestamp === false ) {
+                       $timestamp = $dbw->timestamp();
+               }
+
                $props['description'] = $comment;
                $props['user'] = $user->getId();
                $props['user_text'] = $user->getName();
-               $props['timestamp'] = wfTimestamp( TS_MW );
+               $props['timestamp'] = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
                $this->setProps( $props );
 
                # Delete thumbnails
@@ -878,16 +962,12 @@ class LocalFile extends File {
 
                # Fail now if the file isn't there
                if ( !$this->fileExists ) {
-                       wfDebug( __METHOD__ . ": File " . $this->getPath() . " went missing!\n" );
+                       wfDebug( __METHOD__ . ": File " . $this->getRel() . " went missing!\n" );
                        return false;
                }
 
                $reupload = false;
 
-               if ( $timestamp === false ) {
-                       $timestamp = $dbw->timestamp();
-               }
-
                # Test to see if the row exists using INSERT IGNORE
                # This avoids race conditions by locking the row until the commit, and also
                # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
@@ -960,8 +1040,14 @@ class LocalFile extends File {
                } else {
                        # This is a new file
                        # Update the image count
-                       $site_stats = $dbw->tableName( 'site_stats' );
-                       $dbw->query( "UPDATE $site_stats SET ss_images=ss_images+1", __METHOD__ );
+                       $dbw->begin( __METHOD__ );
+                       $dbw->update(
+                               'site_stats',
+                               array( 'ss_images = ss_images+1' ),
+                               '*',
+                               __METHOD__
+                       );
+                       $dbw->commit( __METHOD__ );
                }
 
                $descTitle = $this->getTitle();
@@ -982,11 +1068,12 @@ class LocalFile extends File {
                                $log->getRcComment(),
                                false
                        );
-                       $nullRevision->insertOn( $dbw );
-
-                       wfRunHooks( 'NewRevisionFromEditComplete', array( $article, $nullRevision, $latest, $user ) );
-                       $article->updateRevisionOn( $dbw, $nullRevision );
+                       if (!is_null($nullRevision)) {
+                               $nullRevision->insertOn( $dbw );
 
+                               wfRunHooks( 'NewRevisionFromEditComplete', array( $article, $nullRevision, $latest, $user ) );
+                               $article->updateRevisionOn( $dbw, $nullRevision );
+                       }
                        # Invalidate the cache for the description page
                        $descTitle->invalidateCache();
                        $descTitle->purgeSquid();
@@ -1035,16 +1122,32 @@ class LocalFile extends File {
         *
         * @param $srcPath String: local filesystem path to the source image
         * @param $flags Integer: a bitwise combination of:
-        *     File::DELETE_SOURCE    Delete the source file, i.e. move
-        *         rather than copy
+        *     File::DELETE_SOURCE      Delete the source file, i.e. move rather than copy
         * @return FileRepoStatus object. On success, the value member contains the
         *     archive name, or an empty string if it was a new file.
         */
        function publish( $srcPath, $flags = 0 ) {
+               return $this->publishTo( $srcPath, $this->getRel(), $flags );
+       }
+
+       /**
+        * Move or copy a file to a specified location. Returns a FileRepoStatus
+        * object with the archive name in the "value" member on success.
+        *
+        * The archive name should be passed through to recordUpload for database
+        * registration.
+        *
+        * @param $srcPath String: local filesystem path to the source image
+        * @param $dstRel String: target relative path
+        * @param $flags Integer: a bitwise combination of:
+        *     File::DELETE_SOURCE      Delete the source file, i.e. move rather than copy
+        * @return FileRepoStatus object. On success, the value member contains the
+        *     archive name, or an empty string if it was a new file.
+        */
+       function publishTo( $srcPath, $dstRel, $flags = 0 ) {
                $this->lock();
 
-               $dstRel = $this->getRel();
-               $archiveName = gmdate( 'YmdHis' ) . '!' . $this->getName();
+               $archiveName = wfTimestamp( TS_MW ) . '!'. $this->getName();
                $archiveRel = 'archive/' . $this->getHashPath() . $archiveName;
                $flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0;
                $status = $this->repo->publish( $srcPath, $dstRel, $archiveRel, $flags );
@@ -1129,6 +1232,7 @@ class LocalFile extends File {
                        array( 'oi_name' => $this->getName() ) );
                foreach ( $result as $row ) {
                        $batch->addOld( $row->oi_archive_name );
+                       $this->purgeOldThumbnails( $row->oi_archive_name );
                }
                $status = $batch->execute();
 
@@ -1163,6 +1267,7 @@ class LocalFile extends File {
 
                $batch = new LocalFileDeleteBatch( $this, $reason, $suppress );
                $batch->addOld( $archiveName );
+               $this->purgeOldThumbnails( $archiveName );
                $status = $batch->execute();
 
                $this->unlock();
@@ -1214,8 +1319,27 @@ class LocalFile extends File {
        /** scaleHeight inherited */
        /** getImageSize inherited */
 
-       /** getDescriptionUrl inherited */
-       /** getDescriptionText inherited */
+       /**
+        * Get the URL of the file description page.
+        */
+       function getDescriptionUrl() {
+               return $this->title->getLocalUrl();
+       }
+
+       /**
+        * Get the HTML text of the description page
+        * This is not used by ImagePage for local files, since (among other things)
+        * it skips the parser cache.
+        */
+       function getDescriptionText() {
+               global $wgParser;
+               $revision = Revision::newFromTitle( $this->title );
+               if ( !$revision ) return false;
+               $text = $revision->getText();
+               if ( !$text ) return false;
+               $pout = $wgParser->parse( $text, $this->title, new ParserOptions() );
+               return $pout->getText();
+       }
 
        function getDescription() {
                $this->load();
@@ -1292,7 +1416,13 @@ class LocalFile extends File {
  * @ingroup FileRepo
  */
 class LocalFileDeleteBatch {
-       var $file, $reason, $srcRels = array(), $archiveUrls = array(), $deletionBatch, $suppress;
+
+       /**
+        * @var LocalFile
+        */
+       var $file;
+
+       var $reason, $srcRels = array(), $archiveUrls = array(), $deletionBatch, $suppress;
        var $status;
 
        function __construct( File $file, $reason = '', $suppress = false ) {
@@ -1324,7 +1454,7 @@ class LocalFileDeleteBatch {
                return array( $oldRels, $deleteCurrent );
        }
 
-       /*protected*/ function getHashes() {
+       protected function getHashes() {
                $hashes = array();
                list( $oldRels, $deleteCurrent ) = $this->getOldRels();
 
@@ -1603,7 +1733,12 @@ class LocalFileDeleteBatch {
  * @ingroup FileRepo
  */
 class LocalFileRestoreBatch {
-       var $file, $cleanupBatch, $ids, $all, $unsuppress = false;
+       /**
+        * @var LocalFile
+        */
+       var $file;
+
+       var $cleanupBatch, $ids, $all, $unsuppress = false;
 
        function __construct( File $file, $unsuppress = false ) {
                $this->file = $file;
@@ -1916,13 +2051,23 @@ class LocalFileRestoreBatch {
 
                return $status;
        }
-       
+
+       /**
+        * Cleanup a failed batch. The batch was only partially successful, so
+        * rollback by removing all items that were succesfully copied.
+        *
+        * @param Status $storeStatus
+        * @param array $storeBatch
+        */
        function cleanupFailedBatch( $storeStatus, $storeBatch ) {
-               $cleanupBatch = array(); 
-               
+               $cleanupBatch = array();
+
                foreach ( $storeStatus->success as $i => $success ) {
+                       // Check if this item of the batch was successfully copied
                        if ( $success ) {
-                               $cleanupBatch[] = array( $storeBatch[$i][1], $storeBatch[$i][1] );
+                               // Item was successfully copied and needs to be removed again
+                               // Extract ($dstZone, $dstRel) from the batch
+                               $cleanupBatch[] = array( $storeBatch[$i][1], $storeBatch[$i][2] );
                        }
                }
                $this->file->repo->cleanupBatch( $cleanupBatch );
@@ -1936,7 +2081,18 @@ class LocalFileRestoreBatch {
  * @ingroup FileRepo
  */
 class LocalFileMoveBatch {
-       var $file, $cur, $olds, $oldCount, $archive, $target, $db;
+
+       /**
+        * @var File
+        */
+       var $file;
+
+       /**
+        * @var Title
+        */
+       var $target;
+
+       var $cur, $olds, $oldCount, $archive, $db;
 
        function __construct( File $file, Title $target ) {
                $this->file = $file;
@@ -1976,14 +2132,14 @@ class LocalFileMoveBatch {
                        $bits = explode( '!', $oldName, 2 );
 
                        if ( count( $bits ) != 2 ) {
-                               wfDebug( "Invalid old file name: $oldName \n" );
+                               wfDebug( "Old file name missing !: '$oldName' \n" );
                                continue;
                        }
 
                        list( $timestamp, $filename ) = $bits;
 
                        if ( $this->oldName != $filename ) {
-                               wfDebug( "Invalid old file name: $oldName \n" );
+                               wfDebug( "Old file name doesn't match: '$oldName' \n" );
                                continue;
                        }
 
@@ -2010,15 +2166,31 @@ class LocalFileMoveBatch {
                $triplets = $this->getMoveTriplets();
 
                $triplets = $this->removeNonexistentFiles( $triplets );
-               $statusDb = $this->doDBUpdates();
-               wfDebugLog( 'imagemove', "Renamed {$this->file->name} in database: {$statusDb->successCount} successes, {$statusDb->failCount} failures" );
-               $statusMove = $repo->storeBatch( $triplets, FSRepo::DELETE_SOURCE );
-               wfDebugLog( 'imagemove', "Moved files for {$this->file->name}: {$statusMove->successCount} successes, {$statusMove->failCount} failures" );
 
-               if ( !$statusMove->isOk() ) {
+               // Copy the files into their new location
+               $statusMove = $repo->storeBatch( $triplets );
+               wfDebugLog( 'imagemove', "Moved files for {$this->file->getName()}: {$statusMove->successCount} successes, {$statusMove->failCount} failures" );
+               if ( !$statusMove->isGood() ) {
                        wfDebugLog( 'imagemove', "Error in moving files: " . $statusMove->getWikiText() );
+                       $this->cleanupTarget( $triplets );
+                       $statusMove->ok = false;
+                       return $statusMove;
+               }
+
+               $this->db->begin();
+               $statusDb = $this->doDBUpdates();
+               wfDebugLog( 'imagemove', "Renamed {$this->file->getName()} in database: {$statusDb->successCount} successes, {$statusDb->failCount} failures" );
+               if ( !$statusDb->isGood() ) {
                        $this->db->rollback();
+                       // Something went wrong with the DB updates, so remove the target files
+                       $this->cleanupTarget( $triplets );
+                       $statusDb->ok = false;
+                       return $statusDb;
                }
+               $this->db->commit();
+
+               // Everything went ok, remove the source files
+               $this->cleanupSource( $triplets );
 
                $status->merge( $statusDb );
                $status->merge( $statusMove );
@@ -2049,6 +2221,8 @@ class LocalFileMoveBatch {
                        $status->successCount++;
                } else {
                        $status->failCount++;
+                       $status->fatal( 'imageinvalidfilename' );
+                       return $status;
                }
 
                // Update old images
@@ -2066,6 +2240,9 @@ class LocalFileMoveBatch {
                $total = $this->oldCount;
                $status->successCount += $affected;
                $status->failCount += $total - $affected;
+               if ( $status->failCount ) {
+                       $status->error( 'imageinvalidfilename' );
+               }
 
                return $status;
        }
@@ -2081,7 +2258,7 @@ class LocalFileMoveBatch {
                        // $move: (oldRelativePath, newRelativePath)
                        $srcUrl = $this->file->repo->getVirtualUrl() . '/public/' . rawurlencode( $move[0] );
                        $triplets[] = array( $srcUrl, 'public', $move[1] );
-                       wfDebugLog( 'imagemove', "Generated move triplet for {$this->file->name}: {$srcUrl} :: public :: {$move[1]}" );
+                       wfDebugLog( 'imagemove', "Generated move triplet for {$this->file->getName()}: {$srcUrl} :: public :: {$move[1]}" );
                }
 
                return $triplets;
@@ -2110,4 +2287,32 @@ class LocalFileMoveBatch {
 
                return $filteredTriplets;
        }
+
+       /**
+        * Cleanup a partially moved array of triplets by deleting the target
+        * files. Called if something went wrong half way.
+        */
+       function cleanupTarget( $triplets ) {
+               // Create dest pairs from the triplets
+               $pairs = array();
+               foreach ( $triplets as $triplet ) {
+                       $pairs[] = array( $triplet[1], $triplet[2] );
+               }
+
+               $this->file->repo->cleanupBatch( $pairs );
+       }
+
+       /**
+        * Cleanup a fully moved array of triplets by deleting the source files.
+        * Called at the end of the move process if everything else went ok.
+        */
+       function cleanupSource( $triplets ) {
+               // Create source file names from the triplets
+               $files = array();
+               foreach ( $triplets as $triplet ) {
+                       $files[] = $triplet[0];
+               }
+
+               $this->file->repo->cleanupBatch( $files );
+       }
 }