MediaWiki  REL1_24
LocalFile.php
Go to the documentation of this file.
00001 <?php
00027 define( 'MW_FILE_VERSION', 9 );
00028 
00046 class LocalFile extends File {
00047     const CACHE_FIELD_MAX_LEN = 1000;
00048 
00050     protected $fileExists;
00051 
00053     protected $width;
00054 
00056     protected $height;
00057 
00059     protected $bits;
00060 
00062     protected $media_type;
00063 
00065     protected $mime;
00066 
00068     protected $size;
00069 
00071     protected $metadata;
00072 
00074     protected $sha1;
00075 
00077     protected $dataLoaded;
00078 
00080     protected $extraDataLoaded;
00081 
00083     protected $deleted;
00084 
00086     protected $repoClass = 'LocalRepo';
00087 
00089     private $historyLine;
00090 
00092     private $historyRes;
00093 
00095     private $major_mime;
00096 
00098     private $minor_mime;
00099 
00101     private $timestamp;
00102 
00104     private $user;
00105 
00107     private $user_text;
00108 
00110     private $description;
00111 
00113     private $upgraded;
00114 
00116     private $locked;
00117 
00119     private $lockedOwnTrx;
00120 
00122     private $missing;
00123 
00125     private $lastMarkedVolatile = 0;
00126 
00127     const LOAD_ALL = 1; // integer; load all the lazy fields too (like metadata)
00128     const LOAD_VIA_SLAVE = 2; // integer; use a slave to load the data
00129 
00130     const VOLATILE_TTL = 300; // integer; seconds
00131 
00144     static function newFromTitle( $title, $repo, $unused = null ) {
00145         return new self( $title, $repo );
00146     }
00147 
00157     static function newFromRow( $row, $repo ) {
00158         $title = Title::makeTitle( NS_FILE, $row->img_name );
00159         $file = new self( $title, $repo );
00160         $file->loadFromRow( $row );
00161 
00162         return $file;
00163     }
00164 
00174     static function newFromKey( $sha1, $repo, $timestamp = false ) {
00175         $dbr = $repo->getSlaveDB();
00176 
00177         $conds = array( 'img_sha1' => $sha1 );
00178         if ( $timestamp ) {
00179             $conds['img_timestamp'] = $dbr->timestamp( $timestamp );
00180         }
00181 
00182         $row = $dbr->selectRow( 'image', self::selectFields(), $conds, __METHOD__ );
00183         if ( $row ) {
00184             return self::newFromRow( $row, $repo );
00185         } else {
00186             return false;
00187         }
00188     }
00189 
00194     static function selectFields() {
00195         return array(
00196             'img_name',
00197             'img_size',
00198             'img_width',
00199             'img_height',
00200             'img_metadata',
00201             'img_bits',
00202             'img_media_type',
00203             'img_major_mime',
00204             'img_minor_mime',
00205             'img_description',
00206             'img_user',
00207             'img_user_text',
00208             'img_timestamp',
00209             'img_sha1',
00210         );
00211     }
00212 
00219     function __construct( $title, $repo ) {
00220         parent::__construct( $title, $repo );
00221 
00222         $this->metadata = '';
00223         $this->historyLine = 0;
00224         $this->historyRes = null;
00225         $this->dataLoaded = false;
00226         $this->extraDataLoaded = false;
00227 
00228         $this->assertRepoDefined();
00229         $this->assertTitleDefined();
00230     }
00231 
00237     function getCacheKey() {
00238         $hashedName = md5( $this->getName() );
00239 
00240         return $this->repo->getSharedCacheKey( 'file', $hashedName );
00241     }
00242 
00247     function loadFromCache() {
00248         global $wgMemc;
00249 
00250         wfProfileIn( __METHOD__ );
00251         $this->dataLoaded = false;
00252         $this->extraDataLoaded = false;
00253         $key = $this->getCacheKey();
00254 
00255         if ( !$key ) {
00256             wfProfileOut( __METHOD__ );
00257 
00258             return false;
00259         }
00260 
00261         $cachedValues = $wgMemc->get( $key );
00262 
00263         // Check if the key existed and belongs to this version of MediaWiki
00264         if ( isset( $cachedValues['version'] ) && $cachedValues['version'] == MW_FILE_VERSION ) {
00265             wfDebug( "Pulling file metadata from cache key $key\n" );
00266             $this->fileExists = $cachedValues['fileExists'];
00267             if ( $this->fileExists ) {
00268                 $this->setProps( $cachedValues );
00269             }
00270             $this->dataLoaded = true;
00271             $this->extraDataLoaded = true;
00272             foreach ( $this->getLazyCacheFields( '' ) as $field ) {
00273                 $this->extraDataLoaded = $this->extraDataLoaded && isset( $cachedValues[$field] );
00274             }
00275         }
00276 
00277         if ( $this->dataLoaded ) {
00278             wfIncrStats( 'image_cache_hit' );
00279         } else {
00280             wfIncrStats( 'image_cache_miss' );
00281         }
00282 
00283         wfProfileOut( __METHOD__ );
00284 
00285         return $this->dataLoaded;
00286     }
00287 
00291     function saveToCache() {
00292         global $wgMemc;
00293 
00294         $this->load();
00295         $key = $this->getCacheKey();
00296 
00297         if ( !$key ) {
00298             return;
00299         }
00300 
00301         $fields = $this->getCacheFields( '' );
00302         $cache = array( 'version' => MW_FILE_VERSION );
00303         $cache['fileExists'] = $this->fileExists;
00304 
00305         if ( $this->fileExists ) {
00306             foreach ( $fields as $field ) {
00307                 $cache[$field] = $this->$field;
00308             }
00309         }
00310 
00311         // Strip off excessive entries from the subset of fields that can become large.
00312         // If the cache value gets to large it will not fit in memcached and nothing will
00313         // get cached at all, causing master queries for any file access.
00314         foreach ( $this->getLazyCacheFields( '' ) as $field ) {
00315             if ( isset( $cache[$field] ) && strlen( $cache[$field] ) > 100 * 1024 ) {
00316                 unset( $cache[$field] ); // don't let the value get too big
00317             }
00318         }
00319 
00320         // Cache presence for 1 week and negatives for 1 day
00321         $wgMemc->set( $key, $cache, $this->fileExists ? 86400 * 7 : 86400 );
00322     }
00323 
00327     function loadFromFile() {
00328         $props = $this->repo->getFileProps( $this->getVirtualUrl() );
00329         $this->setProps( $props );
00330     }
00331 
00336     function getCacheFields( $prefix = 'img_' ) {
00337         static $fields = array( 'size', 'width', 'height', 'bits', 'media_type',
00338             'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1', 'user',
00339             'user_text', 'description' );
00340         static $results = array();
00341 
00342         if ( $prefix == '' ) {
00343             return $fields;
00344         }
00345 
00346         if ( !isset( $results[$prefix] ) ) {
00347             $prefixedFields = array();
00348             foreach ( $fields as $field ) {
00349                 $prefixedFields[] = $prefix . $field;
00350             }
00351             $results[$prefix] = $prefixedFields;
00352         }
00353 
00354         return $results[$prefix];
00355     }
00356 
00361     function getLazyCacheFields( $prefix = 'img_' ) {
00362         static $fields = array( 'metadata' );
00363         static $results = array();
00364 
00365         if ( $prefix == '' ) {
00366             return $fields;
00367         }
00368 
00369         if ( !isset( $results[$prefix] ) ) {
00370             $prefixedFields = array();
00371             foreach ( $fields as $field ) {
00372                 $prefixedFields[] = $prefix . $field;
00373             }
00374             $results[$prefix] = $prefixedFields;
00375         }
00376 
00377         return $results[$prefix];
00378     }
00379 
00384     function loadFromDB( $flags = 0 ) {
00385         # Polymorphic function name to distinguish foreign and local fetches
00386         $fname = get_class( $this ) . '::' . __FUNCTION__;
00387         wfProfileIn( $fname );
00388 
00389         # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
00390         $this->dataLoaded = true;
00391         $this->extraDataLoaded = true;
00392 
00393         $dbr = ( $flags & self::LOAD_VIA_SLAVE )
00394             ? $this->repo->getSlaveDB()
00395             : $this->repo->getMasterDB();
00396 
00397         $row = $dbr->selectRow( 'image', $this->getCacheFields( 'img_' ),
00398             array( 'img_name' => $this->getName() ), $fname );
00399 
00400         if ( $row ) {
00401             $this->loadFromRow( $row );
00402         } else {
00403             $this->fileExists = false;
00404         }
00405 
00406         wfProfileOut( $fname );
00407     }
00408 
00413     protected function loadExtraFromDB() {
00414         # Polymorphic function name to distinguish foreign and local fetches
00415         $fname = get_class( $this ) . '::' . __FUNCTION__;
00416         wfProfileIn( $fname );
00417 
00418         # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
00419         $this->extraDataLoaded = true;
00420 
00421         $fieldMap = $this->loadFieldsWithTimestamp( $this->repo->getSlaveDB(), $fname );
00422         if ( !$fieldMap ) {
00423             $fieldMap = $this->loadFieldsWithTimestamp( $this->repo->getMasterDB(), $fname );
00424         }
00425 
00426         if ( $fieldMap ) {
00427             foreach ( $fieldMap as $name => $value ) {
00428                 $this->$name = $value;
00429             }
00430         } else {
00431             wfProfileOut( $fname );
00432             throw new MWException( "Could not find data for image '{$this->getName()}'." );
00433         }
00434 
00435         wfProfileOut( $fname );
00436     }
00437 
00443     private function loadFieldsWithTimestamp( $dbr, $fname ) {
00444         $fieldMap = false;
00445 
00446         $row = $dbr->selectRow( 'image', $this->getLazyCacheFields( 'img_' ),
00447             array( 'img_name' => $this->getName(), 'img_timestamp' => $this->getTimestamp() ),
00448             $fname );
00449         if ( $row ) {
00450             $fieldMap = $this->unprefixRow( $row, 'img_' );
00451         } else {
00452             # File may have been uploaded over in the meantime; check the old versions
00453             $row = $dbr->selectRow( 'oldimage', $this->getLazyCacheFields( 'oi_' ),
00454                 array( 'oi_name' => $this->getName(), 'oi_timestamp' => $this->getTimestamp() ),
00455                 $fname );
00456             if ( $row ) {
00457                 $fieldMap = $this->unprefixRow( $row, 'oi_' );
00458             }
00459         }
00460 
00461         return $fieldMap;
00462     }
00463 
00470     protected function unprefixRow( $row, $prefix = 'img_' ) {
00471         $array = (array)$row;
00472         $prefixLength = strlen( $prefix );
00473 
00474         // Sanity check prefix once
00475         if ( substr( key( $array ), 0, $prefixLength ) !== $prefix ) {
00476             throw new MWException( __METHOD__ . ': incorrect $prefix parameter' );
00477         }
00478 
00479         $decoded = array();
00480         foreach ( $array as $name => $value ) {
00481             $decoded[substr( $name, $prefixLength )] = $value;
00482         }
00483 
00484         return $decoded;
00485     }
00486 
00495     function decodeRow( $row, $prefix = 'img_' ) {
00496         $decoded = $this->unprefixRow( $row, $prefix );
00497 
00498         $decoded['timestamp'] = wfTimestamp( TS_MW, $decoded['timestamp'] );
00499 
00500         $decoded['metadata'] = $this->repo->getSlaveDB()->decodeBlob( $decoded['metadata'] );
00501 
00502         if ( empty( $decoded['major_mime'] ) ) {
00503             $decoded['mime'] = 'unknown/unknown';
00504         } else {
00505             if ( !$decoded['minor_mime'] ) {
00506                 $decoded['minor_mime'] = 'unknown';
00507             }
00508             $decoded['mime'] = $decoded['major_mime'] . '/' . $decoded['minor_mime'];
00509         }
00510 
00511         # Trim zero padding from char/binary field
00512         $decoded['sha1'] = rtrim( $decoded['sha1'], "\0" );
00513 
00514         return $decoded;
00515     }
00516 
00523     function loadFromRow( $row, $prefix = 'img_' ) {
00524         $this->dataLoaded = true;
00525         $this->extraDataLoaded = true;
00526 
00527         $array = $this->decodeRow( $row, $prefix );
00528 
00529         foreach ( $array as $name => $value ) {
00530             $this->$name = $value;
00531         }
00532 
00533         $this->fileExists = true;
00534         $this->maybeUpgradeRow();
00535     }
00536 
00541     function load( $flags = 0 ) {
00542         if ( !$this->dataLoaded ) {
00543             if ( !$this->loadFromCache() ) {
00544                 $this->loadFromDB( $this->isVolatile() ? 0 : self::LOAD_VIA_SLAVE );
00545                 $this->saveToCache();
00546             }
00547             $this->dataLoaded = true;
00548         }
00549         if ( ( $flags & self::LOAD_ALL ) && !$this->extraDataLoaded ) {
00550             $this->loadExtraFromDB();
00551         }
00552     }
00553 
00557     function maybeUpgradeRow() {
00558         global $wgUpdateCompatibleMetadata;
00559         if ( wfReadOnly() ) {
00560             return;
00561         }
00562 
00563         if ( is_null( $this->media_type ) ||
00564             $this->mime == 'image/svg'
00565         ) {
00566             $this->upgradeRow();
00567             $this->upgraded = true;
00568         } else {
00569             $handler = $this->getHandler();
00570             if ( $handler ) {
00571                 $validity = $handler->isMetadataValid( $this, $this->getMetadata() );
00572                 if ( $validity === MediaHandler::METADATA_BAD
00573                     || ( $validity === MediaHandler::METADATA_COMPATIBLE && $wgUpdateCompatibleMetadata )
00574                 ) {
00575                     $this->upgradeRow();
00576                     $this->upgraded = true;
00577                 }
00578             }
00579         }
00580     }
00581 
00582     function getUpgraded() {
00583         return $this->upgraded;
00584     }
00585 
00589     function upgradeRow() {
00590         wfProfileIn( __METHOD__ );
00591 
00592         $this->lock(); // begin
00593 
00594         $this->loadFromFile();
00595 
00596         # Don't destroy file info of missing files
00597         if ( !$this->fileExists ) {
00598             $this->unlock();
00599             wfDebug( __METHOD__ . ": file does not exist, aborting\n" );
00600             wfProfileOut( __METHOD__ );
00601 
00602             return;
00603         }
00604 
00605         $dbw = $this->repo->getMasterDB();
00606         list( $major, $minor ) = self::splitMime( $this->mime );
00607 
00608         if ( wfReadOnly() ) {
00609             $this->unlock();
00610             wfProfileOut( __METHOD__ );
00611 
00612             return;
00613         }
00614         wfDebug( __METHOD__ . ': upgrading ' . $this->getName() . " to the current schema\n" );
00615 
00616         $dbw->update( 'image',
00617             array(
00618                 'img_size' => $this->size, // sanity
00619                 'img_width' => $this->width,
00620                 'img_height' => $this->height,
00621                 'img_bits' => $this->bits,
00622                 'img_media_type' => $this->media_type,
00623                 'img_major_mime' => $major,
00624                 'img_minor_mime' => $minor,
00625                 'img_metadata' => $dbw->encodeBlob( $this->metadata ),
00626                 'img_sha1' => $this->sha1,
00627             ),
00628             array( 'img_name' => $this->getName() ),
00629             __METHOD__
00630         );
00631 
00632         $this->saveToCache();
00633 
00634         $this->unlock(); // done
00635 
00636         wfProfileOut( __METHOD__ );
00637     }
00638 
00649     function setProps( $info ) {
00650         $this->dataLoaded = true;
00651         $fields = $this->getCacheFields( '' );
00652         $fields[] = 'fileExists';
00653 
00654         foreach ( $fields as $field ) {
00655             if ( isset( $info[$field] ) ) {
00656                 $this->$field = $info[$field];
00657             }
00658         }
00659 
00660         // Fix up mime fields
00661         if ( isset( $info['major_mime'] ) ) {
00662             $this->mime = "{$info['major_mime']}/{$info['minor_mime']}";
00663         } elseif ( isset( $info['mime'] ) ) {
00664             $this->mime = $info['mime'];
00665             list( $this->major_mime, $this->minor_mime ) = self::splitMime( $this->mime );
00666         }
00667     }
00668 
00680     function isMissing() {
00681         if ( $this->missing === null ) {
00682             list( $fileExists ) = $this->repo->fileExists( $this->getVirtualUrl() );
00683             $this->missing = !$fileExists;
00684         }
00685 
00686         return $this->missing;
00687     }
00688 
00695     public function getWidth( $page = 1 ) {
00696         $this->load();
00697 
00698         if ( $this->isMultipage() ) {
00699             $handler = $this->getHandler();
00700             if ( !$handler ) {
00701                 return 0;
00702             }
00703             $dim = $handler->getPageDimensions( $this, $page );
00704             if ( $dim ) {
00705                 return $dim['width'];
00706             } else {
00707                 // For non-paged media, the false goes through an
00708                 // intval, turning failure into 0, so do same here.
00709                 return 0;
00710             }
00711         } else {
00712             return $this->width;
00713         }
00714     }
00715 
00722     public function getHeight( $page = 1 ) {
00723         $this->load();
00724 
00725         if ( $this->isMultipage() ) {
00726             $handler = $this->getHandler();
00727             if ( !$handler ) {
00728                 return 0;
00729             }
00730             $dim = $handler->getPageDimensions( $this, $page );
00731             if ( $dim ) {
00732                 return $dim['height'];
00733             } else {
00734                 // For non-paged media, the false goes through an
00735                 // intval, turning failure into 0, so do same here.
00736                 return 0;
00737             }
00738         } else {
00739             return $this->height;
00740         }
00741     }
00742 
00749     function getUser( $type = 'text' ) {
00750         $this->load();
00751 
00752         if ( $type == 'text' ) {
00753             return $this->user_text;
00754         } elseif ( $type == 'id' ) {
00755             return $this->user;
00756         }
00757     }
00758 
00763     function getMetadata() {
00764         $this->load( self::LOAD_ALL ); // large metadata is loaded in another step
00765         return $this->metadata;
00766     }
00767 
00771     function getBitDepth() {
00772         $this->load();
00773 
00774         return $this->bits;
00775     }
00776 
00781     public function getSize() {
00782         $this->load();
00783 
00784         return $this->size;
00785     }
00786 
00791     function getMimeType() {
00792         $this->load();
00793 
00794         return $this->mime;
00795     }
00796 
00802     function getMediaType() {
00803         $this->load();
00804 
00805         return $this->media_type;
00806     }
00807 
00818     public function exists() {
00819         $this->load();
00820 
00821         return $this->fileExists;
00822     }
00823 
00839     function getThumbnails( $archiveName = false ) {
00840         if ( $archiveName ) {
00841             $dir = $this->getArchiveThumbPath( $archiveName );
00842         } else {
00843             $dir = $this->getThumbPath();
00844         }
00845 
00846         $backend = $this->repo->getBackend();
00847         $files = array( $dir );
00848         try {
00849             $iterator = $backend->getFileList( array( 'dir' => $dir ) );
00850             foreach ( $iterator as $file ) {
00851                 $files[] = $file;
00852             }
00853         } catch ( FileBackendError $e ) {
00854         } // suppress (bug 54674)
00855 
00856         return $files;
00857     }
00858 
00862     function purgeMetadataCache() {
00863         $this->loadFromDB();
00864         $this->saveToCache();
00865         $this->purgeHistory();
00866     }
00867 
00873     function purgeHistory() {
00874         global $wgMemc;
00875 
00876         $hashedName = md5( $this->getName() );
00877         $oldKey = $this->repo->getSharedCacheKey( 'oldfile', $hashedName );
00878 
00879         if ( $oldKey ) {
00880             $wgMemc->delete( $oldKey );
00881         }
00882     }
00883 
00891     function purgeCache( $options = array() ) {
00892         wfProfileIn( __METHOD__ );
00893         // Refresh metadata cache
00894         $this->purgeMetadataCache();
00895 
00896         // Delete thumbnails
00897         $this->purgeThumbnails( $options );
00898 
00899         // Purge squid cache for this file
00900         SquidUpdate::purge( array( $this->getURL() ) );
00901         wfProfileOut( __METHOD__ );
00902     }
00903 
00908     function purgeOldThumbnails( $archiveName ) {
00909         global $wgUseSquid;
00910         wfProfileIn( __METHOD__ );
00911 
00912         // Get a list of old thumbnails and URLs
00913         $files = $this->getThumbnails( $archiveName );
00914 
00915         // Purge any custom thumbnail caches
00916         wfRunHooks( 'LocalFilePurgeThumbnails', array( $this, $archiveName ) );
00917 
00918         $dir = array_shift( $files );
00919         $this->purgeThumbList( $dir, $files );
00920 
00921         // Purge the squid
00922         if ( $wgUseSquid ) {
00923             $urls = array();
00924             foreach ( $files as $file ) {
00925                 $urls[] = $this->getArchiveThumbUrl( $archiveName, $file );
00926             }
00927             SquidUpdate::purge( $urls );
00928         }
00929 
00930         wfProfileOut( __METHOD__ );
00931     }
00932 
00937     function purgeThumbnails( $options = array() ) {
00938         global $wgUseSquid;
00939         wfProfileIn( __METHOD__ );
00940 
00941         // Delete thumbnails
00942         $files = $this->getThumbnails();
00943         // Always purge all files from squid regardless of handler filters
00944         $urls = array();
00945         if ( $wgUseSquid ) {
00946             foreach ( $files as $file ) {
00947                 $urls[] = $this->getThumbUrl( $file );
00948             }
00949             array_shift( $urls ); // don't purge directory
00950         }
00951 
00952         // Give media handler a chance to filter the file purge list
00953         if ( !empty( $options['forThumbRefresh'] ) ) {
00954             $handler = $this->getHandler();
00955             if ( $handler ) {
00956                 $handler->filterThumbnailPurgeList( $files, $options );
00957             }
00958         }
00959 
00960         // Purge any custom thumbnail caches
00961         wfRunHooks( 'LocalFilePurgeThumbnails', array( $this, false ) );
00962 
00963         $dir = array_shift( $files );
00964         $this->purgeThumbList( $dir, $files );
00965 
00966         // Purge the squid
00967         if ( $wgUseSquid ) {
00968             SquidUpdate::purge( $urls );
00969         }
00970 
00971         wfProfileOut( __METHOD__ );
00972     }
00973 
00979     protected function purgeThumbList( $dir, $files ) {
00980         $fileListDebug = strtr(
00981             var_export( $files, true ),
00982             array( "\n" => '' )
00983         );
00984         wfDebug( __METHOD__ . ": $fileListDebug\n" );
00985 
00986         $purgeList = array();
00987         foreach ( $files as $file ) {
00988             # Check that the base file name is part of the thumb name
00989             # This is a basic sanity check to avoid erasing unrelated directories
00990             if ( strpos( $file, $this->getName() ) !== false
00991                 || strpos( $file, "-thumbnail" ) !== false // "short" thumb name
00992             ) {
00993                 $purgeList[] = "{$dir}/{$file}";
00994             }
00995         }
00996 
00997         # Delete the thumbnails
00998         $this->repo->quickPurgeBatch( $purgeList );
00999         # Clear out the thumbnail directory if empty
01000         $this->repo->quickCleanDir( $dir );
01001     }
01002 
01013     function getHistory( $limit = null, $start = null, $end = null, $inc = true ) {
01014         $dbr = $this->repo->getSlaveDB();
01015         $tables = array( 'oldimage' );
01016         $fields = OldLocalFile::selectFields();
01017         $conds = $opts = $join_conds = array();
01018         $eq = $inc ? '=' : '';
01019         $conds[] = "oi_name = " . $dbr->addQuotes( $this->title->getDBkey() );
01020 
01021         if ( $start ) {
01022             $conds[] = "oi_timestamp <$eq " . $dbr->addQuotes( $dbr->timestamp( $start ) );
01023         }
01024 
01025         if ( $end ) {
01026             $conds[] = "oi_timestamp >$eq " . $dbr->addQuotes( $dbr->timestamp( $end ) );
01027         }
01028 
01029         if ( $limit ) {
01030             $opts['LIMIT'] = $limit;
01031         }
01032 
01033         // Search backwards for time > x queries
01034         $order = ( !$start && $end !== null ) ? 'ASC' : 'DESC';
01035         $opts['ORDER BY'] = "oi_timestamp $order";
01036         $opts['USE INDEX'] = array( 'oldimage' => 'oi_name_timestamp' );
01037 
01038         wfRunHooks( 'LocalFile::getHistory', array( &$this, &$tables, &$fields,
01039             &$conds, &$opts, &$join_conds ) );
01040 
01041         $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $opts, $join_conds );
01042         $r = array();
01043 
01044         foreach ( $res as $row ) {
01045             $r[] = $this->repo->newFileFromRow( $row );
01046         }
01047 
01048         if ( $order == 'ASC' ) {
01049             $r = array_reverse( $r ); // make sure it ends up descending
01050         }
01051 
01052         return $r;
01053     }
01054 
01064     public function nextHistoryLine() {
01065         # Polymorphic function name to distinguish foreign and local fetches
01066         $fname = get_class( $this ) . '::' . __FUNCTION__;
01067 
01068         $dbr = $this->repo->getSlaveDB();
01069 
01070         if ( $this->historyLine == 0 ) { // called for the first time, return line from cur
01071             $this->historyRes = $dbr->select( 'image',
01072                 array(
01073                     '*',
01074                     "'' AS oi_archive_name",
01075                     '0 as oi_deleted',
01076                     'img_sha1'
01077                 ),
01078                 array( 'img_name' => $this->title->getDBkey() ),
01079                 $fname
01080             );
01081 
01082             if ( 0 == $dbr->numRows( $this->historyRes ) ) {
01083                 $this->historyRes = null;
01084 
01085                 return false;
01086             }
01087         } elseif ( $this->historyLine == 1 ) {
01088             $this->historyRes = $dbr->select( 'oldimage', '*',
01089                 array( 'oi_name' => $this->title->getDBkey() ),
01090                 $fname,
01091                 array( 'ORDER BY' => 'oi_timestamp DESC' )
01092             );
01093         }
01094         $this->historyLine++;
01095 
01096         return $dbr->fetchObject( $this->historyRes );
01097     }
01098 
01102     public function resetHistory() {
01103         $this->historyLine = 0;
01104 
01105         if ( !is_null( $this->historyRes ) ) {
01106             $this->historyRes = null;
01107         }
01108     }
01109 
01139     function upload( $srcPath, $comment, $pageText, $flags = 0, $props = false,
01140         $timestamp = false, $user = null
01141     ) {
01142         global $wgContLang;
01143 
01144         if ( $this->getRepo()->getReadOnlyReason() !== false ) {
01145             return $this->readOnlyFatalStatus();
01146         }
01147 
01148         if ( !$props ) {
01149             wfProfileIn( __METHOD__ . '-getProps' );
01150             if ( $this->repo->isVirtualUrl( $srcPath )
01151                 || FileBackend::isStoragePath( $srcPath )
01152             ) {
01153                 $props = $this->repo->getFileProps( $srcPath );
01154             } else {
01155                 $props = FSFile::getPropsFromPath( $srcPath );
01156             }
01157             wfProfileOut( __METHOD__ . '-getProps' );
01158         }
01159 
01160         $options = array();
01161         $handler = MediaHandler::getHandler( $props['mime'] );
01162         if ( $handler ) {
01163             $options['headers'] = $handler->getStreamHeaders( $props['metadata'] );
01164         } else {
01165             $options['headers'] = array();
01166         }
01167 
01168         // Trim spaces on user supplied text
01169         $comment = trim( $comment );
01170 
01171         // Truncate nicely or the DB will do it for us
01172         // non-nicely (dangling multi-byte chars, non-truncated version in cache).
01173         $comment = $wgContLang->truncate( $comment, 255 );
01174         $this->lock(); // begin
01175         $status = $this->publish( $srcPath, $flags, $options );
01176 
01177         if ( $status->successCount >= 2 ) {
01178             // There will be a copy+(one of move,copy,store).
01179             // The first succeeding does not commit us to updating the DB
01180             // since it simply copied the current version to a timestamped file name.
01181             // It is only *preferable* to avoid leaving such files orphaned.
01182             // Once the second operation goes through, then the current version was
01183             // updated and we must therefore update the DB too.
01184             if ( !$this->recordUpload2( $status->value, $comment, $pageText, $props, $timestamp, $user ) ) {
01185                 $status->fatal( 'filenotfound', $srcPath );
01186             }
01187         }
01188 
01189         $this->unlock(); // done
01190 
01191         return $status;
01192     }
01193 
01206     function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
01207         $watch = false, $timestamp = false, User $user = null ) {
01208         if ( !$user ) {
01209             global $wgUser;
01210             $user = $wgUser;
01211         }
01212 
01213         $pageText = SpecialUpload::getInitialPageText( $desc, $license, $copyStatus, $source );
01214 
01215         if ( !$this->recordUpload2( $oldver, $desc, $pageText, false, $timestamp, $user ) ) {
01216             return false;
01217         }
01218 
01219         if ( $watch ) {
01220             $user->addWatch( $this->getTitle() );
01221         }
01222 
01223         return true;
01224     }
01225 
01236     function recordUpload2( $oldver, $comment, $pageText, $props = false, $timestamp = false,
01237         $user = null
01238     ) {
01239         wfProfileIn( __METHOD__ );
01240 
01241         if ( is_null( $user ) ) {
01242             global $wgUser;
01243             $user = $wgUser;
01244         }
01245 
01246         $dbw = $this->repo->getMasterDB();
01247         $dbw->begin( __METHOD__ );
01248 
01249         if ( !$props ) {
01250             wfProfileIn( __METHOD__ . '-getProps' );
01251             $props = $this->repo->getFileProps( $this->getVirtualUrl() );
01252             wfProfileOut( __METHOD__ . '-getProps' );
01253         }
01254 
01255         # Imports or such might force a certain timestamp; otherwise we generate
01256         # it and can fudge it slightly to keep (name,timestamp) unique on re-upload.
01257         if ( $timestamp === false ) {
01258             $timestamp = $dbw->timestamp();
01259             $allowTimeKludge = true;
01260         } else {
01261             $allowTimeKludge = false;
01262         }
01263 
01264         $props['description'] = $comment;
01265         $props['user'] = $user->getId();
01266         $props['user_text'] = $user->getName();
01267         $props['timestamp'] = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
01268         $this->setProps( $props );
01269 
01270         # Fail now if the file isn't there
01271         if ( !$this->fileExists ) {
01272             wfDebug( __METHOD__ . ": File " . $this->getRel() . " went missing!\n" );
01273             $dbw->rollback( __METHOD__ );
01274             wfProfileOut( __METHOD__ );
01275 
01276             return false;
01277         }
01278 
01279         $reupload = false;
01280 
01281         # Test to see if the row exists using INSERT IGNORE
01282         # This avoids race conditions by locking the row until the commit, and also
01283         # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
01284         $dbw->insert( 'image',
01285             array(
01286                 'img_name' => $this->getName(),
01287                 'img_size' => $this->size,
01288                 'img_width' => intval( $this->width ),
01289                 'img_height' => intval( $this->height ),
01290                 'img_bits' => $this->bits,
01291                 'img_media_type' => $this->media_type,
01292                 'img_major_mime' => $this->major_mime,
01293                 'img_minor_mime' => $this->minor_mime,
01294                 'img_timestamp' => $timestamp,
01295                 'img_description' => $comment,
01296                 'img_user' => $user->getId(),
01297                 'img_user_text' => $user->getName(),
01298                 'img_metadata' => $dbw->encodeBlob( $this->metadata ),
01299                 'img_sha1' => $this->sha1
01300             ),
01301             __METHOD__,
01302             'IGNORE'
01303         );
01304         if ( $dbw->affectedRows() == 0 ) {
01305             if ( $allowTimeKludge ) {
01306                 # Use FOR UPDATE to ignore any transaction snapshotting
01307                 $ltimestamp = $dbw->selectField( 'image', 'img_timestamp',
01308                     array( 'img_name' => $this->getName() ), __METHOD__, array( 'FOR UPDATE' ) );
01309                 $lUnixtime = $ltimestamp ? wfTimestamp( TS_UNIX, $ltimestamp ) : false;
01310                 # Avoid a timestamp that is not newer than the last version
01311                 # TODO: the image/oldimage tables should be like page/revision with an ID field
01312                 if ( $lUnixtime && wfTimestamp( TS_UNIX, $timestamp ) <= $lUnixtime ) {
01313                     sleep( 1 ); // fast enough re-uploads would go far in the future otherwise
01314                     $timestamp = $dbw->timestamp( $lUnixtime + 1 );
01315                     $this->timestamp = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
01316                 }
01317             }
01318 
01319             # (bug 34993) Note: $oldver can be empty here, if the previous
01320             # version of the file was broken. Allow registration of the new
01321             # version to continue anyway, because that's better than having
01322             # an image that's not fixable by user operations.
01323 
01324             $reupload = true;
01325             # Collision, this is an update of a file
01326             # Insert previous contents into oldimage
01327             $dbw->insertSelect( 'oldimage', 'image',
01328                 array(
01329                     'oi_name' => 'img_name',
01330                     'oi_archive_name' => $dbw->addQuotes( $oldver ),
01331                     'oi_size' => 'img_size',
01332                     'oi_width' => 'img_width',
01333                     'oi_height' => 'img_height',
01334                     'oi_bits' => 'img_bits',
01335                     'oi_timestamp' => 'img_timestamp',
01336                     'oi_description' => 'img_description',
01337                     'oi_user' => 'img_user',
01338                     'oi_user_text' => 'img_user_text',
01339                     'oi_metadata' => 'img_metadata',
01340                     'oi_media_type' => 'img_media_type',
01341                     'oi_major_mime' => 'img_major_mime',
01342                     'oi_minor_mime' => 'img_minor_mime',
01343                     'oi_sha1' => 'img_sha1'
01344                 ),
01345                 array( 'img_name' => $this->getName() ),
01346                 __METHOD__
01347             );
01348 
01349             # Update the current image row
01350             $dbw->update( 'image',
01351                 array( /* SET */
01352                     'img_size' => $this->size,
01353                     'img_width' => intval( $this->width ),
01354                     'img_height' => intval( $this->height ),
01355                     'img_bits' => $this->bits,
01356                     'img_media_type' => $this->media_type,
01357                     'img_major_mime' => $this->major_mime,
01358                     'img_minor_mime' => $this->minor_mime,
01359                     'img_timestamp' => $timestamp,
01360                     'img_description' => $comment,
01361                     'img_user' => $user->getId(),
01362                     'img_user_text' => $user->getName(),
01363                     'img_metadata' => $dbw->encodeBlob( $this->metadata ),
01364                     'img_sha1' => $this->sha1
01365                 ),
01366                 array( 'img_name' => $this->getName() ),
01367                 __METHOD__
01368             );
01369         } else {
01370             # This is a new file, so update the image count
01371             DeferredUpdates::addUpdate( SiteStatsUpdate::factory( array( 'images' => 1 ) ) );
01372         }
01373 
01374         $descTitle = $this->getTitle();
01375         $wikiPage = new WikiFilePage( $descTitle );
01376         $wikiPage->setFile( $this );
01377 
01378         # Add the log entry
01379         $action = $reupload ? 'overwrite' : 'upload';
01380 
01381         $logEntry = new ManualLogEntry( 'upload', $action );
01382         $logEntry->setPerformer( $user );
01383         $logEntry->setComment( $comment );
01384         $logEntry->setTarget( $descTitle );
01385 
01386         // Allow people using the api to associate log entries with the upload.
01387         // Log has a timestamp, but sometimes different from upload timestamp.
01388         $logEntry->setParameters(
01389             array(
01390                 'img_sha1' => $this->sha1,
01391                 'img_timestamp' => $timestamp,
01392             )
01393         );
01394         // Note we keep $logId around since during new image
01395         // creation, page doesn't exist yet, so log_page = 0
01396         // but we want it to point to the page we're making,
01397         // so we later modify the log entry.
01398         // For a similar reason, we avoid making an RC entry
01399         // now and wait until the page exists.
01400         $logId = $logEntry->insert();
01401 
01402         $exists = $descTitle->exists();
01403         if ( $exists ) {
01404             // Page exists, do RC entry now (otherwise we wait for later).
01405             $logEntry->publish( $logId );
01406         }
01407         wfProfileIn( __METHOD__ . '-edit' );
01408 
01409         if ( $exists ) {
01410             # Create a null revision
01411             $latest = $descTitle->getLatestRevID();
01412             $editSummary = LogFormatter::newFromEntry( $logEntry )->getPlainActionText();
01413 
01414             $nullRevision = Revision::newNullRevision(
01415                 $dbw,
01416                 $descTitle->getArticleID(),
01417                 $editSummary,
01418                 false,
01419                 $user
01420             );
01421             if ( !is_null( $nullRevision ) ) {
01422                 $nullRevision->insertOn( $dbw );
01423 
01424                 wfRunHooks( 'NewRevisionFromEditComplete', array( $wikiPage, $nullRevision, $latest, $user ) );
01425                 $wikiPage->updateRevisionOn( $dbw, $nullRevision );
01426             }
01427         }
01428 
01429         # Commit the transaction now, in case something goes wrong later
01430         # The most important thing is that files don't get lost, especially archives
01431         # NOTE: once we have support for nested transactions, the commit may be moved
01432         #       to after $wikiPage->doEdit has been called.
01433         $dbw->commit( __METHOD__ );
01434 
01435         # Save to memcache.
01436         # We shall not saveToCache before the commit since otherwise
01437         # in case of a rollback there is an usable file from memcached
01438         # which in fact doesn't really exist (bug 24978)
01439         $this->saveToCache();
01440 
01441         if ( $exists ) {
01442             # Invalidate the cache for the description page
01443             $descTitle->invalidateCache();
01444             $descTitle->purgeSquid();
01445         } else {
01446             # New file; create the description page.
01447             # There's already a log entry, so don't make a second RC entry
01448             # Squid and file cache for the description page are purged by doEditContent.
01449             $content = ContentHandler::makeContent( $pageText, $descTitle );
01450             $status = $wikiPage->doEditContent(
01451                 $content,
01452                 $comment,
01453                 EDIT_NEW | EDIT_SUPPRESS_RC,
01454                 false,
01455                 $user
01456             );
01457 
01458             $dbw->begin( __METHOD__ ); // XXX; doEdit() uses a transaction
01459             // Now that the page exists, make an RC entry.
01460             $logEntry->publish( $logId );
01461             if ( isset( $status->value['revision'] ) ) {
01462                 $dbw->update( 'logging',
01463                     array( 'log_page' => $status->value['revision']->getPage() ),
01464                     array( 'log_id' => $logId ),
01465                     __METHOD__
01466                 );
01467             }
01468             $dbw->commit( __METHOD__ ); // commit before anything bad can happen
01469         }
01470 
01471         wfProfileOut( __METHOD__ . '-edit' );
01472 
01473         if ( $reupload ) {
01474             # Delete old thumbnails
01475             wfProfileIn( __METHOD__ . '-purge' );
01476             $this->purgeThumbnails();
01477             wfProfileOut( __METHOD__ . '-purge' );
01478 
01479             # Remove the old file from the squid cache
01480             SquidUpdate::purge( array( $this->getURL() ) );
01481         }
01482 
01483         # Hooks, hooks, the magic of hooks...
01484         wfProfileIn( __METHOD__ . '-hooks' );
01485         wfRunHooks( 'FileUpload', array( $this, $reupload, $descTitle->exists() ) );
01486         wfProfileOut( __METHOD__ . '-hooks' );
01487 
01488         # Invalidate cache for all pages using this file
01489         $update = new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' );
01490         $update->doUpdate();
01491         if ( !$reupload ) {
01492             LinksUpdate::queueRecursiveJobsForTable( $this->getTitle(), 'imagelinks' );
01493         }
01494 
01495         wfProfileOut( __METHOD__ );
01496 
01497         return true;
01498     }
01499 
01515     function publish( $srcPath, $flags = 0, array $options = array() ) {
01516         return $this->publishTo( $srcPath, $this->getRel(), $flags, $options );
01517     }
01518 
01534     function publishTo( $srcPath, $dstRel, $flags = 0, array $options = array() ) {
01535         if ( $this->getRepo()->getReadOnlyReason() !== false ) {
01536             return $this->readOnlyFatalStatus();
01537         }
01538 
01539         $this->lock(); // begin
01540 
01541         $archiveName = wfTimestamp( TS_MW ) . '!' . $this->getName();
01542         $archiveRel = 'archive/' . $this->getHashPath() . $archiveName;
01543         $flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0;
01544         $status = $this->repo->publish( $srcPath, $dstRel, $archiveRel, $flags, $options );
01545 
01546         if ( $status->value == 'new' ) {
01547             $status->value = '';
01548         } else {
01549             $status->value = $archiveName;
01550         }
01551 
01552         $this->unlock(); // done
01553 
01554         return $status;
01555     }
01556 
01574     function move( $target ) {
01575         if ( $this->getRepo()->getReadOnlyReason() !== false ) {
01576             return $this->readOnlyFatalStatus();
01577         }
01578 
01579         wfDebugLog( 'imagemove', "Got request to move {$this->name} to " . $target->getText() );
01580         $batch = new LocalFileMoveBatch( $this, $target );
01581 
01582         $this->lock(); // begin
01583         $batch->addCurrent();
01584         $archiveNames = $batch->addOlds();
01585         $status = $batch->execute();
01586         $this->unlock(); // done
01587 
01588         wfDebugLog( 'imagemove', "Finished moving {$this->name}" );
01589 
01590         // Purge the source and target files...
01591         $oldTitleFile = wfLocalFile( $this->title );
01592         $newTitleFile = wfLocalFile( $target );
01593         // Hack: the lock()/unlock() pair is nested in a transaction so the locking is not
01594         // tied to BEGIN/COMMIT. To avoid slow purges in the transaction, move them outside.
01595         $this->getRepo()->getMasterDB()->onTransactionIdle(
01596             function () use ( $oldTitleFile, $newTitleFile, $archiveNames ) {
01597                 $oldTitleFile->purgeEverything();
01598                 foreach ( $archiveNames as $archiveName ) {
01599                     $oldTitleFile->purgeOldThumbnails( $archiveName );
01600                 }
01601                 $newTitleFile->purgeEverything();
01602             }
01603         );
01604 
01605         if ( $status->isOK() ) {
01606             // Now switch the object
01607             $this->title = $target;
01608             // Force regeneration of the name and hashpath
01609             unset( $this->name );
01610             unset( $this->hashPath );
01611         }
01612 
01613         return $status;
01614     }
01615 
01629     function delete( $reason, $suppress = false, $user = null ) {
01630         if ( $this->getRepo()->getReadOnlyReason() !== false ) {
01631             return $this->readOnlyFatalStatus();
01632         }
01633 
01634         $batch = new LocalFileDeleteBatch( $this, $reason, $suppress, $user );
01635 
01636         $this->lock(); // begin
01637         $batch->addCurrent();
01638         # Get old version relative paths
01639         $archiveNames = $batch->addOlds();
01640         $status = $batch->execute();
01641         $this->unlock(); // done
01642 
01643         if ( $status->isOK() ) {
01644             DeferredUpdates::addUpdate( SiteStatsUpdate::factory( array( 'images' => -1 ) ) );
01645         }
01646 
01647         // Hack: the lock()/unlock() pair is nested in a transaction so the locking is not
01648         // tied to BEGIN/COMMIT. To avoid slow purges in the transaction, move them outside.
01649         $file = $this;
01650         $this->getRepo()->getMasterDB()->onTransactionIdle(
01651             function () use ( $file, $archiveNames ) {
01652                 global $wgUseSquid;
01653 
01654                 $file->purgeEverything();
01655                 foreach ( $archiveNames as $archiveName ) {
01656                     $file->purgeOldThumbnails( $archiveName );
01657                 }
01658 
01659                 if ( $wgUseSquid ) {
01660                     // Purge the squid
01661                     $purgeUrls = array();
01662                     foreach ( $archiveNames as $archiveName ) {
01663                         $purgeUrls[] = $file->getArchiveUrl( $archiveName );
01664                     }
01665                     SquidUpdate::purge( $purgeUrls );
01666                 }
01667             }
01668         );
01669 
01670         return $status;
01671     }
01672 
01688     function deleteOld( $archiveName, $reason, $suppress = false, $user = null ) {
01689         global $wgUseSquid;
01690         if ( $this->getRepo()->getReadOnlyReason() !== false ) {
01691             return $this->readOnlyFatalStatus();
01692         }
01693 
01694         $batch = new LocalFileDeleteBatch( $this, $reason, $suppress, $user );
01695 
01696         $this->lock(); // begin
01697         $batch->addOld( $archiveName );
01698         $status = $batch->execute();
01699         $this->unlock(); // done
01700 
01701         $this->purgeOldThumbnails( $archiveName );
01702         if ( $status->isOK() ) {
01703             $this->purgeDescription();
01704             $this->purgeHistory();
01705         }
01706 
01707         if ( $wgUseSquid ) {
01708             // Purge the squid
01709             SquidUpdate::purge( array( $this->getArchiveUrl( $archiveName ) ) );
01710         }
01711 
01712         return $status;
01713     }
01714 
01726     function restore( $versions = array(), $unsuppress = false ) {
01727         if ( $this->getRepo()->getReadOnlyReason() !== false ) {
01728             return $this->readOnlyFatalStatus();
01729         }
01730 
01731         $batch = new LocalFileRestoreBatch( $this, $unsuppress );
01732 
01733         $this->lock(); // begin
01734         if ( !$versions ) {
01735             $batch->addAll();
01736         } else {
01737             $batch->addIds( $versions );
01738         }
01739         $status = $batch->execute();
01740         if ( $status->isGood() ) {
01741             $cleanupStatus = $batch->cleanup();
01742             $cleanupStatus->successCount = 0;
01743             $cleanupStatus->failCount = 0;
01744             $status->merge( $cleanupStatus );
01745         }
01746         $this->unlock(); // done
01747 
01748         return $status;
01749     }
01750 
01760     function getDescriptionUrl() {
01761         return $this->title->getLocalURL();
01762     }
01763 
01772     function getDescriptionText( $lang = null ) {
01773         $revision = Revision::newFromTitle( $this->title, false, Revision::READ_NORMAL );
01774         if ( !$revision ) {
01775             return false;
01776         }
01777         $content = $revision->getContent();
01778         if ( !$content ) {
01779             return false;
01780         }
01781         $pout = $content->getParserOutput( $this->title, null, new ParserOptions( null, $lang ) );
01782 
01783         return $pout->getText();
01784     }
01785 
01791     function getDescription( $audience = self::FOR_PUBLIC, User $user = null ) {
01792         $this->load();
01793         if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
01794             return '';
01795         } elseif ( $audience == self::FOR_THIS_USER
01796             && !$this->userCan( self::DELETED_COMMENT, $user )
01797         ) {
01798             return '';
01799         } else {
01800             return $this->description;
01801         }
01802     }
01803 
01807     function getTimestamp() {
01808         $this->load();
01809 
01810         return $this->timestamp;
01811     }
01812 
01816     function getSha1() {
01817         $this->load();
01818         // Initialise now if necessary
01819         if ( $this->sha1 == '' && $this->fileExists ) {
01820             $this->lock(); // begin
01821 
01822             $this->sha1 = $this->repo->getFileSha1( $this->getPath() );
01823             if ( !wfReadOnly() && strval( $this->sha1 ) != '' ) {
01824                 $dbw = $this->repo->getMasterDB();
01825                 $dbw->update( 'image',
01826                     array( 'img_sha1' => $this->sha1 ),
01827                     array( 'img_name' => $this->getName() ),
01828                     __METHOD__ );
01829                 $this->saveToCache();
01830             }
01831 
01832             $this->unlock(); // done
01833         }
01834 
01835         return $this->sha1;
01836     }
01837 
01841     function isCacheable() {
01842         $this->load();
01843 
01844         // If extra data (metadata) was not loaded then it must have been large
01845         return $this->extraDataLoaded
01846         && strlen( serialize( $this->metadata ) ) <= self::CACHE_FIELD_MAX_LEN;
01847     }
01848 
01855     function lock() {
01856         $dbw = $this->repo->getMasterDB();
01857 
01858         if ( !$this->locked ) {
01859             if ( !$dbw->trxLevel() ) {
01860                 $dbw->begin( __METHOD__ );
01861                 $this->lockedOwnTrx = true;
01862             }
01863             $this->locked++;
01864             // Bug 54736: use simple lock to handle when the file does not exist.
01865             // SELECT FOR UPDATE prevents changes, not other SELECTs with FOR UPDATE.
01866             // Also, that would cause contention on INSERT of similarly named rows.
01867             $backend = $this->getRepo()->getBackend();
01868             $lockPaths = array( $this->getPath() ); // represents all versions of the file
01869             $status = $backend->lockFiles( $lockPaths, LockManager::LOCK_EX, 5 );
01870             if ( !$status->isGood() ) {
01871                 throw new MWException( "Could not acquire lock for '{$this->getName()}.'" );
01872             }
01873             $dbw->onTransactionIdle( function () use ( $backend, $lockPaths ) {
01874                 $backend->unlockFiles( $lockPaths, LockManager::LOCK_EX ); // release on commit
01875             } );
01876         }
01877 
01878         $this->markVolatile(); // file may change soon
01879 
01880         return true;
01881     }
01882 
01887     function unlock() {
01888         if ( $this->locked ) {
01889             --$this->locked;
01890             if ( !$this->locked && $this->lockedOwnTrx ) {
01891                 $dbw = $this->repo->getMasterDB();
01892                 $dbw->commit( __METHOD__ );
01893                 $this->lockedOwnTrx = false;
01894             }
01895         }
01896     }
01897 
01905     protected function markVolatile() {
01906         global $wgMemc;
01907 
01908         $key = $this->repo->getSharedCacheKey( 'file-volatile', md5( $this->getName() ) );
01909         if ( $key ) {
01910             $this->lastMarkedVolatile = time();
01911             return $wgMemc->set( $key, $this->lastMarkedVolatile, self::VOLATILE_TTL );
01912         }
01913 
01914         return true;
01915     }
01916 
01923     protected function isVolatile() {
01924         global $wgMemc;
01925 
01926         $key = $this->repo->getSharedCacheKey( 'file-volatile', md5( $this->getName() ) );
01927         if ( !$key ) {
01928             // repo unavailable; bail.
01929             return false;
01930         }
01931 
01932         if ( $this->lastMarkedVolatile === 0 ) {
01933             $this->lastMarkedVolatile = $wgMemc->get( $key ) ?: 0;
01934         }
01935 
01936         $volatileDuration = time() - $this->lastMarkedVolatile;
01937         return $volatileDuration <= self::VOLATILE_TTL;
01938     }
01939 
01943     function unlockAndRollback() {
01944         $this->locked = false;
01945         $dbw = $this->repo->getMasterDB();
01946         $dbw->rollback( __METHOD__ );
01947         $this->lockedOwnTrx = false;
01948     }
01949 
01953     protected function readOnlyFatalStatus() {
01954         return $this->getRepo()->newFatal( 'filereadonlyerror', $this->getName(),
01955             $this->getRepo()->getName(), $this->getRepo()->getReadOnlyReason() );
01956     }
01957 
01961     function __destruct() {
01962         $this->unlock();
01963     }
01964 } // LocalFile class
01965 
01966 # ------------------------------------------------------------------------------
01967 
01972 class LocalFileDeleteBatch {
01974     private $file;
01975 
01977     private $reason;
01978 
01980     private $srcRels = array();
01981 
01983     private $archiveUrls = array();
01984 
01986     private $deletionBatch;
01987 
01989     private $suppress;
01990 
01992     private $status;
01993 
01995     private $user;
01996 
02003     function __construct( File $file, $reason = '', $suppress = false, $user = null ) {
02004         $this->file = $file;
02005         $this->reason = $reason;
02006         $this->suppress = $suppress;
02007         if ( $user ) {
02008             $this->user = $user;
02009         } else {
02010             global $wgUser;
02011             $this->user = $wgUser;
02012         }
02013         $this->status = $file->repo->newGood();
02014     }
02015 
02016     function addCurrent() {
02017         $this->srcRels['.'] = $this->file->getRel();
02018     }
02019 
02023     function addOld( $oldName ) {
02024         $this->srcRels[$oldName] = $this->file->getArchiveRel( $oldName );
02025         $this->archiveUrls[] = $this->file->getArchiveUrl( $oldName );
02026     }
02027 
02032     function addOlds() {
02033         $archiveNames = array();
02034 
02035         $dbw = $this->file->repo->getMasterDB();
02036         $result = $dbw->select( 'oldimage',
02037             array( 'oi_archive_name' ),
02038             array( 'oi_name' => $this->file->getName() ),
02039             __METHOD__
02040         );
02041 
02042         foreach ( $result as $row ) {
02043             $this->addOld( $row->oi_archive_name );
02044             $archiveNames[] = $row->oi_archive_name;
02045         }
02046 
02047         return $archiveNames;
02048     }
02049 
02053     function getOldRels() {
02054         if ( !isset( $this->srcRels['.'] ) ) {
02055             $oldRels =& $this->srcRels;
02056             $deleteCurrent = false;
02057         } else {
02058             $oldRels = $this->srcRels;
02059             unset( $oldRels['.'] );
02060             $deleteCurrent = true;
02061         }
02062 
02063         return array( $oldRels, $deleteCurrent );
02064     }
02065 
02069     protected function getHashes() {
02070         $hashes = array();
02071         list( $oldRels, $deleteCurrent ) = $this->getOldRels();
02072 
02073         if ( $deleteCurrent ) {
02074             $hashes['.'] = $this->file->getSha1();
02075         }
02076 
02077         if ( count( $oldRels ) ) {
02078             $dbw = $this->file->repo->getMasterDB();
02079             $res = $dbw->select(
02080                 'oldimage',
02081                 array( 'oi_archive_name', 'oi_sha1' ),
02082                 array( 'oi_archive_name' => array_keys( $oldRels ),
02083                     'oi_name' => $this->file->getName() ), // performance
02084                 __METHOD__
02085             );
02086 
02087             foreach ( $res as $row ) {
02088                 if ( rtrim( $row->oi_sha1, "\0" ) === '' ) {
02089                     // Get the hash from the file
02090                     $oldUrl = $this->file->getArchiveVirtualUrl( $row->oi_archive_name );
02091                     $props = $this->file->repo->getFileProps( $oldUrl );
02092 
02093                     if ( $props['fileExists'] ) {
02094                         // Upgrade the oldimage row
02095                         $dbw->update( 'oldimage',
02096                             array( 'oi_sha1' => $props['sha1'] ),
02097                             array( 'oi_name' => $this->file->getName(), 'oi_archive_name' => $row->oi_archive_name ),
02098                             __METHOD__ );
02099                         $hashes[$row->oi_archive_name] = $props['sha1'];
02100                     } else {
02101                         $hashes[$row->oi_archive_name] = false;
02102                     }
02103                 } else {
02104                     $hashes[$row->oi_archive_name] = $row->oi_sha1;
02105                 }
02106             }
02107         }
02108 
02109         $missing = array_diff_key( $this->srcRels, $hashes );
02110 
02111         foreach ( $missing as $name => $rel ) {
02112             $this->status->error( 'filedelete-old-unregistered', $name );
02113         }
02114 
02115         foreach ( $hashes as $name => $hash ) {
02116             if ( !$hash ) {
02117                 $this->status->error( 'filedelete-missing', $this->srcRels[$name] );
02118                 unset( $hashes[$name] );
02119             }
02120         }
02121 
02122         return $hashes;
02123     }
02124 
02125     function doDBInserts() {
02126         $dbw = $this->file->repo->getMasterDB();
02127         $encTimestamp = $dbw->addQuotes( $dbw->timestamp() );
02128         $encUserId = $dbw->addQuotes( $this->user->getId() );
02129         $encReason = $dbw->addQuotes( $this->reason );
02130         $encGroup = $dbw->addQuotes( 'deleted' );
02131         $ext = $this->file->getExtension();
02132         $dotExt = $ext === '' ? '' : ".$ext";
02133         $encExt = $dbw->addQuotes( $dotExt );
02134         list( $oldRels, $deleteCurrent ) = $this->getOldRels();
02135 
02136         // Bitfields to further suppress the content
02137         if ( $this->suppress ) {
02138             $bitfield = 0;
02139             // This should be 15...
02140             $bitfield |= Revision::DELETED_TEXT;
02141             $bitfield |= Revision::DELETED_COMMENT;
02142             $bitfield |= Revision::DELETED_USER;
02143             $bitfield |= Revision::DELETED_RESTRICTED;
02144         } else {
02145             $bitfield = 'oi_deleted';
02146         }
02147 
02148         if ( $deleteCurrent ) {
02149             $concat = $dbw->buildConcat( array( "img_sha1", $encExt ) );
02150             $where = array( 'img_name' => $this->file->getName() );
02151             $dbw->insertSelect( 'filearchive', 'image',
02152                 array(
02153                     'fa_storage_group' => $encGroup,
02154                     'fa_storage_key' => $dbw->conditional(
02155                         array( 'img_sha1' => '' ),
02156                         $dbw->addQuotes( '' ),
02157                         $concat
02158                     ),
02159                     'fa_deleted_user' => $encUserId,
02160                     'fa_deleted_timestamp' => $encTimestamp,
02161                     'fa_deleted_reason' => $encReason,
02162                     'fa_deleted' => $this->suppress ? $bitfield : 0,
02163 
02164                     'fa_name' => 'img_name',
02165                     'fa_archive_name' => 'NULL',
02166                     'fa_size' => 'img_size',
02167                     'fa_width' => 'img_width',
02168                     'fa_height' => 'img_height',
02169                     'fa_metadata' => 'img_metadata',
02170                     'fa_bits' => 'img_bits',
02171                     'fa_media_type' => 'img_media_type',
02172                     'fa_major_mime' => 'img_major_mime',
02173                     'fa_minor_mime' => 'img_minor_mime',
02174                     'fa_description' => 'img_description',
02175                     'fa_user' => 'img_user',
02176                     'fa_user_text' => 'img_user_text',
02177                     'fa_timestamp' => 'img_timestamp',
02178                     'fa_sha1' => 'img_sha1',
02179                 ), $where, __METHOD__ );
02180         }
02181 
02182         if ( count( $oldRels ) ) {
02183             $concat = $dbw->buildConcat( array( "oi_sha1", $encExt ) );
02184             $where = array(
02185                 'oi_name' => $this->file->getName(),
02186                 'oi_archive_name' => array_keys( $oldRels ) );
02187             $dbw->insertSelect( 'filearchive', 'oldimage',
02188                 array(
02189                     'fa_storage_group' => $encGroup,
02190                     'fa_storage_key' => $dbw->conditional(
02191                         array( 'oi_sha1' => '' ),
02192                         $dbw->addQuotes( '' ),
02193                         $concat
02194                     ),
02195                     'fa_deleted_user' => $encUserId,
02196                     'fa_deleted_timestamp' => $encTimestamp,
02197                     'fa_deleted_reason' => $encReason,
02198                     'fa_deleted' => $this->suppress ? $bitfield : 'oi_deleted',
02199 
02200                     'fa_name' => 'oi_name',
02201                     'fa_archive_name' => 'oi_archive_name',
02202                     'fa_size' => 'oi_size',
02203                     'fa_width' => 'oi_width',
02204                     'fa_height' => 'oi_height',
02205                     'fa_metadata' => 'oi_metadata',
02206                     'fa_bits' => 'oi_bits',
02207                     'fa_media_type' => 'oi_media_type',
02208                     'fa_major_mime' => 'oi_major_mime',
02209                     'fa_minor_mime' => 'oi_minor_mime',
02210                     'fa_description' => 'oi_description',
02211                     'fa_user' => 'oi_user',
02212                     'fa_user_text' => 'oi_user_text',
02213                     'fa_timestamp' => 'oi_timestamp',
02214                     'fa_sha1' => 'oi_sha1',
02215                 ), $where, __METHOD__ );
02216         }
02217     }
02218 
02219     function doDBDeletes() {
02220         $dbw = $this->file->repo->getMasterDB();
02221         list( $oldRels, $deleteCurrent ) = $this->getOldRels();
02222 
02223         if ( count( $oldRels ) ) {
02224             $dbw->delete( 'oldimage',
02225                 array(
02226                     'oi_name' => $this->file->getName(),
02227                     'oi_archive_name' => array_keys( $oldRels )
02228                 ), __METHOD__ );
02229         }
02230 
02231         if ( $deleteCurrent ) {
02232             $dbw->delete( 'image', array( 'img_name' => $this->file->getName() ), __METHOD__ );
02233         }
02234     }
02235 
02240     function execute() {
02241         wfProfileIn( __METHOD__ );
02242 
02243         $this->file->lock();
02244         // Leave private files alone
02245         $privateFiles = array();
02246         list( $oldRels, ) = $this->getOldRels();
02247         $dbw = $this->file->repo->getMasterDB();
02248 
02249         if ( !empty( $oldRels ) ) {
02250             $res = $dbw->select( 'oldimage',
02251                 array( 'oi_archive_name' ),
02252                 array( 'oi_name' => $this->file->getName(),
02253                     'oi_archive_name' => array_keys( $oldRels ),
02254                     $dbw->bitAnd( 'oi_deleted', File::DELETED_FILE ) => File::DELETED_FILE ),
02255                 __METHOD__ );
02256 
02257             foreach ( $res as $row ) {
02258                 $privateFiles[$row->oi_archive_name] = 1;
02259             }
02260         }
02261         // Prepare deletion batch
02262         $hashes = $this->getHashes();
02263         $this->deletionBatch = array();
02264         $ext = $this->file->getExtension();
02265         $dotExt = $ext === '' ? '' : ".$ext";
02266 
02267         foreach ( $this->srcRels as $name => $srcRel ) {
02268             // Skip files that have no hash (missing source).
02269             // Keep private files where they are.
02270             if ( isset( $hashes[$name] ) && !array_key_exists( $name, $privateFiles ) ) {
02271                 $hash = $hashes[$name];
02272                 $key = $hash . $dotExt;
02273                 $dstRel = $this->file->repo->getDeletedHashPath( $key ) . $key;
02274                 $this->deletionBatch[$name] = array( $srcRel, $dstRel );
02275             }
02276         }
02277 
02278         // Lock the filearchive rows so that the files don't get deleted by a cleanup operation
02279         // We acquire this lock by running the inserts now, before the file operations.
02280         //
02281         // This potentially has poor lock contention characteristics -- an alternative
02282         // scheme would be to insert stub filearchive entries with no fa_name and commit
02283         // them in a separate transaction, then run the file ops, then update the fa_name fields.
02284         $this->doDBInserts();
02285 
02286         // Removes non-existent file from the batch, so we don't get errors.
02287         $checkStatus = $this->removeNonexistentFiles( $this->deletionBatch );
02288         if ( !$checkStatus->isGood() ) {
02289             $this->status->merge( $checkStatus );
02290             return $this->status;
02291         }
02292         $this->deletionBatch = $checkStatus->value;
02293 
02294         // Execute the file deletion batch
02295         $status = $this->file->repo->deleteBatch( $this->deletionBatch );
02296 
02297         if ( !$status->isGood() ) {
02298             $this->status->merge( $status );
02299         }
02300 
02301         if ( !$this->status->isOK() ) {
02302             // Critical file deletion error
02303             // Roll back inserts, release lock and abort
02304             // TODO: delete the defunct filearchive rows if we are using a non-transactional DB
02305             $this->file->unlockAndRollback();
02306             wfProfileOut( __METHOD__ );
02307 
02308             return $this->status;
02309         }
02310 
02311         // Delete image/oldimage rows
02312         $this->doDBDeletes();
02313 
02314         // Commit and return
02315         $this->file->unlock();
02316         wfProfileOut( __METHOD__ );
02317 
02318         return $this->status;
02319     }
02320 
02326     function removeNonexistentFiles( $batch ) {
02327         $files = $newBatch = array();
02328 
02329         foreach ( $batch as $batchItem ) {
02330             list( $src, ) = $batchItem;
02331             $files[$src] = $this->file->repo->getVirtualUrl( 'public' ) . '/' . rawurlencode( $src );
02332         }
02333 
02334         $result = $this->file->repo->fileExistsBatch( $files );
02335         if ( in_array( null, $result, true ) ) {
02336             return Status::newFatal( 'backend-fail-internal',
02337                 $this->file->repo->getBackend()->getName() );
02338         }
02339 
02340         foreach ( $batch as $batchItem ) {
02341             if ( $result[$batchItem[0]] ) {
02342                 $newBatch[] = $batchItem;
02343             }
02344         }
02345 
02346         return Status::newGood( $newBatch );
02347     }
02348 }
02349 
02350 # ------------------------------------------------------------------------------
02351 
02356 class LocalFileRestoreBatch {
02358     private $file;
02359 
02361     private $cleanupBatch;
02362 
02364     private $ids;
02365 
02367     private $all;
02368 
02370     private $unsuppress = false;
02371 
02376     function __construct( File $file, $unsuppress = false ) {
02377         $this->file = $file;
02378         $this->cleanupBatch = $this->ids = array();
02379         $this->ids = array();
02380         $this->unsuppress = $unsuppress;
02381     }
02382 
02387     function addId( $fa_id ) {
02388         $this->ids[] = $fa_id;
02389     }
02390 
02395     function addIds( $ids ) {
02396         $this->ids = array_merge( $this->ids, $ids );
02397     }
02398 
02402     function addAll() {
02403         $this->all = true;
02404     }
02405 
02414     function execute() {
02415         global $wgLang;
02416 
02417         if ( !$this->all && !$this->ids ) {
02418             // Do nothing
02419             return $this->file->repo->newGood();
02420         }
02421 
02422         $this->file->lock();
02423 
02424         $dbw = $this->file->repo->getMasterDB();
02425         $status = $this->file->repo->newGood();
02426 
02427         $exists = (bool)$dbw->selectField( 'image', '1',
02428             array( 'img_name' => $this->file->getName() ), __METHOD__, array( 'FOR UPDATE' ) );
02429 
02430         // Fetch all or selected archived revisions for the file,
02431         // sorted from the most recent to the oldest.
02432         $conditions = array( 'fa_name' => $this->file->getName() );
02433 
02434         if ( !$this->all ) {
02435             $conditions['fa_id'] = $this->ids;
02436         }
02437 
02438         $result = $dbw->select(
02439             'filearchive',
02440             ArchivedFile::selectFields(),
02441             $conditions,
02442             __METHOD__,
02443             array( 'ORDER BY' => 'fa_timestamp DESC' )
02444         );
02445 
02446         $idsPresent = array();
02447         $storeBatch = array();
02448         $insertBatch = array();
02449         $insertCurrent = false;
02450         $deleteIds = array();
02451         $first = true;
02452         $archiveNames = array();
02453 
02454         foreach ( $result as $row ) {
02455             $idsPresent[] = $row->fa_id;
02456 
02457             if ( $row->fa_name != $this->file->getName() ) {
02458                 $status->error( 'undelete-filename-mismatch', $wgLang->timeanddate( $row->fa_timestamp ) );
02459                 $status->failCount++;
02460                 continue;
02461             }
02462 
02463             if ( $row->fa_storage_key == '' ) {
02464                 // Revision was missing pre-deletion
02465                 $status->error( 'undelete-bad-store-key', $wgLang->timeanddate( $row->fa_timestamp ) );
02466                 $status->failCount++;
02467                 continue;
02468             }
02469 
02470             $deletedRel = $this->file->repo->getDeletedHashPath( $row->fa_storage_key ) .
02471                 $row->fa_storage_key;
02472             $deletedUrl = $this->file->repo->getVirtualUrl() . '/deleted/' . $deletedRel;
02473 
02474             if ( isset( $row->fa_sha1 ) ) {
02475                 $sha1 = $row->fa_sha1;
02476             } else {
02477                 // old row, populate from key
02478                 $sha1 = LocalRepo::getHashFromKey( $row->fa_storage_key );
02479             }
02480 
02481             # Fix leading zero
02482             if ( strlen( $sha1 ) == 32 && $sha1[0] == '0' ) {
02483                 $sha1 = substr( $sha1, 1 );
02484             }
02485 
02486             if ( is_null( $row->fa_major_mime ) || $row->fa_major_mime == 'unknown'
02487                 || is_null( $row->fa_minor_mime ) || $row->fa_minor_mime == 'unknown'
02488                 || is_null( $row->fa_media_type ) || $row->fa_media_type == 'UNKNOWN'
02489                 || is_null( $row->fa_metadata )
02490             ) {
02491                 // Refresh our metadata
02492                 // Required for a new current revision; nice for older ones too. :)
02493                 $props = RepoGroup::singleton()->getFileProps( $deletedUrl );
02494             } else {
02495                 $props = array(
02496                     'minor_mime' => $row->fa_minor_mime,
02497                     'major_mime' => $row->fa_major_mime,
02498                     'media_type' => $row->fa_media_type,
02499                     'metadata' => $row->fa_metadata
02500                 );
02501             }
02502 
02503             if ( $first && !$exists ) {
02504                 // This revision will be published as the new current version
02505                 $destRel = $this->file->getRel();
02506                 $insertCurrent = array(
02507                     'img_name' => $row->fa_name,
02508                     'img_size' => $row->fa_size,
02509                     'img_width' => $row->fa_width,
02510                     'img_height' => $row->fa_height,
02511                     'img_metadata' => $props['metadata'],
02512                     'img_bits' => $row->fa_bits,
02513                     'img_media_type' => $props['media_type'],
02514                     'img_major_mime' => $props['major_mime'],
02515                     'img_minor_mime' => $props['minor_mime'],
02516                     'img_description' => $row->fa_description,
02517                     'img_user' => $row->fa_user,
02518                     'img_user_text' => $row->fa_user_text,
02519                     'img_timestamp' => $row->fa_timestamp,
02520                     'img_sha1' => $sha1
02521                 );
02522 
02523                 // The live (current) version cannot be hidden!
02524                 if ( !$this->unsuppress && $row->fa_deleted ) {
02525                     $storeBatch[] = array( $deletedUrl, 'public', $destRel );
02526                     $this->cleanupBatch[] = $row->fa_storage_key;
02527                 }
02528             } else {
02529                 $archiveName = $row->fa_archive_name;
02530 
02531                 if ( $archiveName == '' ) {
02532                     // This was originally a current version; we
02533                     // have to devise a new archive name for it.
02534                     // Format is <timestamp of archiving>!<name>
02535                     $timestamp = wfTimestamp( TS_UNIX, $row->fa_deleted_timestamp );
02536 
02537                     do {
02538                         $archiveName = wfTimestamp( TS_MW, $timestamp ) . '!' . $row->fa_name;
02539                         $timestamp++;
02540                     } while ( isset( $archiveNames[$archiveName] ) );
02541                 }
02542 
02543                 $archiveNames[$archiveName] = true;
02544                 $destRel = $this->file->getArchiveRel( $archiveName );
02545                 $insertBatch[] = array(
02546                     'oi_name' => $row->fa_name,
02547                     'oi_archive_name' => $archiveName,
02548                     'oi_size' => $row->fa_size,
02549                     'oi_width' => $row->fa_width,
02550                     'oi_height' => $row->fa_height,
02551                     'oi_bits' => $row->fa_bits,
02552                     'oi_description' => $row->fa_description,
02553                     'oi_user' => $row->fa_user,
02554                     'oi_user_text' => $row->fa_user_text,
02555                     'oi_timestamp' => $row->fa_timestamp,
02556                     'oi_metadata' => $props['metadata'],
02557                     'oi_media_type' => $props['media_type'],
02558                     'oi_major_mime' => $props['major_mime'],
02559                     'oi_minor_mime' => $props['minor_mime'],
02560                     'oi_deleted' => $this->unsuppress ? 0 : $row->fa_deleted,
02561                     'oi_sha1' => $sha1 );
02562             }
02563 
02564             $deleteIds[] = $row->fa_id;
02565 
02566             if ( !$this->unsuppress && $row->fa_deleted & File::DELETED_FILE ) {
02567                 // private files can stay where they are
02568                 $status->successCount++;
02569             } else {
02570                 $storeBatch[] = array( $deletedUrl, 'public', $destRel );
02571                 $this->cleanupBatch[] = $row->fa_storage_key;
02572             }
02573 
02574             $first = false;
02575         }
02576 
02577         unset( $result );
02578 
02579         // Add a warning to the status object for missing IDs
02580         $missingIds = array_diff( $this->ids, $idsPresent );
02581 
02582         foreach ( $missingIds as $id ) {
02583             $status->error( 'undelete-missing-filearchive', $id );
02584         }
02585 
02586         // Remove missing files from batch, so we don't get errors when undeleting them
02587         $checkStatus = $this->removeNonexistentFiles( $storeBatch );
02588         if ( !$checkStatus->isGood() ) {
02589             $status->merge( $checkStatus );
02590             return $status;
02591         }
02592         $storeBatch = $checkStatus->value;
02593 
02594         // Run the store batch
02595         // Use the OVERWRITE_SAME flag to smooth over a common error
02596         $storeStatus = $this->file->repo->storeBatch( $storeBatch, FileRepo::OVERWRITE_SAME );
02597         $status->merge( $storeStatus );
02598 
02599         if ( !$status->isGood() ) {
02600             // Even if some files could be copied, fail entirely as that is the
02601             // easiest thing to do without data loss
02602             $this->cleanupFailedBatch( $storeStatus, $storeBatch );
02603             $status->ok = false;
02604             $this->file->unlock();
02605 
02606             return $status;
02607         }
02608 
02609         // Run the DB updates
02610         // Because we have locked the image row, key conflicts should be rare.
02611         // If they do occur, we can roll back the transaction at this time with
02612         // no data loss, but leaving unregistered files scattered throughout the
02613         // public zone.
02614         // This is not ideal, which is why it's important to lock the image row.
02615         if ( $insertCurrent ) {
02616             $dbw->insert( 'image', $insertCurrent, __METHOD__ );
02617         }
02618 
02619         if ( $insertBatch ) {
02620             $dbw->insert( 'oldimage', $insertBatch, __METHOD__ );
02621         }
02622 
02623         if ( $deleteIds ) {
02624             $dbw->delete( 'filearchive',
02625                 array( 'fa_id' => $deleteIds ),
02626                 __METHOD__ );
02627         }
02628 
02629         // If store batch is empty (all files are missing), deletion is to be considered successful
02630         if ( $status->successCount > 0 || !$storeBatch ) {
02631             if ( !$exists ) {
02632                 wfDebug( __METHOD__ . " restored {$status->successCount} items, creating a new current\n" );
02633 
02634                 DeferredUpdates::addUpdate( SiteStatsUpdate::factory( array( 'images' => 1 ) ) );
02635 
02636                 $this->file->purgeEverything();
02637             } else {
02638                 wfDebug( __METHOD__ . " restored {$status->successCount} as archived versions\n" );
02639                 $this->file->purgeDescription();
02640                 $this->file->purgeHistory();
02641             }
02642         }
02643 
02644         $this->file->unlock();
02645 
02646         return $status;
02647     }
02648 
02654     function removeNonexistentFiles( $triplets ) {
02655         $files = $filteredTriplets = array();
02656         foreach ( $triplets as $file ) {
02657             $files[$file[0]] = $file[0];
02658         }
02659 
02660         $result = $this->file->repo->fileExistsBatch( $files );
02661         if ( in_array( null, $result, true ) ) {
02662             return Status::newFatal( 'backend-fail-internal',
02663                 $this->file->repo->getBackend()->getName() );
02664         }
02665 
02666         foreach ( $triplets as $file ) {
02667             if ( $result[$file[0]] ) {
02668                 $filteredTriplets[] = $file;
02669             }
02670         }
02671 
02672         return Status::newGood( $filteredTriplets );
02673     }
02674 
02680     function removeNonexistentFromCleanup( $batch ) {
02681         $files = $newBatch = array();
02682         $repo = $this->file->repo;
02683 
02684         foreach ( $batch as $file ) {
02685             $files[$file] = $repo->getVirtualUrl( 'deleted' ) . '/' .
02686                 rawurlencode( $repo->getDeletedHashPath( $file ) . $file );
02687         }
02688 
02689         $result = $repo->fileExistsBatch( $files );
02690 
02691         foreach ( $batch as $file ) {
02692             if ( $result[$file] ) {
02693                 $newBatch[] = $file;
02694             }
02695         }
02696 
02697         return $newBatch;
02698     }
02699 
02705     function cleanup() {
02706         if ( !$this->cleanupBatch ) {
02707             return $this->file->repo->newGood();
02708         }
02709 
02710         $this->cleanupBatch = $this->removeNonexistentFromCleanup( $this->cleanupBatch );
02711 
02712         $status = $this->file->repo->cleanupDeletedBatch( $this->cleanupBatch );
02713 
02714         return $status;
02715     }
02716 
02724     function cleanupFailedBatch( $storeStatus, $storeBatch ) {
02725         $cleanupBatch = array();
02726 
02727         foreach ( $storeStatus->success as $i => $success ) {
02728             // Check if this item of the batch was successfully copied
02729             if ( $success ) {
02730                 // Item was successfully copied and needs to be removed again
02731                 // Extract ($dstZone, $dstRel) from the batch
02732                 $cleanupBatch[] = array( $storeBatch[$i][1], $storeBatch[$i][2] );
02733             }
02734         }
02735         $this->file->repo->cleanupBatch( $cleanupBatch );
02736     }
02737 }
02738 
02739 # ------------------------------------------------------------------------------
02740 
02745 class LocalFileMoveBatch {
02747     protected $file;
02748 
02750     protected $target;
02751 
02752     protected $cur;
02753 
02754     protected $olds;
02755 
02756     protected $oldCount;
02757 
02758     protected $archive;
02759 
02761     protected $db;
02762 
02767     function __construct( File $file, Title $target ) {
02768         $this->file = $file;
02769         $this->target = $target;
02770         $this->oldHash = $this->file->repo->getHashPath( $this->file->getName() );
02771         $this->newHash = $this->file->repo->getHashPath( $this->target->getDBkey() );
02772         $this->oldName = $this->file->getName();
02773         $this->newName = $this->file->repo->getNameFromTitle( $this->target );
02774         $this->oldRel = $this->oldHash . $this->oldName;
02775         $this->newRel = $this->newHash . $this->newName;
02776         $this->db = $file->getRepo()->getMasterDb();
02777     }
02778 
02782     function addCurrent() {
02783         $this->cur = array( $this->oldRel, $this->newRel );
02784     }
02785 
02790     function addOlds() {
02791         $archiveBase = 'archive';
02792         $this->olds = array();
02793         $this->oldCount = 0;
02794         $archiveNames = array();
02795 
02796         $result = $this->db->select( 'oldimage',
02797             array( 'oi_archive_name', 'oi_deleted' ),
02798             array( 'oi_name' => $this->oldName ),
02799             __METHOD__,
02800             array( 'FOR UPDATE' ) // ignore snapshot
02801         );
02802 
02803         foreach ( $result as $row ) {
02804             $archiveNames[] = $row->oi_archive_name;
02805             $oldName = $row->oi_archive_name;
02806             $bits = explode( '!', $oldName, 2 );
02807 
02808             if ( count( $bits ) != 2 ) {
02809                 wfDebug( "Old file name missing !: '$oldName' \n" );
02810                 continue;
02811             }
02812 
02813             list( $timestamp, $filename ) = $bits;
02814 
02815             if ( $this->oldName != $filename ) {
02816                 wfDebug( "Old file name doesn't match: '$oldName' \n" );
02817                 continue;
02818             }
02819 
02820             $this->oldCount++;
02821 
02822             // Do we want to add those to oldCount?
02823             if ( $row->oi_deleted & File::DELETED_FILE ) {
02824                 continue;
02825             }
02826 
02827             $this->olds[] = array(
02828                 "{$archiveBase}/{$this->oldHash}{$oldName}",
02829                 "{$archiveBase}/{$this->newHash}{$timestamp}!{$this->newName}"
02830             );
02831         }
02832 
02833         return $archiveNames;
02834     }
02835 
02840     function execute() {
02841         $repo = $this->file->repo;
02842         $status = $repo->newGood();
02843 
02844         $triplets = $this->getMoveTriplets();
02845         $checkStatus = $this->removeNonexistentFiles( $triplets );
02846         if ( !$checkStatus->isGood() ) {
02847             $status->merge( $checkStatus );
02848             return $status;
02849         }
02850         $triplets = $checkStatus->value;
02851         $destFile = wfLocalFile( $this->target );
02852 
02853         $this->file->lock(); // begin
02854         $destFile->lock(); // quickly fail if destination is not available
02855         // Rename the file versions metadata in the DB.
02856         // This implicitly locks the destination file, which avoids race conditions.
02857         // If we moved the files from A -> C before DB updates, another process could
02858         // move files from B -> C at this point, causing storeBatch() to fail and thus
02859         // cleanupTarget() to trigger. It would delete the C files and cause data loss.
02860         $statusDb = $this->doDBUpdates();
02861         if ( !$statusDb->isGood() ) {
02862             $destFile->unlock();
02863             $this->file->unlockAndRollback();
02864             $statusDb->ok = false;
02865 
02866             return $statusDb;
02867         }
02868         wfDebugLog( 'imagemove', "Renamed {$this->file->getName()} in database: " .
02869             "{$statusDb->successCount} successes, {$statusDb->failCount} failures" );
02870 
02871         // Copy the files into their new location.
02872         // If a prior process fataled copying or cleaning up files we tolerate any
02873         // of the existing files if they are identical to the ones being stored.
02874         $statusMove = $repo->storeBatch( $triplets, FileRepo::OVERWRITE_SAME );
02875         wfDebugLog( 'imagemove', "Moved files for {$this->file->getName()}: " .
02876             "{$statusMove->successCount} successes, {$statusMove->failCount} failures" );
02877         if ( !$statusMove->isGood() ) {
02878             // Delete any files copied over (while the destination is still locked)
02879             $this->cleanupTarget( $triplets );
02880             $destFile->unlock();
02881             $this->file->unlockAndRollback(); // unlocks the destination
02882             wfDebugLog( 'imagemove', "Error in moving files: " . $statusMove->getWikiText() );
02883             $statusMove->ok = false;
02884 
02885             return $statusMove;
02886         }
02887         $destFile->unlock();
02888         $this->file->unlock(); // done
02889 
02890         // Everything went ok, remove the source files
02891         $this->cleanupSource( $triplets );
02892 
02893         $status->merge( $statusDb );
02894         $status->merge( $statusMove );
02895 
02896         return $status;
02897     }
02898 
02905     function doDBUpdates() {
02906         $repo = $this->file->repo;
02907         $status = $repo->newGood();
02908         $dbw = $this->db;
02909 
02910         // Update current image
02911         $dbw->update(
02912             'image',
02913             array( 'img_name' => $this->newName ),
02914             array( 'img_name' => $this->oldName ),
02915             __METHOD__
02916         );
02917 
02918         if ( $dbw->affectedRows() ) {
02919             $status->successCount++;
02920         } else {
02921             $status->failCount++;
02922             $status->fatal( 'imageinvalidfilename' );
02923 
02924             return $status;
02925         }
02926 
02927         // Update old images
02928         $dbw->update(
02929             'oldimage',
02930             array(
02931                 'oi_name' => $this->newName,
02932                 'oi_archive_name = ' . $dbw->strreplace( 'oi_archive_name',
02933                     $dbw->addQuotes( $this->oldName ), $dbw->addQuotes( $this->newName ) ),
02934             ),
02935             array( 'oi_name' => $this->oldName ),
02936             __METHOD__
02937         );
02938 
02939         $affected = $dbw->affectedRows();
02940         $total = $this->oldCount;
02941         $status->successCount += $affected;
02942         // Bug 34934: $total is based on files that actually exist.
02943         // There may be more DB rows than such files, in which case $affected
02944         // can be greater than $total. We use max() to avoid negatives here.
02945         $status->failCount += max( 0, $total - $affected );
02946         if ( $status->failCount ) {
02947             $status->error( 'imageinvalidfilename' );
02948         }
02949 
02950         return $status;
02951     }
02952 
02957     function getMoveTriplets() {
02958         $moves = array_merge( array( $this->cur ), $this->olds );
02959         $triplets = array(); // The format is: (srcUrl, destZone, destUrl)
02960 
02961         foreach ( $moves as $move ) {
02962             // $move: (oldRelativePath, newRelativePath)
02963             $srcUrl = $this->file->repo->getVirtualUrl() . '/public/' . rawurlencode( $move[0] );
02964             $triplets[] = array( $srcUrl, 'public', $move[1] );
02965             wfDebugLog(
02966                 'imagemove',
02967                 "Generated move triplet for {$this->file->getName()}: {$srcUrl} :: public :: {$move[1]}"
02968             );
02969         }
02970 
02971         return $triplets;
02972     }
02973 
02979     function removeNonexistentFiles( $triplets ) {
02980         $files = array();
02981 
02982         foreach ( $triplets as $file ) {
02983             $files[$file[0]] = $file[0];
02984         }
02985 
02986         $result = $this->file->repo->fileExistsBatch( $files );
02987         if ( in_array( null, $result, true ) ) {
02988             return Status::newFatal( 'backend-fail-internal',
02989                 $this->file->repo->getBackend()->getName() );
02990         }
02991 
02992         $filteredTriplets = array();
02993         foreach ( $triplets as $file ) {
02994             if ( $result[$file[0]] ) {
02995                 $filteredTriplets[] = $file;
02996             } else {
02997                 wfDebugLog( 'imagemove', "File {$file[0]} does not exist" );
02998             }
02999         }
03000 
03001         return Status::newGood( $filteredTriplets );
03002     }
03003 
03009     function cleanupTarget( $triplets ) {
03010         // Create dest pairs from the triplets
03011         $pairs = array();
03012         foreach ( $triplets as $triplet ) {
03013             // $triplet: (old source virtual URL, dst zone, dest rel)
03014             $pairs[] = array( $triplet[1], $triplet[2] );
03015         }
03016 
03017         $this->file->repo->cleanupBatch( $pairs );
03018     }
03019 
03025     function cleanupSource( $triplets ) {
03026         // Create source file names from the triplets
03027         $files = array();
03028         foreach ( $triplets as $triplet ) {
03029             $files[] = $triplet[0];
03030         }
03031 
03032         $this->file->repo->cleanupBatch( $files );
03033     }
03034 }