MediaWiki  REL1_23
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 
00124     const LOAD_ALL = 1; // integer; load all the lazy fields too (like metadata)
00125 
00138     static function newFromTitle( $title, $repo, $unused = null ) {
00139         return new self( $title, $repo );
00140     }
00141 
00151     static function newFromRow( $row, $repo ) {
00152         $title = Title::makeTitle( NS_FILE, $row->img_name );
00153         $file = new self( $title, $repo );
00154         $file->loadFromRow( $row );
00155 
00156         return $file;
00157     }
00158 
00168     static function newFromKey( $sha1, $repo, $timestamp = false ) {
00169         $dbr = $repo->getSlaveDB();
00170 
00171         $conds = array( 'img_sha1' => $sha1 );
00172         if ( $timestamp ) {
00173             $conds['img_timestamp'] = $dbr->timestamp( $timestamp );
00174         }
00175 
00176         $row = $dbr->selectRow( 'image', self::selectFields(), $conds, __METHOD__ );
00177         if ( $row ) {
00178             return self::newFromRow( $row, $repo );
00179         } else {
00180             return false;
00181         }
00182     }
00183 
00188     static function selectFields() {
00189         return array(
00190             'img_name',
00191             'img_size',
00192             'img_width',
00193             'img_height',
00194             'img_metadata',
00195             'img_bits',
00196             'img_media_type',
00197             'img_major_mime',
00198             'img_minor_mime',
00199             'img_description',
00200             'img_user',
00201             'img_user_text',
00202             'img_timestamp',
00203             'img_sha1',
00204         );
00205     }
00206 
00211     function __construct( $title, $repo ) {
00212         parent::__construct( $title, $repo );
00213 
00214         $this->metadata = '';
00215         $this->historyLine = 0;
00216         $this->historyRes = null;
00217         $this->dataLoaded = false;
00218         $this->extraDataLoaded = false;
00219 
00220         $this->assertRepoDefined();
00221         $this->assertTitleDefined();
00222     }
00223 
00229     function getCacheKey() {
00230         $hashedName = md5( $this->getName() );
00231 
00232         return $this->repo->getSharedCacheKey( 'file', $hashedName );
00233     }
00234 
00239     function loadFromCache() {
00240         global $wgMemc;
00241 
00242         wfProfileIn( __METHOD__ );
00243         $this->dataLoaded = false;
00244         $this->extraDataLoaded = false;
00245         $key = $this->getCacheKey();
00246 
00247         if ( !$key ) {
00248             wfProfileOut( __METHOD__ );
00249 
00250             return false;
00251         }
00252 
00253         $cachedValues = $wgMemc->get( $key );
00254 
00255         // Check if the key existed and belongs to this version of MediaWiki
00256         if ( isset( $cachedValues['version'] ) && $cachedValues['version'] == MW_FILE_VERSION ) {
00257             wfDebug( "Pulling file metadata from cache key $key\n" );
00258             $this->fileExists = $cachedValues['fileExists'];
00259             if ( $this->fileExists ) {
00260                 $this->setProps( $cachedValues );
00261             }
00262             $this->dataLoaded = true;
00263             $this->extraDataLoaded = true;
00264             foreach ( $this->getLazyCacheFields( '' ) as $field ) {
00265                 $this->extraDataLoaded = $this->extraDataLoaded && isset( $cachedValues[$field] );
00266             }
00267         }
00268 
00269         if ( $this->dataLoaded ) {
00270             wfIncrStats( 'image_cache_hit' );
00271         } else {
00272             wfIncrStats( 'image_cache_miss' );
00273         }
00274 
00275         wfProfileOut( __METHOD__ );
00276 
00277         return $this->dataLoaded;
00278     }
00279 
00283     function saveToCache() {
00284         global $wgMemc;
00285 
00286         $this->load();
00287         $key = $this->getCacheKey();
00288 
00289         if ( !$key ) {
00290             return;
00291         }
00292 
00293         $fields = $this->getCacheFields( '' );
00294         $cache = array( 'version' => MW_FILE_VERSION );
00295         $cache['fileExists'] = $this->fileExists;
00296 
00297         if ( $this->fileExists ) {
00298             foreach ( $fields as $field ) {
00299                 $cache[$field] = $this->$field;
00300             }
00301         }
00302 
00303         // Strip off excessive entries from the subset of fields that can become large.
00304         // If the cache value gets to large it will not fit in memcached and nothing will
00305         // get cached at all, causing master queries for any file access.
00306         foreach ( $this->getLazyCacheFields( '' ) as $field ) {
00307             if ( isset( $cache[$field] ) && strlen( $cache[$field] ) > 100 * 1024 ) {
00308                 unset( $cache[$field] ); // don't let the value get too big
00309             }
00310         }
00311 
00312         // Cache presence for 1 week and negatives for 1 day
00313         $wgMemc->set( $key, $cache, $this->fileExists ? 86400 * 7 : 86400 );
00314     }
00315 
00319     function loadFromFile() {
00320         $props = $this->repo->getFileProps( $this->getVirtualUrl() );
00321         $this->setProps( $props );
00322     }
00323 
00328     function getCacheFields( $prefix = 'img_' ) {
00329         static $fields = array( 'size', 'width', 'height', 'bits', 'media_type',
00330             'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1', 'user',
00331             'user_text', 'description' );
00332         static $results = array();
00333 
00334         if ( $prefix == '' ) {
00335             return $fields;
00336         }
00337 
00338         if ( !isset( $results[$prefix] ) ) {
00339             $prefixedFields = array();
00340             foreach ( $fields as $field ) {
00341                 $prefixedFields[] = $prefix . $field;
00342             }
00343             $results[$prefix] = $prefixedFields;
00344         }
00345 
00346         return $results[$prefix];
00347     }
00348 
00353     function getLazyCacheFields( $prefix = 'img_' ) {
00354         static $fields = array( 'metadata' );
00355         static $results = array();
00356 
00357         if ( $prefix == '' ) {
00358             return $fields;
00359         }
00360 
00361         if ( !isset( $results[$prefix] ) ) {
00362             $prefixedFields = array();
00363             foreach ( $fields as $field ) {
00364                 $prefixedFields[] = $prefix . $field;
00365             }
00366             $results[$prefix] = $prefixedFields;
00367         }
00368 
00369         return $results[$prefix];
00370     }
00371 
00375     function loadFromDB() {
00376         # Polymorphic function name to distinguish foreign and local fetches
00377         $fname = get_class( $this ) . '::' . __FUNCTION__;
00378         wfProfileIn( $fname );
00379 
00380         # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
00381         $this->dataLoaded = true;
00382         $this->extraDataLoaded = true;
00383 
00384         $dbr = $this->repo->getMasterDB();
00385         $row = $dbr->selectRow( 'image', $this->getCacheFields( 'img_' ),
00386             array( 'img_name' => $this->getName() ), $fname );
00387 
00388         if ( $row ) {
00389             $this->loadFromRow( $row );
00390         } else {
00391             $this->fileExists = false;
00392         }
00393 
00394         wfProfileOut( $fname );
00395     }
00396 
00401     protected function loadExtraFromDB() {
00402         # Polymorphic function name to distinguish foreign and local fetches
00403         $fname = get_class( $this ) . '::' . __FUNCTION__;
00404         wfProfileIn( $fname );
00405 
00406         # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
00407         $this->extraDataLoaded = true;
00408 
00409         $dbr = $this->repo->getSlaveDB();
00410         // In theory the file could have just been renamed/deleted...oh well
00411         $row = $dbr->selectRow( 'image', $this->getLazyCacheFields( 'img_' ),
00412             array( 'img_name' => $this->getName() ), $fname );
00413 
00414         if ( !$row ) { // fallback to master
00415             $dbr = $this->repo->getMasterDB();
00416             $row = $dbr->selectRow( 'image', $this->getLazyCacheFields( 'img_' ),
00417                 array( 'img_name' => $this->getName() ), $fname );
00418         }
00419 
00420         if ( $row ) {
00421             foreach ( $this->unprefixRow( $row, 'img_' ) as $name => $value ) {
00422                 $this->$name = $value;
00423             }
00424         } else {
00425             wfProfileOut( $fname );
00426             throw new MWException( "Could not find data for image '{$this->getName()}'." );
00427         }
00428 
00429         wfProfileOut( $fname );
00430     }
00431 
00438     protected function unprefixRow( $row, $prefix = 'img_' ) {
00439         $array = (array)$row;
00440         $prefixLength = strlen( $prefix );
00441 
00442         // Sanity check prefix once
00443         if ( substr( key( $array ), 0, $prefixLength ) !== $prefix ) {
00444             throw new MWException( __METHOD__ . ': incorrect $prefix parameter' );
00445         }
00446 
00447         $decoded = array();
00448         foreach ( $array as $name => $value ) {
00449             $decoded[substr( $name, $prefixLength )] = $value;
00450         }
00451 
00452         return $decoded;
00453     }
00454 
00463     function decodeRow( $row, $prefix = 'img_' ) {
00464         $decoded = $this->unprefixRow( $row, $prefix );
00465 
00466         $decoded['timestamp'] = wfTimestamp( TS_MW, $decoded['timestamp'] );
00467 
00468         $decoded['metadata'] = $this->repo->getSlaveDB()->decodeBlob( $decoded['metadata'] );
00469 
00470         if ( empty( $decoded['major_mime'] ) ) {
00471             $decoded['mime'] = 'unknown/unknown';
00472         } else {
00473             if ( !$decoded['minor_mime'] ) {
00474                 $decoded['minor_mime'] = 'unknown';
00475             }
00476             $decoded['mime'] = $decoded['major_mime'] . '/' . $decoded['minor_mime'];
00477         }
00478 
00479         # Trim zero padding from char/binary field
00480         $decoded['sha1'] = rtrim( $decoded['sha1'], "\0" );
00481 
00482         return $decoded;
00483     }
00484 
00488     function loadFromRow( $row, $prefix = 'img_' ) {
00489         $this->dataLoaded = true;
00490         $this->extraDataLoaded = true;
00491 
00492         $array = $this->decodeRow( $row, $prefix );
00493 
00494         foreach ( $array as $name => $value ) {
00495             $this->$name = $value;
00496         }
00497 
00498         $this->fileExists = true;
00499         $this->maybeUpgradeRow();
00500     }
00501 
00506     function load( $flags = 0 ) {
00507         if ( !$this->dataLoaded ) {
00508             if ( !$this->loadFromCache() ) {
00509                 $this->loadFromDB();
00510                 $this->saveToCache();
00511             }
00512             $this->dataLoaded = true;
00513         }
00514         if ( ( $flags & self::LOAD_ALL ) && !$this->extraDataLoaded ) {
00515             $this->loadExtraFromDB();
00516         }
00517     }
00518 
00522     function maybeUpgradeRow() {
00523         global $wgUpdateCompatibleMetadata;
00524         if ( wfReadOnly() ) {
00525             return;
00526         }
00527 
00528         if ( is_null( $this->media_type ) ||
00529             $this->mime == 'image/svg'
00530         ) {
00531             $this->upgradeRow();
00532             $this->upgraded = true;
00533         } else {
00534             $handler = $this->getHandler();
00535             if ( $handler ) {
00536                 $validity = $handler->isMetadataValid( $this, $this->getMetadata() );
00537                 if ( $validity === MediaHandler::METADATA_BAD
00538                     || ( $validity === MediaHandler::METADATA_COMPATIBLE && $wgUpdateCompatibleMetadata )
00539                 ) {
00540                     $this->upgradeRow();
00541                     $this->upgraded = true;
00542                 }
00543             }
00544         }
00545     }
00546 
00547     function getUpgraded() {
00548         return $this->upgraded;
00549     }
00550 
00554     function upgradeRow() {
00555         wfProfileIn( __METHOD__ );
00556 
00557         $this->lock(); // begin
00558 
00559         $this->loadFromFile();
00560 
00561         # Don't destroy file info of missing files
00562         if ( !$this->fileExists ) {
00563             wfDebug( __METHOD__ . ": file does not exist, aborting\n" );
00564             wfProfileOut( __METHOD__ );
00565 
00566             return;
00567         }
00568 
00569         $dbw = $this->repo->getMasterDB();
00570         list( $major, $minor ) = self::splitMime( $this->mime );
00571 
00572         if ( wfReadOnly() ) {
00573             wfProfileOut( __METHOD__ );
00574 
00575             return;
00576         }
00577         wfDebug( __METHOD__ . ': upgrading ' . $this->getName() . " to the current schema\n" );
00578 
00579         $dbw->update( 'image',
00580             array(
00581                 'img_size' => $this->size, // sanity
00582                 'img_width' => $this->width,
00583                 'img_height' => $this->height,
00584                 'img_bits' => $this->bits,
00585                 'img_media_type' => $this->media_type,
00586                 'img_major_mime' => $major,
00587                 'img_minor_mime' => $minor,
00588                 'img_metadata' => $dbw->encodeBlob( $this->metadata ),
00589                 'img_sha1' => $this->sha1,
00590             ),
00591             array( 'img_name' => $this->getName() ),
00592             __METHOD__
00593         );
00594 
00595         $this->saveToCache();
00596 
00597         $this->unlock(); // done
00598 
00599         wfProfileOut( __METHOD__ );
00600     }
00601 
00610     function setProps( $info ) {
00611         $this->dataLoaded = true;
00612         $fields = $this->getCacheFields( '' );
00613         $fields[] = 'fileExists';
00614 
00615         foreach ( $fields as $field ) {
00616             if ( isset( $info[$field] ) ) {
00617                 $this->$field = $info[$field];
00618             }
00619         }
00620 
00621         // Fix up mime fields
00622         if ( isset( $info['major_mime'] ) ) {
00623             $this->mime = "{$info['major_mime']}/{$info['minor_mime']}";
00624         } elseif ( isset( $info['mime'] ) ) {
00625             $this->mime = $info['mime'];
00626             list( $this->major_mime, $this->minor_mime ) = self::splitMime( $this->mime );
00627         }
00628     }
00629 
00641     function isMissing() {
00642         if ( $this->missing === null ) {
00643             list( $fileExists ) = $this->repo->fileExists( $this->getVirtualUrl() );
00644             $this->missing = !$fileExists;
00645         }
00646 
00647         return $this->missing;
00648     }
00649 
00656     public function getWidth( $page = 1 ) {
00657         $this->load();
00658 
00659         if ( $this->isMultipage() ) {
00660             $handler = $this->getHandler();
00661             if ( !$handler ) {
00662                 return 0;
00663             }
00664             $dim = $handler->getPageDimensions( $this, $page );
00665             if ( $dim ) {
00666                 return $dim['width'];
00667             } else {
00668                 // For non-paged media, the false goes through an
00669                 // intval, turning failure into 0, so do same here.
00670                 return 0;
00671             }
00672         } else {
00673             return $this->width;
00674         }
00675     }
00676 
00683     public function getHeight( $page = 1 ) {
00684         $this->load();
00685 
00686         if ( $this->isMultipage() ) {
00687             $handler = $this->getHandler();
00688             if ( !$handler ) {
00689                 return 0;
00690             }
00691             $dim = $handler->getPageDimensions( $this, $page );
00692             if ( $dim ) {
00693                 return $dim['height'];
00694             } else {
00695                 // For non-paged media, the false goes through an
00696                 // intval, turning failure into 0, so do same here.
00697                 return 0;
00698             }
00699         } else {
00700             return $this->height;
00701         }
00702     }
00703 
00710     function getUser( $type = 'text' ) {
00711         $this->load();
00712 
00713         if ( $type == 'text' ) {
00714             return $this->user_text;
00715         } elseif ( $type == 'id' ) {
00716             return $this->user;
00717         }
00718     }
00719 
00724     function getMetadata() {
00725         $this->load( self::LOAD_ALL ); // large metadata is loaded in another step
00726         return $this->metadata;
00727     }
00728 
00732     function getBitDepth() {
00733         $this->load();
00734 
00735         return $this->bits;
00736     }
00737 
00742     public function getSize() {
00743         $this->load();
00744 
00745         return $this->size;
00746     }
00747 
00752     function getMimeType() {
00753         $this->load();
00754 
00755         return $this->mime;
00756     }
00757 
00763     function getMediaType() {
00764         $this->load();
00765 
00766         return $this->media_type;
00767     }
00768 
00779     public function exists() {
00780         $this->load();
00781 
00782         return $this->fileExists;
00783     }
00784 
00797     function migrateThumbFile( $thumbName ) {
00798         /* Old code for bug 2532
00799         $thumbDir = $this->getThumbPath();
00800         $thumbPath = "$thumbDir/$thumbName";
00801         if ( is_dir( $thumbPath ) ) {
00802             // Directory where file should be
00803             // This happened occasionally due to broken migration code in 1.5
00804             // Rename to broken-*
00805             for ( $i = 0; $i < 100; $i++ ) {
00806                 $broken = $this->repo->getZonePath( 'public' ) . "/broken-$i-$thumbName";
00807                 if ( !file_exists( $broken ) ) {
00808                     rename( $thumbPath, $broken );
00809                     break;
00810                 }
00811             }
00812             // Doesn't exist anymore
00813             clearstatcache();
00814         }
00815         */
00816         /*
00817         if ( $this->repo->fileExists( $thumbDir ) ) {
00818             // Delete file where directory should be
00819             $this->repo->cleanupBatch( array( $thumbDir ) );
00820         }
00821         */
00822     }
00823 
00833     function getThumbnails( $archiveName = false ) {
00834         if ( $archiveName ) {
00835             $dir = $this->getArchiveThumbPath( $archiveName );
00836         } else {
00837             $dir = $this->getThumbPath();
00838         }
00839 
00840         $backend = $this->repo->getBackend();
00841         $files = array( $dir );
00842         try {
00843             $iterator = $backend->getFileList( array( 'dir' => $dir ) );
00844             foreach ( $iterator as $file ) {
00845                 $files[] = $file;
00846             }
00847         } catch ( FileBackendError $e ) {
00848         } // suppress (bug 54674)
00849 
00850         return $files;
00851     }
00852 
00856     function purgeMetadataCache() {
00857         $this->loadFromDB();
00858         $this->saveToCache();
00859         $this->purgeHistory();
00860     }
00861 
00867     function purgeHistory() {
00868         global $wgMemc;
00869 
00870         $hashedName = md5( $this->getName() );
00871         $oldKey = $this->repo->getSharedCacheKey( 'oldfile', $hashedName );
00872 
00873         if ( $oldKey ) {
00874             $wgMemc->delete( $oldKey );
00875         }
00876     }
00877 
00885     function purgeCache( $options = array() ) {
00886         wfProfileIn( __METHOD__ );
00887         // Refresh metadata cache
00888         $this->purgeMetadataCache();
00889 
00890         // Delete thumbnails
00891         $this->purgeThumbnails( $options );
00892 
00893         // Purge squid cache for this file
00894         SquidUpdate::purge( array( $this->getURL() ) );
00895         wfProfileOut( __METHOD__ );
00896     }
00897 
00902     function purgeOldThumbnails( $archiveName ) {
00903         global $wgUseSquid;
00904         wfProfileIn( __METHOD__ );
00905 
00906         // Get a list of old thumbnails and URLs
00907         $files = $this->getThumbnails( $archiveName );
00908 
00909         // Purge any custom thumbnail caches
00910         wfRunHooks( 'LocalFilePurgeThumbnails', array( $this, $archiveName ) );
00911 
00912         $dir = array_shift( $files );
00913         $this->purgeThumbList( $dir, $files );
00914 
00915         // Purge the squid
00916         if ( $wgUseSquid ) {
00917             $urls = array();
00918             foreach ( $files as $file ) {
00919                 $urls[] = $this->getArchiveThumbUrl( $archiveName, $file );
00920             }
00921             SquidUpdate::purge( $urls );
00922         }
00923 
00924         wfProfileOut( __METHOD__ );
00925     }
00926 
00930     function purgeThumbnails( $options = array() ) {
00931         global $wgUseSquid;
00932         wfProfileIn( __METHOD__ );
00933 
00934         // Delete thumbnails
00935         $files = $this->getThumbnails();
00936         // Always purge all files from squid regardless of handler filters
00937         $urls = array();
00938         if ( $wgUseSquid ) {
00939             foreach ( $files as $file ) {
00940                 $urls[] = $this->getThumbUrl( $file );
00941             }
00942             array_shift( $urls ); // don't purge directory
00943         }
00944 
00945         // Give media handler a chance to filter the file purge list
00946         if ( !empty( $options['forThumbRefresh'] ) ) {
00947             $handler = $this->getHandler();
00948             if ( $handler ) {
00949                 $handler->filterThumbnailPurgeList( $files, $options );
00950             }
00951         }
00952 
00953         // Purge any custom thumbnail caches
00954         wfRunHooks( 'LocalFilePurgeThumbnails', array( $this, false ) );
00955 
00956         $dir = array_shift( $files );
00957         $this->purgeThumbList( $dir, $files );
00958 
00959         // Purge the squid
00960         if ( $wgUseSquid ) {
00961             SquidUpdate::purge( $urls );
00962         }
00963 
00964         wfProfileOut( __METHOD__ );
00965     }
00966 
00972     protected function purgeThumbList( $dir, $files ) {
00973         $fileListDebug = strtr(
00974             var_export( $files, true ),
00975             array( "\n" => '' )
00976         );
00977         wfDebug( __METHOD__ . ": $fileListDebug\n" );
00978 
00979         $purgeList = array();
00980         foreach ( $files as $file ) {
00981             # Check that the base file name is part of the thumb name
00982             # This is a basic sanity check to avoid erasing unrelated directories
00983             if ( strpos( $file, $this->getName() ) !== false
00984                 || strpos( $file, "-thumbnail" ) !== false // "short" thumb name
00985             ) {
00986                 $purgeList[] = "{$dir}/{$file}";
00987             }
00988         }
00989 
00990         # Delete the thumbnails
00991         $this->repo->quickPurgeBatch( $purgeList );
00992         # Clear out the thumbnail directory if empty
00993         $this->repo->quickCleanDir( $dir );
00994     }
00995 
01006     function getHistory( $limit = null, $start = null, $end = null, $inc = true ) {
01007         $dbr = $this->repo->getSlaveDB();
01008         $tables = array( 'oldimage' );
01009         $fields = OldLocalFile::selectFields();
01010         $conds = $opts = $join_conds = array();
01011         $eq = $inc ? '=' : '';
01012         $conds[] = "oi_name = " . $dbr->addQuotes( $this->title->getDBkey() );
01013 
01014         if ( $start ) {
01015             $conds[] = "oi_timestamp <$eq " . $dbr->addQuotes( $dbr->timestamp( $start ) );
01016         }
01017 
01018         if ( $end ) {
01019             $conds[] = "oi_timestamp >$eq " . $dbr->addQuotes( $dbr->timestamp( $end ) );
01020         }
01021 
01022         if ( $limit ) {
01023             $opts['LIMIT'] = $limit;
01024         }
01025 
01026         // Search backwards for time > x queries
01027         $order = ( !$start && $end !== null ) ? 'ASC' : 'DESC';
01028         $opts['ORDER BY'] = "oi_timestamp $order";
01029         $opts['USE INDEX'] = array( 'oldimage' => 'oi_name_timestamp' );
01030 
01031         wfRunHooks( 'LocalFile::getHistory', array( &$this, &$tables, &$fields,
01032             &$conds, &$opts, &$join_conds ) );
01033 
01034         $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $opts, $join_conds );
01035         $r = array();
01036 
01037         foreach ( $res as $row ) {
01038             $r[] = $this->repo->newFileFromRow( $row );
01039         }
01040 
01041         if ( $order == 'ASC' ) {
01042             $r = array_reverse( $r ); // make sure it ends up descending
01043         }
01044 
01045         return $r;
01046     }
01047 
01057     public function nextHistoryLine() {
01058         # Polymorphic function name to distinguish foreign and local fetches
01059         $fname = get_class( $this ) . '::' . __FUNCTION__;
01060 
01061         $dbr = $this->repo->getSlaveDB();
01062 
01063         if ( $this->historyLine == 0 ) { // called for the first time, return line from cur
01064             $this->historyRes = $dbr->select( 'image',
01065                 array(
01066                     '*',
01067                     "'' AS oi_archive_name",
01068                     '0 as oi_deleted',
01069                     'img_sha1'
01070                 ),
01071                 array( 'img_name' => $this->title->getDBkey() ),
01072                 $fname
01073             );
01074 
01075             if ( 0 == $dbr->numRows( $this->historyRes ) ) {
01076                 $this->historyRes = null;
01077 
01078                 return false;
01079             }
01080         } elseif ( $this->historyLine == 1 ) {
01081             $this->historyRes = $dbr->select( 'oldimage', '*',
01082                 array( 'oi_name' => $this->title->getDBkey() ),
01083                 $fname,
01084                 array( 'ORDER BY' => 'oi_timestamp DESC' )
01085             );
01086         }
01087         $this->historyLine++;
01088 
01089         return $dbr->fetchObject( $this->historyRes );
01090     }
01091 
01095     public function resetHistory() {
01096         $this->historyLine = 0;
01097 
01098         if ( !is_null( $this->historyRes ) ) {
01099             $this->historyRes = null;
01100         }
01101     }
01102 
01132     function upload( $srcPath, $comment, $pageText, $flags = 0, $props = false,
01133         $timestamp = false, $user = null
01134     ) {
01135         global $wgContLang;
01136 
01137         if ( $this->getRepo()->getReadOnlyReason() !== false ) {
01138             return $this->readOnlyFatalStatus();
01139         }
01140 
01141         if ( !$props ) {
01142             wfProfileIn( __METHOD__ . '-getProps' );
01143             if ( $this->repo->isVirtualUrl( $srcPath )
01144                 || FileBackend::isStoragePath( $srcPath )
01145             ) {
01146                 $props = $this->repo->getFileProps( $srcPath );
01147             } else {
01148                 $props = FSFile::getPropsFromPath( $srcPath );
01149             }
01150             wfProfileOut( __METHOD__ . '-getProps' );
01151         }
01152 
01153         $options = array();
01154         $handler = MediaHandler::getHandler( $props['mime'] );
01155         if ( $handler ) {
01156             $options['headers'] = $handler->getStreamHeaders( $props['metadata'] );
01157         } else {
01158             $options['headers'] = array();
01159         }
01160 
01161         // Trim spaces on user supplied text
01162         $comment = trim( $comment );
01163 
01164         // truncate nicely or the DB will do it for us
01165         // non-nicely (dangling multi-byte chars, non-truncated version in cache).
01166         $comment = $wgContLang->truncate( $comment, 255 );
01167         $this->lock(); // begin
01168         $status = $this->publish( $srcPath, $flags, $options );
01169 
01170         if ( $status->successCount > 0 ) {
01171             # Essentially we are displacing any existing current file and saving
01172             # a new current file at the old location. If just the first succeeded,
01173             # we still need to displace the current DB entry and put in a new one.
01174             if ( !$this->recordUpload2( $status->value, $comment, $pageText, $props, $timestamp, $user ) ) {
01175                 $status->fatal( 'filenotfound', $srcPath );
01176             }
01177         }
01178 
01179         $this->unlock(); // done
01180 
01181         return $status;
01182     }
01183 
01196     function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
01197         $watch = false, $timestamp = false, User $user = null ) {
01198         if ( !$user ) {
01199             global $wgUser;
01200             $user = $wgUser;
01201         }
01202 
01203         $pageText = SpecialUpload::getInitialPageText( $desc, $license, $copyStatus, $source );
01204 
01205         if ( !$this->recordUpload2( $oldver, $desc, $pageText, false, $timestamp, $user ) ) {
01206             return false;
01207         }
01208 
01209         if ( $watch ) {
01210             $user->addWatch( $this->getTitle() );
01211         }
01212 
01213         return true;
01214     }
01215 
01226     function recordUpload2( $oldver, $comment, $pageText, $props = false, $timestamp = false,
01227         $user = null
01228     ) {
01229         wfProfileIn( __METHOD__ );
01230 
01231         if ( is_null( $user ) ) {
01232             global $wgUser;
01233             $user = $wgUser;
01234         }
01235 
01236         $dbw = $this->repo->getMasterDB();
01237         $dbw->begin( __METHOD__ );
01238 
01239         if ( !$props ) {
01240             wfProfileIn( __METHOD__ . '-getProps' );
01241             $props = $this->repo->getFileProps( $this->getVirtualUrl() );
01242             wfProfileOut( __METHOD__ . '-getProps' );
01243         }
01244 
01245         if ( $timestamp === false ) {
01246             $timestamp = $dbw->timestamp();
01247         }
01248 
01249         $props['description'] = $comment;
01250         $props['user'] = $user->getId();
01251         $props['user_text'] = $user->getName();
01252         $props['timestamp'] = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
01253         $this->setProps( $props );
01254 
01255         # Fail now if the file isn't there
01256         if ( !$this->fileExists ) {
01257             wfDebug( __METHOD__ . ": File " . $this->getRel() . " went missing!\n" );
01258             wfProfileOut( __METHOD__ );
01259 
01260             return false;
01261         }
01262 
01263         $reupload = false;
01264 
01265         # Test to see if the row exists using INSERT IGNORE
01266         # This avoids race conditions by locking the row until the commit, and also
01267         # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
01268         $dbw->insert( 'image',
01269             array(
01270                 'img_name' => $this->getName(),
01271                 'img_size' => $this->size,
01272                 'img_width' => intval( $this->width ),
01273                 'img_height' => intval( $this->height ),
01274                 'img_bits' => $this->bits,
01275                 'img_media_type' => $this->media_type,
01276                 'img_major_mime' => $this->major_mime,
01277                 'img_minor_mime' => $this->minor_mime,
01278                 'img_timestamp' => $timestamp,
01279                 'img_description' => $comment,
01280                 'img_user' => $user->getId(),
01281                 'img_user_text' => $user->getName(),
01282                 'img_metadata' => $dbw->encodeBlob( $this->metadata ),
01283                 'img_sha1' => $this->sha1
01284             ),
01285             __METHOD__,
01286             'IGNORE'
01287         );
01288         if ( $dbw->affectedRows() == 0 ) {
01289             # (bug 34993) Note: $oldver can be empty here, if the previous
01290             # version of the file was broken. Allow registration of the new
01291             # version to continue anyway, because that's better than having
01292             # an image that's not fixable by user operations.
01293 
01294             $reupload = true;
01295             # Collision, this is an update of a file
01296             # Insert previous contents into oldimage
01297             $dbw->insertSelect( 'oldimage', 'image',
01298                 array(
01299                     'oi_name' => 'img_name',
01300                     'oi_archive_name' => $dbw->addQuotes( $oldver ),
01301                     'oi_size' => 'img_size',
01302                     'oi_width' => 'img_width',
01303                     'oi_height' => 'img_height',
01304                     'oi_bits' => 'img_bits',
01305                     'oi_timestamp' => 'img_timestamp',
01306                     'oi_description' => 'img_description',
01307                     'oi_user' => 'img_user',
01308                     'oi_user_text' => 'img_user_text',
01309                     'oi_metadata' => 'img_metadata',
01310                     'oi_media_type' => 'img_media_type',
01311                     'oi_major_mime' => 'img_major_mime',
01312                     'oi_minor_mime' => 'img_minor_mime',
01313                     'oi_sha1' => 'img_sha1'
01314                 ),
01315                 array( 'img_name' => $this->getName() ),
01316                 __METHOD__
01317             );
01318 
01319             # Update the current image row
01320             $dbw->update( 'image',
01321                 array( /* SET */
01322                     'img_size' => $this->size,
01323                     'img_width' => intval( $this->width ),
01324                     'img_height' => intval( $this->height ),
01325                     'img_bits' => $this->bits,
01326                     'img_media_type' => $this->media_type,
01327                     'img_major_mime' => $this->major_mime,
01328                     'img_minor_mime' => $this->minor_mime,
01329                     'img_timestamp' => $timestamp,
01330                     'img_description' => $comment,
01331                     'img_user' => $user->getId(),
01332                     'img_user_text' => $user->getName(),
01333                     'img_metadata' => $dbw->encodeBlob( $this->metadata ),
01334                     'img_sha1' => $this->sha1
01335                 ),
01336                 array( 'img_name' => $this->getName() ),
01337                 __METHOD__
01338             );
01339         } else {
01340             # This is a new file, so update the image count
01341             DeferredUpdates::addUpdate( SiteStatsUpdate::factory( array( 'images' => 1 ) ) );
01342         }
01343 
01344         $descTitle = $this->getTitle();
01345         $wikiPage = new WikiFilePage( $descTitle );
01346         $wikiPage->setFile( $this );
01347 
01348         # Add the log entry
01349         $action = $reupload ? 'overwrite' : 'upload';
01350 
01351         $logEntry = new ManualLogEntry( 'upload', $action );
01352         $logEntry->setPerformer( $user );
01353         $logEntry->setComment( $comment );
01354         $logEntry->setTarget( $descTitle );
01355 
01356         // Allow people using the api to associate log entries with the upload.
01357         // Log has a timestamp, but sometimes different from upload timestamp.
01358         $logEntry->setParameters(
01359             array(
01360                 'img_sha1' => $this->sha1,
01361                 'img_timestamp' => $timestamp,
01362             )
01363         );
01364         // Note we keep $logId around since during new image
01365         // creation, page doesn't exist yet, so log_page = 0
01366         // but we want it to point to the page we're making,
01367         // so we later modify the log entry.
01368         // For a similar reason, we avoid making an RC entry
01369         // now and wait until the page exists.
01370         $logId = $logEntry->insert();
01371 
01372         $exists = $descTitle->exists();
01373         if ( $exists ) {
01374             // Page exists, do RC entry now (otherwise we wait for later).
01375             $logEntry->publish( $logId );
01376         }
01377         wfProfileIn( __METHOD__ . '-edit' );
01378 
01379         if ( $exists ) {
01380             # Create a null revision
01381             $latest = $descTitle->getLatestRevID();
01382             $editSummary = LogFormatter::newFromEntry( $logEntry )->getPlainActionText();
01383 
01384             $nullRevision = Revision::newNullRevision(
01385                 $dbw,
01386                 $descTitle->getArticleID(),
01387                 $editSummary,
01388                 false
01389             );
01390             if ( !is_null( $nullRevision ) ) {
01391                 $nullRevision->insertOn( $dbw );
01392 
01393                 wfRunHooks( 'NewRevisionFromEditComplete', array( $wikiPage, $nullRevision, $latest, $user ) );
01394                 $wikiPage->updateRevisionOn( $dbw, $nullRevision );
01395             }
01396         }
01397 
01398         # Commit the transaction now, in case something goes wrong later
01399         # The most important thing is that files don't get lost, especially archives
01400         # NOTE: once we have support for nested transactions, the commit may be moved
01401         #       to after $wikiPage->doEdit has been called.
01402         $dbw->commit( __METHOD__ );
01403 
01404         # Save to memcache.
01405         # We shall not saveToCache before the commit since otherwise
01406         # in case of a rollback there is an usable file from memcached
01407         # which in fact doesn't really exist (bug 24978)
01408         $this->saveToCache();
01409 
01410         if ( $exists ) {
01411             # Invalidate the cache for the description page
01412             $descTitle->invalidateCache();
01413             $descTitle->purgeSquid();
01414         } else {
01415             # New file; create the description page.
01416             # There's already a log entry, so don't make a second RC entry
01417             # Squid and file cache for the description page are purged by doEditContent.
01418             $content = ContentHandler::makeContent( $pageText, $descTitle );
01419             $status = $wikiPage->doEditContent(
01420                 $content,
01421                 $comment,
01422                 EDIT_NEW | EDIT_SUPPRESS_RC,
01423                 false,
01424                 $user
01425             );
01426 
01427             $dbw->begin( __METHOD__ ); // XXX; doEdit() uses a transaction
01428             // Now that the page exists, make an RC entry.
01429             $logEntry->publish( $logId );
01430             if ( isset( $status->value['revision'] ) ) {
01431                 $dbw->update( 'logging',
01432                     array( 'log_page' => $status->value['revision']->getPage() ),
01433                     array( 'log_id' => $logId ),
01434                     __METHOD__
01435                 );
01436             }
01437             $dbw->commit( __METHOD__ ); // commit before anything bad can happen
01438         }
01439 
01440         wfProfileOut( __METHOD__ . '-edit' );
01441 
01442 
01443         if ( $reupload ) {
01444             # Delete old thumbnails
01445             wfProfileIn( __METHOD__ . '-purge' );
01446             $this->purgeThumbnails();
01447             wfProfileOut( __METHOD__ . '-purge' );
01448 
01449             # Remove the old file from the squid cache
01450             SquidUpdate::purge( array( $this->getURL() ) );
01451         }
01452 
01453         # Hooks, hooks, the magic of hooks...
01454         wfProfileIn( __METHOD__ . '-hooks' );
01455         wfRunHooks( 'FileUpload', array( $this, $reupload, $descTitle->exists() ) );
01456         wfProfileOut( __METHOD__ . '-hooks' );
01457 
01458         # Invalidate cache for all pages using this file
01459         $update = new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' );
01460         $update->doUpdate();
01461         if ( !$reupload ) {
01462             LinksUpdate::queueRecursiveJobsForTable( $this->getTitle(), 'imagelinks' );
01463         }
01464 
01465         wfProfileOut( __METHOD__ );
01466 
01467         return true;
01468     }
01469 
01485     function publish( $srcPath, $flags = 0, array $options = array() ) {
01486         return $this->publishTo( $srcPath, $this->getRel(), $flags, $options );
01487     }
01488 
01504     function publishTo( $srcPath, $dstRel, $flags = 0, array $options = array() ) {
01505         if ( $this->getRepo()->getReadOnlyReason() !== false ) {
01506             return $this->readOnlyFatalStatus();
01507         }
01508 
01509         $this->lock(); // begin
01510 
01511         $archiveName = wfTimestamp( TS_MW ) . '!' . $this->getName();
01512         $archiveRel = 'archive/' . $this->getHashPath() . $archiveName;
01513         $flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0;
01514         $status = $this->repo->publish( $srcPath, $dstRel, $archiveRel, $flags, $options );
01515 
01516         if ( $status->value == 'new' ) {
01517             $status->value = '';
01518         } else {
01519             $status->value = $archiveName;
01520         }
01521 
01522         $this->unlock(); // done
01523 
01524         return $status;
01525     }
01526 
01544     function move( $target ) {
01545         if ( $this->getRepo()->getReadOnlyReason() !== false ) {
01546             return $this->readOnlyFatalStatus();
01547         }
01548 
01549         wfDebugLog( 'imagemove', "Got request to move {$this->name} to " . $target->getText() );
01550         $batch = new LocalFileMoveBatch( $this, $target );
01551 
01552         $this->lock(); // begin
01553         $batch->addCurrent();
01554         $archiveNames = $batch->addOlds();
01555         $status = $batch->execute();
01556         $this->unlock(); // done
01557 
01558         wfDebugLog( 'imagemove', "Finished moving {$this->name}" );
01559 
01560         // Purge the source and target files...
01561         $oldTitleFile = wfLocalFile( $this->title );
01562         $newTitleFile = wfLocalFile( $target );
01563         // Hack: the lock()/unlock() pair is nested in a transaction so the locking is not
01564         // tied to BEGIN/COMMIT. To avoid slow purges in the transaction, move them outside.
01565         $this->getRepo()->getMasterDB()->onTransactionIdle(
01566             function () use ( $oldTitleFile, $newTitleFile, $archiveNames ) {
01567                 $oldTitleFile->purgeEverything();
01568                 foreach ( $archiveNames as $archiveName ) {
01569                     $oldTitleFile->purgeOldThumbnails( $archiveName );
01570                 }
01571                 $newTitleFile->purgeEverything();
01572             }
01573         );
01574 
01575         if ( $status->isOK() ) {
01576             // Now switch the object
01577             $this->title = $target;
01578             // Force regeneration of the name and hashpath
01579             unset( $this->name );
01580             unset( $this->hashPath );
01581         }
01582 
01583         return $status;
01584     }
01585 
01598     function delete( $reason, $suppress = false ) {
01599         if ( $this->getRepo()->getReadOnlyReason() !== false ) {
01600             return $this->readOnlyFatalStatus();
01601         }
01602 
01603         $batch = new LocalFileDeleteBatch( $this, $reason, $suppress );
01604 
01605         $this->lock(); // begin
01606         $batch->addCurrent();
01607         # Get old version relative paths
01608         $archiveNames = $batch->addOlds();
01609         $status = $batch->execute();
01610         $this->unlock(); // done
01611 
01612         if ( $status->isOK() ) {
01613             DeferredUpdates::addUpdate( SiteStatsUpdate::factory( array( 'images' => -1 ) ) );
01614         }
01615 
01616         // Hack: the lock()/unlock() pair is nested in a transaction so the locking is not
01617         // tied to BEGIN/COMMIT. To avoid slow purges in the transaction, move them outside.
01618         $file = $this;
01619         $this->getRepo()->getMasterDB()->onTransactionIdle(
01620             function () use ( $file, $archiveNames ) {
01621                 global $wgUseSquid;
01622 
01623                 $file->purgeEverything();
01624                 foreach ( $archiveNames as $archiveName ) {
01625                     $file->purgeOldThumbnails( $archiveName );
01626                 }
01627 
01628                 if ( $wgUseSquid ) {
01629                     // Purge the squid
01630                     $purgeUrls = array();
01631                     foreach ( $archiveNames as $archiveName ) {
01632                         $purgeUrls[] = $file->getArchiveUrl( $archiveName );
01633                     }
01634                     SquidUpdate::purge( $purgeUrls );
01635                 }
01636             }
01637         );
01638 
01639         return $status;
01640     }
01641 
01656     function deleteOld( $archiveName, $reason, $suppress = false ) {
01657         global $wgUseSquid;
01658         if ( $this->getRepo()->getReadOnlyReason() !== false ) {
01659             return $this->readOnlyFatalStatus();
01660         }
01661 
01662         $batch = new LocalFileDeleteBatch( $this, $reason, $suppress );
01663 
01664         $this->lock(); // begin
01665         $batch->addOld( $archiveName );
01666         $status = $batch->execute();
01667         $this->unlock(); // done
01668 
01669         $this->purgeOldThumbnails( $archiveName );
01670         if ( $status->isOK() ) {
01671             $this->purgeDescription();
01672             $this->purgeHistory();
01673         }
01674 
01675         if ( $wgUseSquid ) {
01676             // Purge the squid
01677             SquidUpdate::purge( array( $this->getArchiveUrl( $archiveName ) ) );
01678         }
01679 
01680         return $status;
01681     }
01682 
01694     function restore( $versions = array(), $unsuppress = false ) {
01695         if ( $this->getRepo()->getReadOnlyReason() !== false ) {
01696             return $this->readOnlyFatalStatus();
01697         }
01698 
01699         $batch = new LocalFileRestoreBatch( $this, $unsuppress );
01700 
01701         $this->lock(); // begin
01702         if ( !$versions ) {
01703             $batch->addAll();
01704         } else {
01705             $batch->addIds( $versions );
01706         }
01707         $status = $batch->execute();
01708         if ( $status->isGood() ) {
01709             $cleanupStatus = $batch->cleanup();
01710             $cleanupStatus->successCount = 0;
01711             $cleanupStatus->failCount = 0;
01712             $status->merge( $cleanupStatus );
01713         }
01714         $this->unlock(); // done
01715 
01716         return $status;
01717     }
01718 
01728     function getDescriptionUrl() {
01729         return $this->title->getLocalURL();
01730     }
01731 
01740     function getDescriptionText( $lang = null ) {
01741         $revision = Revision::newFromTitle( $this->title, false, Revision::READ_NORMAL );
01742         if ( !$revision ) {
01743             return false;
01744         }
01745         $content = $revision->getContent();
01746         if ( !$content ) {
01747             return false;
01748         }
01749         $pout = $content->getParserOutput( $this->title, null, new ParserOptions( null, $lang ) );
01750 
01751         return $pout->getText();
01752     }
01753 
01759     function getDescription( $audience = self::FOR_PUBLIC, User $user = null ) {
01760         $this->load();
01761         if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
01762             return '';
01763         } elseif ( $audience == self::FOR_THIS_USER
01764             && !$this->userCan( self::DELETED_COMMENT, $user )
01765         ) {
01766             return '';
01767         } else {
01768             return $this->description;
01769         }
01770     }
01771 
01775     function getTimestamp() {
01776         $this->load();
01777 
01778         return $this->timestamp;
01779     }
01780 
01784     function getSha1() {
01785         $this->load();
01786         // Initialise now if necessary
01787         if ( $this->sha1 == '' && $this->fileExists ) {
01788             $this->lock(); // begin
01789 
01790             $this->sha1 = $this->repo->getFileSha1( $this->getPath() );
01791             if ( !wfReadOnly() && strval( $this->sha1 ) != '' ) {
01792                 $dbw = $this->repo->getMasterDB();
01793                 $dbw->update( 'image',
01794                     array( 'img_sha1' => $this->sha1 ),
01795                     array( 'img_name' => $this->getName() ),
01796                     __METHOD__ );
01797                 $this->saveToCache();
01798             }
01799 
01800             $this->unlock(); // done
01801         }
01802 
01803         return $this->sha1;
01804     }
01805 
01809     function isCacheable() {
01810         $this->load();
01811 
01812         // If extra data (metadata) was not loaded then it must have been large
01813         return $this->extraDataLoaded
01814         && strlen( serialize( $this->metadata ) ) <= self::CACHE_FIELD_MAX_LEN;
01815     }
01816 
01823     function lock() {
01824         $dbw = $this->repo->getMasterDB();
01825 
01826         if ( !$this->locked ) {
01827             if ( !$dbw->trxLevel() ) {
01828                 $dbw->begin( __METHOD__ );
01829                 $this->lockedOwnTrx = true;
01830             }
01831             $this->locked++;
01832             // Bug 54736: use simple lock to handle when the file does not exist.
01833             // SELECT FOR UPDATE only locks records not the gaps where there are none.
01834             $cache = wfGetMainCache();
01835             $key = $this->getCacheKey();
01836             if ( !$cache->lock( $key, 5 ) ) {
01837                 throw new MWException( "Could not acquire lock for '{$this->getName()}.'" );
01838             }
01839             $dbw->onTransactionIdle( function () use ( $cache, $key ) {
01840                 $cache->unlock( $key ); // release on commit
01841             } );
01842         }
01843 
01844         return $dbw->selectField( 'image', '1',
01845             array( 'img_name' => $this->getName() ), __METHOD__, array( 'FOR UPDATE' ) );
01846     }
01847 
01852     function unlock() {
01853         if ( $this->locked ) {
01854             --$this->locked;
01855             if ( !$this->locked && $this->lockedOwnTrx ) {
01856                 $dbw = $this->repo->getMasterDB();
01857                 $dbw->commit( __METHOD__ );
01858                 $this->lockedOwnTrx = false;
01859             }
01860         }
01861     }
01862 
01866     function unlockAndRollback() {
01867         $this->locked = false;
01868         $dbw = $this->repo->getMasterDB();
01869         $dbw->rollback( __METHOD__ );
01870         $this->lockedOwnTrx = false;
01871     }
01872 
01876     protected function readOnlyFatalStatus() {
01877         return $this->getRepo()->newFatal( 'filereadonlyerror', $this->getName(),
01878             $this->getRepo()->getName(), $this->getRepo()->getReadOnlyReason() );
01879     }
01880 
01884     function __destruct() {
01885         $this->unlock();
01886     }
01887 } // LocalFile class
01888 
01889 # ------------------------------------------------------------------------------
01890 
01895 class LocalFileDeleteBatch {
01897     private $file;
01898 
01900     private $reason;
01901 
01903     private $srcRels = array();
01904 
01906     private $archiveUrls = array();
01907 
01909     private $deletionBatch;
01910 
01912     private $suppress;
01913 
01915     private $status;
01916 
01922     function __construct( File $file, $reason = '', $suppress = false ) {
01923         $this->file = $file;
01924         $this->reason = $reason;
01925         $this->suppress = $suppress;
01926         $this->status = $file->repo->newGood();
01927     }
01928 
01929     function addCurrent() {
01930         $this->srcRels['.'] = $this->file->getRel();
01931     }
01932 
01936     function addOld( $oldName ) {
01937         $this->srcRels[$oldName] = $this->file->getArchiveRel( $oldName );
01938         $this->archiveUrls[] = $this->file->getArchiveUrl( $oldName );
01939     }
01940 
01945     function addOlds() {
01946         $archiveNames = array();
01947 
01948         $dbw = $this->file->repo->getMasterDB();
01949         $result = $dbw->select( 'oldimage',
01950             array( 'oi_archive_name' ),
01951             array( 'oi_name' => $this->file->getName() ),
01952             __METHOD__
01953         );
01954 
01955         foreach ( $result as $row ) {
01956             $this->addOld( $row->oi_archive_name );
01957             $archiveNames[] = $row->oi_archive_name;
01958         }
01959 
01960         return $archiveNames;
01961     }
01962 
01966     function getOldRels() {
01967         if ( !isset( $this->srcRels['.'] ) ) {
01968             $oldRels =& $this->srcRels;
01969             $deleteCurrent = false;
01970         } else {
01971             $oldRels = $this->srcRels;
01972             unset( $oldRels['.'] );
01973             $deleteCurrent = true;
01974         }
01975 
01976         return array( $oldRels, $deleteCurrent );
01977     }
01978 
01982     protected function getHashes() {
01983         $hashes = array();
01984         list( $oldRels, $deleteCurrent ) = $this->getOldRels();
01985 
01986         if ( $deleteCurrent ) {
01987             $hashes['.'] = $this->file->getSha1();
01988         }
01989 
01990         if ( count( $oldRels ) ) {
01991             $dbw = $this->file->repo->getMasterDB();
01992             $res = $dbw->select(
01993                 'oldimage',
01994                 array( 'oi_archive_name', 'oi_sha1' ),
01995                 array( 'oi_archive_name' => array_keys( $oldRels ) ),
01996                 __METHOD__
01997             );
01998 
01999             foreach ( $res as $row ) {
02000                 if ( rtrim( $row->oi_sha1, "\0" ) === '' ) {
02001                     // Get the hash from the file
02002                     $oldUrl = $this->file->getArchiveVirtualUrl( $row->oi_archive_name );
02003                     $props = $this->file->repo->getFileProps( $oldUrl );
02004 
02005                     if ( $props['fileExists'] ) {
02006                         // Upgrade the oldimage row
02007                         $dbw->update( 'oldimage',
02008                             array( 'oi_sha1' => $props['sha1'] ),
02009                             array( 'oi_name' => $this->file->getName(), 'oi_archive_name' => $row->oi_archive_name ),
02010                             __METHOD__ );
02011                         $hashes[$row->oi_archive_name] = $props['sha1'];
02012                     } else {
02013                         $hashes[$row->oi_archive_name] = false;
02014                     }
02015                 } else {
02016                     $hashes[$row->oi_archive_name] = $row->oi_sha1;
02017                 }
02018             }
02019         }
02020 
02021         $missing = array_diff_key( $this->srcRels, $hashes );
02022 
02023         foreach ( $missing as $name => $rel ) {
02024             $this->status->error( 'filedelete-old-unregistered', $name );
02025         }
02026 
02027         foreach ( $hashes as $name => $hash ) {
02028             if ( !$hash ) {
02029                 $this->status->error( 'filedelete-missing', $this->srcRels[$name] );
02030                 unset( $hashes[$name] );
02031             }
02032         }
02033 
02034         return $hashes;
02035     }
02036 
02037     function doDBInserts() {
02038         global $wgUser;
02039 
02040         $dbw = $this->file->repo->getMasterDB();
02041         $encTimestamp = $dbw->addQuotes( $dbw->timestamp() );
02042         $encUserId = $dbw->addQuotes( $wgUser->getId() );
02043         $encReason = $dbw->addQuotes( $this->reason );
02044         $encGroup = $dbw->addQuotes( 'deleted' );
02045         $ext = $this->file->getExtension();
02046         $dotExt = $ext === '' ? '' : ".$ext";
02047         $encExt = $dbw->addQuotes( $dotExt );
02048         list( $oldRels, $deleteCurrent ) = $this->getOldRels();
02049 
02050         // Bitfields to further suppress the content
02051         if ( $this->suppress ) {
02052             $bitfield = 0;
02053             // This should be 15...
02054             $bitfield |= Revision::DELETED_TEXT;
02055             $bitfield |= Revision::DELETED_COMMENT;
02056             $bitfield |= Revision::DELETED_USER;
02057             $bitfield |= Revision::DELETED_RESTRICTED;
02058         } else {
02059             $bitfield = 'oi_deleted';
02060         }
02061 
02062         if ( $deleteCurrent ) {
02063             $concat = $dbw->buildConcat( array( "img_sha1", $encExt ) );
02064             $where = array( 'img_name' => $this->file->getName() );
02065             $dbw->insertSelect( 'filearchive', 'image',
02066                 array(
02067                     'fa_storage_group' => $encGroup,
02068                     'fa_storage_key' => "CASE WHEN img_sha1='' THEN '' ELSE $concat END",
02069                     'fa_deleted_user' => $encUserId,
02070                     'fa_deleted_timestamp' => $encTimestamp,
02071                     'fa_deleted_reason' => $encReason,
02072                     'fa_deleted' => $this->suppress ? $bitfield : 0,
02073 
02074                     'fa_name' => 'img_name',
02075                     'fa_archive_name' => 'NULL',
02076                     'fa_size' => 'img_size',
02077                     'fa_width' => 'img_width',
02078                     'fa_height' => 'img_height',
02079                     'fa_metadata' => 'img_metadata',
02080                     'fa_bits' => 'img_bits',
02081                     'fa_media_type' => 'img_media_type',
02082                     'fa_major_mime' => 'img_major_mime',
02083                     'fa_minor_mime' => 'img_minor_mime',
02084                     'fa_description' => 'img_description',
02085                     'fa_user' => 'img_user',
02086                     'fa_user_text' => 'img_user_text',
02087                     'fa_timestamp' => 'img_timestamp',
02088                     'fa_sha1' => 'img_sha1',
02089                 ), $where, __METHOD__ );
02090         }
02091 
02092         if ( count( $oldRels ) ) {
02093             $concat = $dbw->buildConcat( array( "oi_sha1", $encExt ) );
02094             $where = array(
02095                 'oi_name' => $this->file->getName(),
02096                 'oi_archive_name' => array_keys( $oldRels ) );
02097             $dbw->insertSelect( 'filearchive', 'oldimage',
02098                 array(
02099                     'fa_storage_group' => $encGroup,
02100                     'fa_storage_key' => "CASE WHEN oi_sha1='' THEN '' ELSE $concat END",
02101                     'fa_deleted_user' => $encUserId,
02102                     'fa_deleted_timestamp' => $encTimestamp,
02103                     'fa_deleted_reason' => $encReason,
02104                     'fa_deleted' => $this->suppress ? $bitfield : 'oi_deleted',
02105 
02106                     'fa_name' => 'oi_name',
02107                     'fa_archive_name' => 'oi_archive_name',
02108                     'fa_size' => 'oi_size',
02109                     'fa_width' => 'oi_width',
02110                     'fa_height' => 'oi_height',
02111                     'fa_metadata' => 'oi_metadata',
02112                     'fa_bits' => 'oi_bits',
02113                     'fa_media_type' => 'oi_media_type',
02114                     'fa_major_mime' => 'oi_major_mime',
02115                     'fa_minor_mime' => 'oi_minor_mime',
02116                     'fa_description' => 'oi_description',
02117                     'fa_user' => 'oi_user',
02118                     'fa_user_text' => 'oi_user_text',
02119                     'fa_timestamp' => 'oi_timestamp',
02120                     'fa_sha1' => 'oi_sha1',
02121                 ), $where, __METHOD__ );
02122         }
02123     }
02124 
02125     function doDBDeletes() {
02126         $dbw = $this->file->repo->getMasterDB();
02127         list( $oldRels, $deleteCurrent ) = $this->getOldRels();
02128 
02129         if ( count( $oldRels ) ) {
02130             $dbw->delete( 'oldimage',
02131                 array(
02132                     'oi_name' => $this->file->getName(),
02133                     'oi_archive_name' => array_keys( $oldRels )
02134                 ), __METHOD__ );
02135         }
02136 
02137         if ( $deleteCurrent ) {
02138             $dbw->delete( 'image', array( 'img_name' => $this->file->getName() ), __METHOD__ );
02139         }
02140     }
02141 
02146     function execute() {
02147         wfProfileIn( __METHOD__ );
02148 
02149         $this->file->lock();
02150         // Leave private files alone
02151         $privateFiles = array();
02152         list( $oldRels, ) = $this->getOldRels();
02153         $dbw = $this->file->repo->getMasterDB();
02154 
02155         if ( !empty( $oldRels ) ) {
02156             $res = $dbw->select( 'oldimage',
02157                 array( 'oi_archive_name' ),
02158                 array( 'oi_name' => $this->file->getName(),
02159                     'oi_archive_name' => array_keys( $oldRels ),
02160                     $dbw->bitAnd( 'oi_deleted', File::DELETED_FILE ) => File::DELETED_FILE ),
02161                 __METHOD__ );
02162 
02163             foreach ( $res as $row ) {
02164                 $privateFiles[$row->oi_archive_name] = 1;
02165             }
02166         }
02167         // Prepare deletion batch
02168         $hashes = $this->getHashes();
02169         $this->deletionBatch = array();
02170         $ext = $this->file->getExtension();
02171         $dotExt = $ext === '' ? '' : ".$ext";
02172 
02173         foreach ( $this->srcRels as $name => $srcRel ) {
02174             // Skip files that have no hash (missing source).
02175             // Keep private files where they are.
02176             if ( isset( $hashes[$name] ) && !array_key_exists( $name, $privateFiles ) ) {
02177                 $hash = $hashes[$name];
02178                 $key = $hash . $dotExt;
02179                 $dstRel = $this->file->repo->getDeletedHashPath( $key ) . $key;
02180                 $this->deletionBatch[$name] = array( $srcRel, $dstRel );
02181             }
02182         }
02183 
02184         // Lock the filearchive rows so that the files don't get deleted by a cleanup operation
02185         // We acquire this lock by running the inserts now, before the file operations.
02186         //
02187         // This potentially has poor lock contention characteristics -- an alternative
02188         // scheme would be to insert stub filearchive entries with no fa_name and commit
02189         // them in a separate transaction, then run the file ops, then update the fa_name fields.
02190         $this->doDBInserts();
02191 
02192         // Removes non-existent file from the batch, so we don't get errors.
02193         $this->deletionBatch = $this->removeNonexistentFiles( $this->deletionBatch );
02194 
02195         // Execute the file deletion batch
02196         $status = $this->file->repo->deleteBatch( $this->deletionBatch );
02197 
02198         if ( !$status->isGood() ) {
02199             $this->status->merge( $status );
02200         }
02201 
02202         if ( !$this->status->isOK() ) {
02203             // Critical file deletion error
02204             // Roll back inserts, release lock and abort
02205             // TODO: delete the defunct filearchive rows if we are using a non-transactional DB
02206             $this->file->unlockAndRollback();
02207             wfProfileOut( __METHOD__ );
02208 
02209             return $this->status;
02210         }
02211 
02212         // Delete image/oldimage rows
02213         $this->doDBDeletes();
02214 
02215         // Commit and return
02216         $this->file->unlock();
02217         wfProfileOut( __METHOD__ );
02218 
02219         return $this->status;
02220     }
02221 
02227     function removeNonexistentFiles( $batch ) {
02228         $files = $newBatch = array();
02229 
02230         foreach ( $batch as $batchItem ) {
02231             list( $src, ) = $batchItem;
02232             $files[$src] = $this->file->repo->getVirtualUrl( 'public' ) . '/' . rawurlencode( $src );
02233         }
02234 
02235         $result = $this->file->repo->fileExistsBatch( $files );
02236 
02237         foreach ( $batch as $batchItem ) {
02238             if ( $result[$batchItem[0]] ) {
02239                 $newBatch[] = $batchItem;
02240             }
02241         }
02242 
02243         return $newBatch;
02244     }
02245 }
02246 
02247 # ------------------------------------------------------------------------------
02248 
02253 class LocalFileRestoreBatch {
02255     private $file;
02256 
02258     private $cleanupBatch;
02259 
02261     private $ids;
02262 
02264     private $all;
02265 
02267     private $unsuppress = false;
02268 
02273     function __construct( File $file, $unsuppress = false ) {
02274         $this->file = $file;
02275         $this->cleanupBatch = $this->ids = array();
02276         $this->ids = array();
02277         $this->unsuppress = $unsuppress;
02278     }
02279 
02283     function addId( $fa_id ) {
02284         $this->ids[] = $fa_id;
02285     }
02286 
02290     function addIds( $ids ) {
02291         $this->ids = array_merge( $this->ids, $ids );
02292     }
02293 
02297     function addAll() {
02298         $this->all = true;
02299     }
02300 
02309     function execute() {
02310         global $wgLang;
02311 
02312         if ( !$this->all && !$this->ids ) {
02313             // Do nothing
02314             return $this->file->repo->newGood();
02315         }
02316 
02317         $exists = $this->file->lock();
02318         $dbw = $this->file->repo->getMasterDB();
02319         $status = $this->file->repo->newGood();
02320 
02321         // Fetch all or selected archived revisions for the file,
02322         // sorted from the most recent to the oldest.
02323         $conditions = array( 'fa_name' => $this->file->getName() );
02324 
02325         if ( !$this->all ) {
02326             $conditions['fa_id'] = $this->ids;
02327         }
02328 
02329         $result = $dbw->select(
02330             'filearchive',
02331             ArchivedFile::selectFields(),
02332             $conditions,
02333             __METHOD__,
02334             array( 'ORDER BY' => 'fa_timestamp DESC' )
02335         );
02336 
02337         $idsPresent = array();
02338         $storeBatch = array();
02339         $insertBatch = array();
02340         $insertCurrent = false;
02341         $deleteIds = array();
02342         $first = true;
02343         $archiveNames = array();
02344 
02345         foreach ( $result as $row ) {
02346             $idsPresent[] = $row->fa_id;
02347 
02348             if ( $row->fa_name != $this->file->getName() ) {
02349                 $status->error( 'undelete-filename-mismatch', $wgLang->timeanddate( $row->fa_timestamp ) );
02350                 $status->failCount++;
02351                 continue;
02352             }
02353 
02354             if ( $row->fa_storage_key == '' ) {
02355                 // Revision was missing pre-deletion
02356                 $status->error( 'undelete-bad-store-key', $wgLang->timeanddate( $row->fa_timestamp ) );
02357                 $status->failCount++;
02358                 continue;
02359             }
02360 
02361             $deletedRel = $this->file->repo->getDeletedHashPath( $row->fa_storage_key ) .
02362                 $row->fa_storage_key;
02363             $deletedUrl = $this->file->repo->getVirtualUrl() . '/deleted/' . $deletedRel;
02364 
02365             if ( isset( $row->fa_sha1 ) ) {
02366                 $sha1 = $row->fa_sha1;
02367             } else {
02368                 // old row, populate from key
02369                 $sha1 = LocalRepo::getHashFromKey( $row->fa_storage_key );
02370             }
02371 
02372             # Fix leading zero
02373             if ( strlen( $sha1 ) == 32 && $sha1[0] == '0' ) {
02374                 $sha1 = substr( $sha1, 1 );
02375             }
02376 
02377             if ( is_null( $row->fa_major_mime ) || $row->fa_major_mime == 'unknown'
02378                 || is_null( $row->fa_minor_mime ) || $row->fa_minor_mime == 'unknown'
02379                 || is_null( $row->fa_media_type ) || $row->fa_media_type == 'UNKNOWN'
02380                 || is_null( $row->fa_metadata )
02381             ) {
02382                 // Refresh our metadata
02383                 // Required for a new current revision; nice for older ones too. :)
02384                 $props = RepoGroup::singleton()->getFileProps( $deletedUrl );
02385             } else {
02386                 $props = array(
02387                     'minor_mime' => $row->fa_minor_mime,
02388                     'major_mime' => $row->fa_major_mime,
02389                     'media_type' => $row->fa_media_type,
02390                     'metadata' => $row->fa_metadata
02391                 );
02392             }
02393 
02394             if ( $first && !$exists ) {
02395                 // This revision will be published as the new current version
02396                 $destRel = $this->file->getRel();
02397                 $insertCurrent = array(
02398                     'img_name' => $row->fa_name,
02399                     'img_size' => $row->fa_size,
02400                     'img_width' => $row->fa_width,
02401                     'img_height' => $row->fa_height,
02402                     'img_metadata' => $props['metadata'],
02403                     'img_bits' => $row->fa_bits,
02404                     'img_media_type' => $props['media_type'],
02405                     'img_major_mime' => $props['major_mime'],
02406                     'img_minor_mime' => $props['minor_mime'],
02407                     'img_description' => $row->fa_description,
02408                     'img_user' => $row->fa_user,
02409                     'img_user_text' => $row->fa_user_text,
02410                     'img_timestamp' => $row->fa_timestamp,
02411                     'img_sha1' => $sha1
02412                 );
02413 
02414                 // The live (current) version cannot be hidden!
02415                 if ( !$this->unsuppress && $row->fa_deleted ) {
02416                     $storeBatch[] = array( $deletedUrl, 'public', $destRel );
02417                     $this->cleanupBatch[] = $row->fa_storage_key;
02418                 }
02419             } else {
02420                 $archiveName = $row->fa_archive_name;
02421 
02422                 if ( $archiveName == '' ) {
02423                     // This was originally a current version; we
02424                     // have to devise a new archive name for it.
02425                     // Format is <timestamp of archiving>!<name>
02426                     $timestamp = wfTimestamp( TS_UNIX, $row->fa_deleted_timestamp );
02427 
02428                     do {
02429                         $archiveName = wfTimestamp( TS_MW, $timestamp ) . '!' . $row->fa_name;
02430                         $timestamp++;
02431                     } while ( isset( $archiveNames[$archiveName] ) );
02432                 }
02433 
02434                 $archiveNames[$archiveName] = true;
02435                 $destRel = $this->file->getArchiveRel( $archiveName );
02436                 $insertBatch[] = array(
02437                     'oi_name' => $row->fa_name,
02438                     'oi_archive_name' => $archiveName,
02439                     'oi_size' => $row->fa_size,
02440                     'oi_width' => $row->fa_width,
02441                     'oi_height' => $row->fa_height,
02442                     'oi_bits' => $row->fa_bits,
02443                     'oi_description' => $row->fa_description,
02444                     'oi_user' => $row->fa_user,
02445                     'oi_user_text' => $row->fa_user_text,
02446                     'oi_timestamp' => $row->fa_timestamp,
02447                     'oi_metadata' => $props['metadata'],
02448                     'oi_media_type' => $props['media_type'],
02449                     'oi_major_mime' => $props['major_mime'],
02450                     'oi_minor_mime' => $props['minor_mime'],
02451                     'oi_deleted' => $this->unsuppress ? 0 : $row->fa_deleted,
02452                     'oi_sha1' => $sha1 );
02453             }
02454 
02455             $deleteIds[] = $row->fa_id;
02456 
02457             if ( !$this->unsuppress && $row->fa_deleted & File::DELETED_FILE ) {
02458                 // private files can stay where they are
02459                 $status->successCount++;
02460             } else {
02461                 $storeBatch[] = array( $deletedUrl, 'public', $destRel );
02462                 $this->cleanupBatch[] = $row->fa_storage_key;
02463             }
02464 
02465             $first = false;
02466         }
02467 
02468         unset( $result );
02469 
02470         // Add a warning to the status object for missing IDs
02471         $missingIds = array_diff( $this->ids, $idsPresent );
02472 
02473         foreach ( $missingIds as $id ) {
02474             $status->error( 'undelete-missing-filearchive', $id );
02475         }
02476 
02477         // Remove missing files from batch, so we don't get errors when undeleting them
02478         $storeBatch = $this->removeNonexistentFiles( $storeBatch );
02479 
02480         // Run the store batch
02481         // Use the OVERWRITE_SAME flag to smooth over a common error
02482         $storeStatus = $this->file->repo->storeBatch( $storeBatch, FileRepo::OVERWRITE_SAME );
02483         $status->merge( $storeStatus );
02484 
02485         if ( !$status->isGood() ) {
02486             // Even if some files could be copied, fail entirely as that is the
02487             // easiest thing to do without data loss
02488             $this->cleanupFailedBatch( $storeStatus, $storeBatch );
02489             $status->ok = false;
02490             $this->file->unlock();
02491 
02492             return $status;
02493         }
02494 
02495         // Run the DB updates
02496         // Because we have locked the image row, key conflicts should be rare.
02497         // If they do occur, we can roll back the transaction at this time with
02498         // no data loss, but leaving unregistered files scattered throughout the
02499         // public zone.
02500         // This is not ideal, which is why it's important to lock the image row.
02501         if ( $insertCurrent ) {
02502             $dbw->insert( 'image', $insertCurrent, __METHOD__ );
02503         }
02504 
02505         if ( $insertBatch ) {
02506             $dbw->insert( 'oldimage', $insertBatch, __METHOD__ );
02507         }
02508 
02509         if ( $deleteIds ) {
02510             $dbw->delete( 'filearchive',
02511                 array( 'fa_id' => $deleteIds ),
02512                 __METHOD__ );
02513         }
02514 
02515         // If store batch is empty (all files are missing), deletion is to be considered successful
02516         if ( $status->successCount > 0 || !$storeBatch ) {
02517             if ( !$exists ) {
02518                 wfDebug( __METHOD__ . " restored {$status->successCount} items, creating a new current\n" );
02519 
02520                 DeferredUpdates::addUpdate( SiteStatsUpdate::factory( array( 'images' => 1 ) ) );
02521 
02522                 $this->file->purgeEverything();
02523             } else {
02524                 wfDebug( __METHOD__ . " restored {$status->successCount} as archived versions\n" );
02525                 $this->file->purgeDescription();
02526                 $this->file->purgeHistory();
02527             }
02528         }
02529 
02530         $this->file->unlock();
02531 
02532         return $status;
02533     }
02534 
02540     function removeNonexistentFiles( $triplets ) {
02541         $files = $filteredTriplets = array();
02542         foreach ( $triplets as $file ) {
02543             $files[$file[0]] = $file[0];
02544         }
02545 
02546         $result = $this->file->repo->fileExistsBatch( $files );
02547 
02548         foreach ( $triplets as $file ) {
02549             if ( $result[$file[0]] ) {
02550                 $filteredTriplets[] = $file;
02551             }
02552         }
02553 
02554         return $filteredTriplets;
02555     }
02556 
02562     function removeNonexistentFromCleanup( $batch ) {
02563         $files = $newBatch = array();
02564         $repo = $this->file->repo;
02565 
02566         foreach ( $batch as $file ) {
02567             $files[$file] = $repo->getVirtualUrl( 'deleted' ) . '/' .
02568                 rawurlencode( $repo->getDeletedHashPath( $file ) . $file );
02569         }
02570 
02571         $result = $repo->fileExistsBatch( $files );
02572 
02573         foreach ( $batch as $file ) {
02574             if ( $result[$file] ) {
02575                 $newBatch[] = $file;
02576             }
02577         }
02578 
02579         return $newBatch;
02580     }
02581 
02587     function cleanup() {
02588         if ( !$this->cleanupBatch ) {
02589             return $this->file->repo->newGood();
02590         }
02591 
02592         $this->cleanupBatch = $this->removeNonexistentFromCleanup( $this->cleanupBatch );
02593 
02594         $status = $this->file->repo->cleanupDeletedBatch( $this->cleanupBatch );
02595 
02596         return $status;
02597     }
02598 
02606     function cleanupFailedBatch( $storeStatus, $storeBatch ) {
02607         $cleanupBatch = array();
02608 
02609         foreach ( $storeStatus->success as $i => $success ) {
02610             // Check if this item of the batch was successfully copied
02611             if ( $success ) {
02612                 // Item was successfully copied and needs to be removed again
02613                 // Extract ($dstZone, $dstRel) from the batch
02614                 $cleanupBatch[] = array( $storeBatch[$i][1], $storeBatch[$i][2] );
02615             }
02616         }
02617         $this->file->repo->cleanupBatch( $cleanupBatch );
02618     }
02619 }
02620 
02621 # ------------------------------------------------------------------------------
02622 
02627 class LocalFileMoveBatch {
02629     protected $file;
02630 
02632     protected $target;
02633 
02634     protected $cur;
02635 
02636     protected $olds;
02637 
02638     protected $oldCount;
02639 
02640     protected $archive;
02641 
02643     protected $db;
02644 
02649     function __construct( File $file, Title $target ) {
02650         $this->file = $file;
02651         $this->target = $target;
02652         $this->oldHash = $this->file->repo->getHashPath( $this->file->getName() );
02653         $this->newHash = $this->file->repo->getHashPath( $this->target->getDBkey() );
02654         $this->oldName = $this->file->getName();
02655         $this->newName = $this->file->repo->getNameFromTitle( $this->target );
02656         $this->oldRel = $this->oldHash . $this->oldName;
02657         $this->newRel = $this->newHash . $this->newName;
02658         $this->db = $file->getRepo()->getMasterDb();
02659     }
02660 
02664     function addCurrent() {
02665         $this->cur = array( $this->oldRel, $this->newRel );
02666     }
02667 
02672     function addOlds() {
02673         $archiveBase = 'archive';
02674         $this->olds = array();
02675         $this->oldCount = 0;
02676         $archiveNames = array();
02677 
02678         $result = $this->db->select( 'oldimage',
02679             array( 'oi_archive_name', 'oi_deleted' ),
02680             array( 'oi_name' => $this->oldName ),
02681             __METHOD__
02682         );
02683 
02684         foreach ( $result as $row ) {
02685             $archiveNames[] = $row->oi_archive_name;
02686             $oldName = $row->oi_archive_name;
02687             $bits = explode( '!', $oldName, 2 );
02688 
02689             if ( count( $bits ) != 2 ) {
02690                 wfDebug( "Old file name missing !: '$oldName' \n" );
02691                 continue;
02692             }
02693 
02694             list( $timestamp, $filename ) = $bits;
02695 
02696             if ( $this->oldName != $filename ) {
02697                 wfDebug( "Old file name doesn't match: '$oldName' \n" );
02698                 continue;
02699             }
02700 
02701             $this->oldCount++;
02702 
02703             // Do we want to add those to oldCount?
02704             if ( $row->oi_deleted & File::DELETED_FILE ) {
02705                 continue;
02706             }
02707 
02708             $this->olds[] = array(
02709                 "{$archiveBase}/{$this->oldHash}{$oldName}",
02710                 "{$archiveBase}/{$this->newHash}{$timestamp}!{$this->newName}"
02711             );
02712         }
02713 
02714         return $archiveNames;
02715     }
02716 
02721     function execute() {
02722         $repo = $this->file->repo;
02723         $status = $repo->newGood();
02724 
02725         $triplets = $this->getMoveTriplets();
02726         $triplets = $this->removeNonexistentFiles( $triplets );
02727         $destFile = wfLocalFile( $this->target );
02728 
02729         $this->file->lock(); // begin
02730         $destFile->lock(); // quickly fail if destination is not available
02731         // Rename the file versions metadata in the DB.
02732         // This implicitly locks the destination file, which avoids race conditions.
02733         // If we moved the files from A -> C before DB updates, another process could
02734         // move files from B -> C at this point, causing storeBatch() to fail and thus
02735         // cleanupTarget() to trigger. It would delete the C files and cause data loss.
02736         $statusDb = $this->doDBUpdates();
02737         if ( !$statusDb->isGood() ) {
02738             $this->file->unlockAndRollback();
02739             $statusDb->ok = false;
02740 
02741             return $statusDb;
02742         }
02743         wfDebugLog( 'imagemove', "Renamed {$this->file->getName()} in database: " .
02744             "{$statusDb->successCount} successes, {$statusDb->failCount} failures" );
02745 
02746         // Copy the files into their new location.
02747         // If a prior process fataled copying or cleaning up files we tolerate any
02748         // of the existing files if they are identical to the ones being stored.
02749         $statusMove = $repo->storeBatch( $triplets, FileRepo::OVERWRITE_SAME );
02750         wfDebugLog( 'imagemove', "Moved files for {$this->file->getName()}: " .
02751             "{$statusMove->successCount} successes, {$statusMove->failCount} failures" );
02752         if ( !$statusMove->isGood() ) {
02753             // Delete any files copied over (while the destination is still locked)
02754             $this->cleanupTarget( $triplets );
02755             $this->file->unlockAndRollback(); // unlocks the destination
02756             wfDebugLog( 'imagemove', "Error in moving files: " . $statusMove->getWikiText() );
02757             $statusMove->ok = false;
02758 
02759             return $statusMove;
02760         }
02761         $destFile->unlock();
02762         $this->file->unlock(); // done
02763 
02764         // Everything went ok, remove the source files
02765         $this->cleanupSource( $triplets );
02766 
02767         $status->merge( $statusDb );
02768         $status->merge( $statusMove );
02769 
02770         return $status;
02771     }
02772 
02779     function doDBUpdates() {
02780         $repo = $this->file->repo;
02781         $status = $repo->newGood();
02782         $dbw = $this->db;
02783 
02784         // Update current image
02785         $dbw->update(
02786             'image',
02787             array( 'img_name' => $this->newName ),
02788             array( 'img_name' => $this->oldName ),
02789             __METHOD__
02790         );
02791 
02792         if ( $dbw->affectedRows() ) {
02793             $status->successCount++;
02794         } else {
02795             $status->failCount++;
02796             $status->fatal( 'imageinvalidfilename' );
02797 
02798             return $status;
02799         }
02800 
02801         // Update old images
02802         $dbw->update(
02803             'oldimage',
02804             array(
02805                 'oi_name' => $this->newName,
02806                 'oi_archive_name = ' . $dbw->strreplace( 'oi_archive_name',
02807                     $dbw->addQuotes( $this->oldName ), $dbw->addQuotes( $this->newName ) ),
02808             ),
02809             array( 'oi_name' => $this->oldName ),
02810             __METHOD__
02811         );
02812 
02813         $affected = $dbw->affectedRows();
02814         $total = $this->oldCount;
02815         $status->successCount += $affected;
02816         // Bug 34934: $total is based on files that actually exist.
02817         // There may be more DB rows than such files, in which case $affected
02818         // can be greater than $total. We use max() to avoid negatives here.
02819         $status->failCount += max( 0, $total - $affected );
02820         if ( $status->failCount ) {
02821             $status->error( 'imageinvalidfilename' );
02822         }
02823 
02824         return $status;
02825     }
02826 
02831     function getMoveTriplets() {
02832         $moves = array_merge( array( $this->cur ), $this->olds );
02833         $triplets = array(); // The format is: (srcUrl, destZone, destUrl)
02834 
02835         foreach ( $moves as $move ) {
02836             // $move: (oldRelativePath, newRelativePath)
02837             $srcUrl = $this->file->repo->getVirtualUrl() . '/public/' . rawurlencode( $move[0] );
02838             $triplets[] = array( $srcUrl, 'public', $move[1] );
02839             wfDebugLog(
02840                 'imagemove',
02841                 "Generated move triplet for {$this->file->getName()}: {$srcUrl} :: public :: {$move[1]}"
02842             );
02843         }
02844 
02845         return $triplets;
02846     }
02847 
02853     function removeNonexistentFiles( $triplets ) {
02854         $files = array();
02855 
02856         foreach ( $triplets as $file ) {
02857             $files[$file[0]] = $file[0];
02858         }
02859 
02860         $result = $this->file->repo->fileExistsBatch( $files );
02861         $filteredTriplets = array();
02862 
02863         foreach ( $triplets as $file ) {
02864             if ( $result[$file[0]] ) {
02865                 $filteredTriplets[] = $file;
02866             } else {
02867                 wfDebugLog( 'imagemove', "File {$file[0]} does not exist" );
02868             }
02869         }
02870 
02871         return $filteredTriplets;
02872     }
02873 
02878     function cleanupTarget( $triplets ) {
02879         // Create dest pairs from the triplets
02880         $pairs = array();
02881         foreach ( $triplets as $triplet ) {
02882             // $triplet: (old source virtual URL, dst zone, dest rel)
02883             $pairs[] = array( $triplet[1], $triplet[2] );
02884         }
02885 
02886         $this->file->repo->cleanupBatch( $pairs );
02887     }
02888 
02893     function cleanupSource( $triplets ) {
02894         // Create source file names from the triplets
02895         $files = array();
02896         foreach ( $triplets as $triplet ) {
02897             $files[] = $triplet[0];
02898         }
02899 
02900         $this->file->repo->cleanupBatch( $files );
02901     }
02902 }