MediaWiki  REL1_22
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 
00052     var
00053         $fileExists,       # does the file exist on disk? (loadFromXxx)
00054         $historyLine,      # Number of line to return by nextHistoryLine() (constructor)
00055         $historyRes,       # result of the query for the file's history (nextHistoryLine)
00056         $width,            # \
00057         $height,           #  |
00058         $bits,             #   --- returned by getimagesize (loadFromXxx)
00059         $attr,             # /
00060         $media_type,       # MEDIATYPE_xxx (bitmap, drawing, audio...)
00061         $mime,             # MIME type, determined by MimeMagic::guessMimeType
00062         $major_mime,       # Major mime type
00063         $minor_mime,       # Minor mime type
00064         $size,             # Size in bytes (loadFromXxx)
00065         $metadata,         # Handler-specific metadata
00066         $timestamp,        # Upload timestamp
00067         $sha1,             # SHA-1 base 36 content hash
00068         $user, $user_text, # User, who uploaded the file
00069         $description,      # Description of current revision of the file
00070         $dataLoaded,       # Whether or not core data has been loaded from the database (loadFromXxx)
00071         $extraDataLoaded,  # Whether or not lazy-loaded data has been loaded from the database
00072         $upgraded,         # Whether the row was upgraded on load
00073         $locked,           # True if the image row is locked
00074         $lockedOwnTrx,     # True if the image row is locked with a lock initiated transaction
00075         $missing,          # True if file is not present in file system. Not to be cached in memcached
00076         $deleted;          # Bitfield akin to rev_deleted
00077 
00083     var $repo;
00084 
00085     protected $repoClass = 'LocalRepo';
00086 
00087     const LOAD_ALL = 1; // integer; load all the lazy fields too (like metadata)
00088 
00101     static function newFromTitle( $title, $repo, $unused = null ) {
00102         return new self( $title, $repo );
00103     }
00104 
00114     static function newFromRow( $row, $repo ) {
00115         $title = Title::makeTitle( NS_FILE, $row->img_name );
00116         $file = new self( $title, $repo );
00117         $file->loadFromRow( $row );
00118 
00119         return $file;
00120     }
00121 
00132     static function newFromKey( $sha1, $repo, $timestamp = false ) {
00133         $dbr = $repo->getSlaveDB();
00134 
00135         $conds = array( 'img_sha1' => $sha1 );
00136         if ( $timestamp ) {
00137             $conds['img_timestamp'] = $dbr->timestamp( $timestamp );
00138         }
00139 
00140         $row = $dbr->selectRow( 'image', self::selectFields(), $conds, __METHOD__ );
00141         if ( $row ) {
00142             return self::newFromRow( $row, $repo );
00143         } else {
00144             return false;
00145         }
00146     }
00147 
00152     static function selectFields() {
00153         return array(
00154             'img_name',
00155             'img_size',
00156             'img_width',
00157             'img_height',
00158             'img_metadata',
00159             'img_bits',
00160             'img_media_type',
00161             'img_major_mime',
00162             'img_minor_mime',
00163             'img_description',
00164             'img_user',
00165             'img_user_text',
00166             'img_timestamp',
00167             'img_sha1',
00168         );
00169     }
00170 
00175     function __construct( $title, $repo ) {
00176         parent::__construct( $title, $repo );
00177 
00178         $this->metadata = '';
00179         $this->historyLine = 0;
00180         $this->historyRes = null;
00181         $this->dataLoaded = false;
00182         $this->extraDataLoaded = false;
00183 
00184         $this->assertRepoDefined();
00185         $this->assertTitleDefined();
00186     }
00187 
00193     function getCacheKey() {
00194         $hashedName = md5( $this->getName() );
00195 
00196         return $this->repo->getSharedCacheKey( 'file', $hashedName );
00197     }
00198 
00203     function loadFromCache() {
00204         global $wgMemc;
00205 
00206         wfProfileIn( __METHOD__ );
00207         $this->dataLoaded = false;
00208         $this->extraDataLoaded = false;
00209         $key = $this->getCacheKey();
00210 
00211         if ( !$key ) {
00212             wfProfileOut( __METHOD__ );
00213             return false;
00214         }
00215 
00216         $cachedValues = $wgMemc->get( $key );
00217 
00218         // Check if the key existed and belongs to this version of MediaWiki
00219         if ( isset( $cachedValues['version'] ) && $cachedValues['version'] == MW_FILE_VERSION ) {
00220             wfDebug( "Pulling file metadata from cache key $key\n" );
00221             $this->fileExists = $cachedValues['fileExists'];
00222             if ( $this->fileExists ) {
00223                 $this->setProps( $cachedValues );
00224             }
00225             $this->dataLoaded = true;
00226             $this->extraDataLoaded = true;
00227             foreach ( $this->getLazyCacheFields( '' ) as $field ) {
00228                 $this->extraDataLoaded = $this->extraDataLoaded && isset( $cachedValues[$field] );
00229             }
00230         }
00231 
00232         if ( $this->dataLoaded ) {
00233             wfIncrStats( 'image_cache_hit' );
00234         } else {
00235             wfIncrStats( 'image_cache_miss' );
00236         }
00237 
00238         wfProfileOut( __METHOD__ );
00239         return $this->dataLoaded;
00240     }
00241 
00245     function saveToCache() {
00246         global $wgMemc;
00247 
00248         $this->load();
00249         $key = $this->getCacheKey();
00250 
00251         if ( !$key ) {
00252             return;
00253         }
00254 
00255         $fields = $this->getCacheFields( '' );
00256         $cache = array( 'version' => MW_FILE_VERSION );
00257         $cache['fileExists'] = $this->fileExists;
00258 
00259         if ( $this->fileExists ) {
00260             foreach ( $fields as $field ) {
00261                 $cache[$field] = $this->$field;
00262             }
00263         }
00264 
00265         // Strip off excessive entries from the subset of fields that can become large.
00266         // If the cache value gets to large it will not fit in memcached and nothing will
00267         // get cached at all, causing master queries for any file access.
00268         foreach ( $this->getLazyCacheFields( '' ) as $field ) {
00269             if ( isset( $cache[$field] ) && strlen( $cache[$field] ) > 100 * 1024 ) {
00270                 unset( $cache[$field] ); // don't let the value get too big
00271             }
00272         }
00273 
00274         // Cache presence for 1 week and negatives for 1 day
00275         $wgMemc->set( $key, $cache, $this->fileExists ? 86400 * 7 : 86400 );
00276     }
00277 
00281     function loadFromFile() {
00282         $props = $this->repo->getFileProps( $this->getVirtualUrl() );
00283         $this->setProps( $props );
00284     }
00285 
00290     function getCacheFields( $prefix = 'img_' ) {
00291         static $fields = array( 'size', 'width', 'height', 'bits', 'media_type',
00292             'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1', 'user', 'user_text', 'description' );
00293         static $results = array();
00294 
00295         if ( $prefix == '' ) {
00296             return $fields;
00297         }
00298 
00299         if ( !isset( $results[$prefix] ) ) {
00300             $prefixedFields = array();
00301             foreach ( $fields as $field ) {
00302                 $prefixedFields[] = $prefix . $field;
00303             }
00304             $results[$prefix] = $prefixedFields;
00305         }
00306 
00307         return $results[$prefix];
00308     }
00309 
00313     function getLazyCacheFields( $prefix = 'img_' ) {
00314         static $fields = array( 'metadata' );
00315         static $results = array();
00316 
00317         if ( $prefix == '' ) {
00318             return $fields;
00319         }
00320 
00321         if ( !isset( $results[$prefix] ) ) {
00322             $prefixedFields = array();
00323             foreach ( $fields as $field ) {
00324                 $prefixedFields[] = $prefix . $field;
00325             }
00326             $results[$prefix] = $prefixedFields;
00327         }
00328 
00329         return $results[$prefix];
00330     }
00331 
00335     function loadFromDB() {
00336         # Polymorphic function name to distinguish foreign and local fetches
00337         $fname = get_class( $this ) . '::' . __FUNCTION__;
00338         wfProfileIn( $fname );
00339 
00340         # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
00341         $this->dataLoaded = true;
00342         $this->extraDataLoaded = true;
00343 
00344         $dbr = $this->repo->getMasterDB();
00345         $row = $dbr->selectRow( 'image', $this->getCacheFields( 'img_' ),
00346             array( 'img_name' => $this->getName() ), $fname );
00347 
00348         if ( $row ) {
00349             $this->loadFromRow( $row );
00350         } else {
00351             $this->fileExists = false;
00352         }
00353 
00354         wfProfileOut( $fname );
00355     }
00356 
00361     protected function loadExtraFromDB() {
00362         # Polymorphic function name to distinguish foreign and local fetches
00363         $fname = get_class( $this ) . '::' . __FUNCTION__;
00364         wfProfileIn( $fname );
00365 
00366         # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
00367         $this->extraDataLoaded = true;
00368 
00369         $dbr = $this->repo->getSlaveDB();
00370         // In theory the file could have just been renamed/deleted...oh well
00371         $row = $dbr->selectRow( 'image', $this->getLazyCacheFields( 'img_' ),
00372             array( 'img_name' => $this->getName() ), $fname );
00373 
00374         if ( !$row ) { // fallback to master
00375             $dbr = $this->repo->getMasterDB();
00376             $row = $dbr->selectRow( 'image', $this->getLazyCacheFields( 'img_' ),
00377                 array( 'img_name' => $this->getName() ), $fname );
00378         }
00379 
00380         if ( $row ) {
00381             foreach ( $this->unprefixRow( $row, 'img_' ) as $name => $value ) {
00382                 $this->$name = $value;
00383             }
00384         } else {
00385             wfProfileOut( $fname );
00386             throw new MWException( "Could not find data for image '{$this->getName()}'." );
00387         }
00388 
00389         wfProfileOut( $fname );
00390     }
00391 
00397     protected function unprefixRow( $row, $prefix = 'img_' ) {
00398         $array = (array)$row;
00399         $prefixLength = strlen( $prefix );
00400 
00401         // Sanity check prefix once
00402         if ( substr( key( $array ), 0, $prefixLength ) !== $prefix ) {
00403             throw new MWException( __METHOD__ . ': incorrect $prefix parameter' );
00404         }
00405 
00406         $decoded = array();
00407         foreach ( $array as $name => $value ) {
00408             $decoded[substr( $name, $prefixLength )] = $value;
00409         }
00410         return $decoded;
00411     }
00412 
00421     function decodeRow( $row, $prefix = 'img_' ) {
00422         $decoded = $this->unprefixRow( $row, $prefix );
00423 
00424         $decoded['timestamp'] = wfTimestamp( TS_MW, $decoded['timestamp'] );
00425 
00426         $decoded['metadata'] = $this->repo->getSlaveDB()->decodeBlob( $decoded['metadata'] );
00427 
00428         if ( empty( $decoded['major_mime'] ) ) {
00429             $decoded['mime'] = 'unknown/unknown';
00430         } else {
00431             if ( !$decoded['minor_mime'] ) {
00432                 $decoded['minor_mime'] = 'unknown';
00433             }
00434             $decoded['mime'] = $decoded['major_mime'] . '/' . $decoded['minor_mime'];
00435         }
00436 
00437         # Trim zero padding from char/binary field
00438         $decoded['sha1'] = rtrim( $decoded['sha1'], "\0" );
00439 
00440         return $decoded;
00441     }
00442 
00446     function loadFromRow( $row, $prefix = 'img_' ) {
00447         $this->dataLoaded = true;
00448         $this->extraDataLoaded = true;
00449 
00450         $array = $this->decodeRow( $row, $prefix );
00451 
00452         foreach ( $array as $name => $value ) {
00453             $this->$name = $value;
00454         }
00455 
00456         $this->fileExists = true;
00457         $this->maybeUpgradeRow();
00458     }
00459 
00464     function load( $flags = 0 ) {
00465         if ( !$this->dataLoaded ) {
00466             if ( !$this->loadFromCache() ) {
00467                 $this->loadFromDB();
00468                 $this->saveToCache();
00469             }
00470             $this->dataLoaded = true;
00471         }
00472         if ( ( $flags & self::LOAD_ALL ) && !$this->extraDataLoaded ) {
00473             $this->loadExtraFromDB();
00474         }
00475     }
00476 
00480     function maybeUpgradeRow() {
00481         global $wgUpdateCompatibleMetadata;
00482         if ( wfReadOnly() ) {
00483             return;
00484         }
00485 
00486         if ( is_null( $this->media_type ) ||
00487             $this->mime == 'image/svg'
00488         ) {
00489             $this->upgradeRow();
00490             $this->upgraded = true;
00491         } else {
00492             $handler = $this->getHandler();
00493             if ( $handler ) {
00494                 $validity = $handler->isMetadataValid( $this, $this->getMetadata() );
00495                 if ( $validity === MediaHandler::METADATA_BAD
00496                     || ( $validity === MediaHandler::METADATA_COMPATIBLE && $wgUpdateCompatibleMetadata )
00497                 ) {
00498                     $this->upgradeRow();
00499                     $this->upgraded = true;
00500                 }
00501             }
00502         }
00503     }
00504 
00505     function getUpgraded() {
00506         return $this->upgraded;
00507     }
00508 
00512     function upgradeRow() {
00513         wfProfileIn( __METHOD__ );
00514 
00515         $this->lock(); // begin
00516 
00517         $this->loadFromFile();
00518 
00519         # Don't destroy file info of missing files
00520         if ( !$this->fileExists ) {
00521             wfDebug( __METHOD__ . ": file does not exist, aborting\n" );
00522             wfProfileOut( __METHOD__ );
00523             return;
00524         }
00525 
00526         $dbw = $this->repo->getMasterDB();
00527         list( $major, $minor ) = self::splitMime( $this->mime );
00528 
00529         if ( wfReadOnly() ) {
00530             wfProfileOut( __METHOD__ );
00531             return;
00532         }
00533         wfDebug( __METHOD__ . ': upgrading ' . $this->getName() . " to the current schema\n" );
00534 
00535         $dbw->update( 'image',
00536             array(
00537                 'img_size' => $this->size, // sanity
00538                 'img_width' => $this->width,
00539                 'img_height' => $this->height,
00540                 'img_bits' => $this->bits,
00541                 'img_media_type' => $this->media_type,
00542                 'img_major_mime' => $major,
00543                 'img_minor_mime' => $minor,
00544                 'img_metadata' => $dbw->encodeBlob($this->metadata),
00545                 'img_sha1' => $this->sha1,
00546             ),
00547             array( 'img_name' => $this->getName() ),
00548             __METHOD__
00549         );
00550 
00551         $this->saveToCache();
00552 
00553         $this->unlock(); // done
00554 
00555         wfProfileOut( __METHOD__ );
00556     }
00557 
00566     function setProps( $info ) {
00567         $this->dataLoaded = true;
00568         $fields = $this->getCacheFields( '' );
00569         $fields[] = 'fileExists';
00570 
00571         foreach ( $fields as $field ) {
00572             if ( isset( $info[$field] ) ) {
00573                 $this->$field = $info[$field];
00574             }
00575         }
00576 
00577         // Fix up mime fields
00578         if ( isset( $info['major_mime'] ) ) {
00579             $this->mime = "{$info['major_mime']}/{$info['minor_mime']}";
00580         } elseif ( isset( $info['mime'] ) ) {
00581             $this->mime = $info['mime'];
00582             list( $this->major_mime, $this->minor_mime ) = self::splitMime( $this->mime );
00583         }
00584     }
00585 
00597     function isMissing() {
00598         if ( $this->missing === null ) {
00599             list( $fileExists ) = $this->repo->fileExists( $this->getVirtualUrl() );
00600             $this->missing = !$fileExists;
00601         }
00602         return $this->missing;
00603     }
00604 
00611     public function getWidth( $page = 1 ) {
00612         $this->load();
00613 
00614         if ( $this->isMultipage() ) {
00615             $handler = $this->getHandler();
00616             if ( !$handler ) {
00617                 return 0;
00618             }
00619             $dim = $handler->getPageDimensions( $this, $page );
00620             if ( $dim ) {
00621                 return $dim['width'];
00622             } else {
00623                 // For non-paged media, the false goes through an
00624                 // intval, turning failure into 0, so do same here.
00625                 return 0;
00626             }
00627         } else {
00628             return $this->width;
00629         }
00630     }
00631 
00638     public function getHeight( $page = 1 ) {
00639         $this->load();
00640 
00641         if ( $this->isMultipage() ) {
00642             $handler = $this->getHandler();
00643             if ( !$handler ) {
00644                 return 0;
00645             }
00646             $dim = $handler->getPageDimensions( $this, $page );
00647             if ( $dim ) {
00648                 return $dim['height'];
00649             } else {
00650                 // For non-paged media, the false goes through an
00651                 // intval, turning failure into 0, so do same here.
00652                 return 0;
00653             }
00654         } else {
00655             return $this->height;
00656         }
00657     }
00658 
00665     function getUser( $type = 'text' ) {
00666         $this->load();
00667 
00668         if ( $type == 'text' ) {
00669             return $this->user_text;
00670         } elseif ( $type == 'id' ) {
00671             return $this->user;
00672         }
00673     }
00674 
00679     function getMetadata() {
00680         $this->load( self::LOAD_ALL ); // large metadata is loaded in another step
00681         return $this->metadata;
00682     }
00683 
00687     function getBitDepth() {
00688         $this->load();
00689         return $this->bits;
00690     }
00691 
00696     public function getSize() {
00697         $this->load();
00698         return $this->size;
00699     }
00700 
00705     function getMimeType() {
00706         $this->load();
00707         return $this->mime;
00708     }
00709 
00715     function getMediaType() {
00716         $this->load();
00717         return $this->media_type;
00718     }
00719 
00730     public function exists() {
00731         $this->load();
00732         return $this->fileExists;
00733     }
00734 
00747     function migrateThumbFile( $thumbName ) {
00748         /* Old code for bug 2532
00749         $thumbDir = $this->getThumbPath();
00750         $thumbPath = "$thumbDir/$thumbName";
00751         if ( is_dir( $thumbPath ) ) {
00752             // Directory where file should be
00753             // This happened occasionally due to broken migration code in 1.5
00754             // Rename to broken-*
00755             for ( $i = 0; $i < 100; $i++ ) {
00756                 $broken = $this->repo->getZonePath( 'public' ) . "/broken-$i-$thumbName";
00757                 if ( !file_exists( $broken ) ) {
00758                     rename( $thumbPath, $broken );
00759                     break;
00760                 }
00761             }
00762             // Doesn't exist anymore
00763             clearstatcache();
00764         }
00765         */
00766 
00767         /*
00768         if ( $this->repo->fileExists( $thumbDir ) ) {
00769             // Delete file where directory should be
00770             $this->repo->cleanupBatch( array( $thumbDir ) );
00771         }
00772         */
00773     }
00774 
00784     function getThumbnails( $archiveName = false ) {
00785         if ( $archiveName ) {
00786             $dir = $this->getArchiveThumbPath( $archiveName );
00787         } else {
00788             $dir = $this->getThumbPath();
00789         }
00790 
00791         $backend = $this->repo->getBackend();
00792         $files = array( $dir );
00793         try {
00794             $iterator = $backend->getFileList( array( 'dir' => $dir ) );
00795             foreach ( $iterator as $file ) {
00796                 $files[] = $file;
00797             }
00798         } catch ( FileBackendError $e ) {} // suppress (bug 54674)
00799 
00800         return $files;
00801     }
00802 
00806     function purgeMetadataCache() {
00807         $this->loadFromDB();
00808         $this->saveToCache();
00809         $this->purgeHistory();
00810     }
00811 
00817     function purgeHistory() {
00818         global $wgMemc;
00819 
00820         $hashedName = md5( $this->getName() );
00821         $oldKey = $this->repo->getSharedCacheKey( 'oldfile', $hashedName );
00822 
00823         if ( $oldKey ) {
00824             $wgMemc->delete( $oldKey );
00825         }
00826     }
00827 
00835     function purgeCache( $options = array() ) {
00836         wfProfileIn( __METHOD__ );
00837         // Refresh metadata cache
00838         $this->purgeMetadataCache();
00839 
00840         // Delete thumbnails
00841         $this->purgeThumbnails( $options );
00842 
00843         // Purge squid cache for this file
00844         SquidUpdate::purge( array( $this->getURL() ) );
00845         wfProfileOut( __METHOD__ );
00846     }
00847 
00852     function purgeOldThumbnails( $archiveName ) {
00853         global $wgUseSquid;
00854         wfProfileIn( __METHOD__ );
00855 
00856         // Get a list of old thumbnails and URLs
00857         $files = $this->getThumbnails( $archiveName );
00858         $dir = array_shift( $files );
00859         $this->purgeThumbList( $dir, $files );
00860 
00861         // Purge any custom thumbnail caches
00862         wfRunHooks( 'LocalFilePurgeThumbnails', array( $this, $archiveName ) );
00863 
00864         // Purge the squid
00865         if ( $wgUseSquid ) {
00866             $urls = array();
00867             foreach ( $files as $file ) {
00868                 $urls[] = $this->getArchiveThumbUrl( $archiveName, $file );
00869             }
00870             SquidUpdate::purge( $urls );
00871         }
00872 
00873         wfProfileOut( __METHOD__ );
00874     }
00875 
00879     function purgeThumbnails( $options = array() ) {
00880         global $wgUseSquid;
00881         wfProfileIn( __METHOD__ );
00882 
00883         // Delete thumbnails
00884         $files = $this->getThumbnails();
00885         // Always purge all files from squid regardless of handler filters
00886         if ( $wgUseSquid ) {
00887             $urls = array();
00888             foreach ( $files as $file ) {
00889                 $urls[] = $this->getThumbUrl( $file );
00890             }
00891             array_shift( $urls ); // don't purge directory
00892         }
00893 
00894         // Give media handler a chance to filter the file purge list
00895         if ( !empty( $options['forThumbRefresh'] ) ) {
00896             $handler = $this->getHandler();
00897             if ( $handler ) {
00898                 $handler->filterThumbnailPurgeList( $files, $options );
00899             }
00900         }
00901 
00902         $dir = array_shift( $files );
00903         $this->purgeThumbList( $dir, $files );
00904 
00905         // Purge any custom thumbnail caches
00906         wfRunHooks( 'LocalFilePurgeThumbnails', array( $this, false ) );
00907 
00908         // Purge the squid
00909         if ( $wgUseSquid ) {
00910             SquidUpdate::purge( $urls );
00911         }
00912 
00913         wfProfileOut( __METHOD__ );
00914     }
00915 
00921     protected function purgeThumbList( $dir, $files ) {
00922         $fileListDebug = strtr(
00923             var_export( $files, true ),
00924             array( "\n" => '' )
00925         );
00926         wfDebug( __METHOD__ . ": $fileListDebug\n" );
00927 
00928         $purgeList = array();
00929         foreach ( $files as $file ) {
00930             # Check that the base file name is part of the thumb name
00931             # This is a basic sanity check to avoid erasing unrelated directories
00932             if ( strpos( $file, $this->getName() ) !== false
00933                 || strpos( $file, "-thumbnail" ) !== false // "short" thumb name
00934             ) {
00935                 $purgeList[] = "{$dir}/{$file}";
00936             }
00937         }
00938 
00939         # Delete the thumbnails
00940         $this->repo->quickPurgeBatch( $purgeList );
00941         # Clear out the thumbnail directory if empty
00942         $this->repo->quickCleanDir( $dir );
00943     }
00944 
00955     function getHistory( $limit = null, $start = null, $end = null, $inc = true ) {
00956         $dbr = $this->repo->getSlaveDB();
00957         $tables = array( 'oldimage' );
00958         $fields = OldLocalFile::selectFields();
00959         $conds = $opts = $join_conds = array();
00960         $eq = $inc ? '=' : '';
00961         $conds[] = "oi_name = " . $dbr->addQuotes( $this->title->getDBkey() );
00962 
00963         if ( $start ) {
00964             $conds[] = "oi_timestamp <$eq " . $dbr->addQuotes( $dbr->timestamp( $start ) );
00965         }
00966 
00967         if ( $end ) {
00968             $conds[] = "oi_timestamp >$eq " . $dbr->addQuotes( $dbr->timestamp( $end ) );
00969         }
00970 
00971         if ( $limit ) {
00972             $opts['LIMIT'] = $limit;
00973         }
00974 
00975         // Search backwards for time > x queries
00976         $order = ( !$start && $end !== null ) ? 'ASC' : 'DESC';
00977         $opts['ORDER BY'] = "oi_timestamp $order";
00978         $opts['USE INDEX'] = array( 'oldimage' => 'oi_name_timestamp' );
00979 
00980         wfRunHooks( 'LocalFile::getHistory', array( &$this, &$tables, &$fields,
00981             &$conds, &$opts, &$join_conds ) );
00982 
00983         $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $opts, $join_conds );
00984         $r = array();
00985 
00986         foreach ( $res as $row ) {
00987             if ( $this->repo->oldFileFromRowFactory ) {
00988                 $r[] = call_user_func( $this->repo->oldFileFromRowFactory, $row, $this->repo );
00989             } else {
00990                 $r[] = OldLocalFile::newFromRow( $row, $this->repo );
00991             }
00992         }
00993 
00994         if ( $order == 'ASC' ) {
00995             $r = array_reverse( $r ); // make sure it ends up descending
00996         }
00997 
00998         return $r;
00999     }
01000 
01010     public function nextHistoryLine() {
01011         # Polymorphic function name to distinguish foreign and local fetches
01012         $fname = get_class( $this ) . '::' . __FUNCTION__;
01013 
01014         $dbr = $this->repo->getSlaveDB();
01015 
01016         if ( $this->historyLine == 0 ) {// called for the first time, return line from cur
01017             $this->historyRes = $dbr->select( 'image',
01018                 array(
01019                     '*',
01020                     "'' AS oi_archive_name",
01021                     '0 as oi_deleted',
01022                     'img_sha1'
01023                 ),
01024                 array( 'img_name' => $this->title->getDBkey() ),
01025                 $fname
01026             );
01027 
01028             if ( 0 == $dbr->numRows( $this->historyRes ) ) {
01029                 $this->historyRes = null;
01030                 return false;
01031             }
01032         } elseif ( $this->historyLine == 1 ) {
01033             $this->historyRes = $dbr->select( 'oldimage', '*',
01034                 array( 'oi_name' => $this->title->getDBkey() ),
01035                 $fname,
01036                 array( 'ORDER BY' => 'oi_timestamp DESC' )
01037             );
01038         }
01039         $this->historyLine ++;
01040 
01041         return $dbr->fetchObject( $this->historyRes );
01042     }
01043 
01047     public function resetHistory() {
01048         $this->historyLine = 0;
01049 
01050         if ( !is_null( $this->historyRes ) ) {
01051             $this->historyRes = null;
01052         }
01053     }
01054 
01083     function upload( $srcPath, $comment, $pageText, $flags = 0, $props = false, $timestamp = false, $user = null ) {
01084         global $wgContLang;
01085 
01086         if ( $this->getRepo()->getReadOnlyReason() !== false ) {
01087             return $this->readOnlyFatalStatus();
01088         }
01089 
01090         if ( !$props ) {
01091             wfProfileIn( __METHOD__ . '-getProps' );
01092             if ( $this->repo->isVirtualUrl( $srcPath )
01093                 || FileBackend::isStoragePath( $srcPath ) )
01094             {
01095                 $props = $this->repo->getFileProps( $srcPath );
01096             } else {
01097                 $props = FSFile::getPropsFromPath( $srcPath );
01098             }
01099             wfProfileOut( __METHOD__ . '-getProps' );
01100         }
01101 
01102         $options = array();
01103         $handler = MediaHandler::getHandler( $props['mime'] );
01104         if ( $handler ) {
01105             $options['headers'] = $handler->getStreamHeaders( $props['metadata'] );
01106         } else {
01107             $options['headers'] = array();
01108         }
01109 
01110         // Trim spaces on user supplied text
01111         $comment = trim( $comment );
01112 
01113         // truncate nicely or the DB will do it for us
01114         // non-nicely (dangling multi-byte chars, non-truncated version in cache).
01115         $comment = $wgContLang->truncate( $comment, 255 );
01116         $this->lock(); // begin
01117         $status = $this->publish( $srcPath, $flags, $options );
01118 
01119         if ( $status->successCount > 0 ) {
01120             # Essentially we are displacing any existing current file and saving
01121             # a new current file at the old location. If just the first succeeded,
01122             # we still need to displace the current DB entry and put in a new one.
01123             if ( !$this->recordUpload2( $status->value, $comment, $pageText, $props, $timestamp, $user ) ) {
01124                 $status->fatal( 'filenotfound', $srcPath );
01125             }
01126         }
01127 
01128         $this->unlock(); // done
01129 
01130         return $status;
01131     }
01132 
01145     function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
01146         $watch = false, $timestamp = false, User $user = null )
01147     {
01148         if ( !$user ) {
01149             global $wgUser;
01150             $user = $wgUser;
01151         }
01152 
01153         $pageText = SpecialUpload::getInitialPageText( $desc, $license, $copyStatus, $source );
01154 
01155         if ( !$this->recordUpload2( $oldver, $desc, $pageText, false, $timestamp, $user ) ) {
01156             return false;
01157         }
01158 
01159         if ( $watch ) {
01160             $user->addWatch( $this->getTitle() );
01161         }
01162         return true;
01163     }
01164 
01175     function recordUpload2(
01176         $oldver, $comment, $pageText, $props = false, $timestamp = false, $user = null
01177     ) {
01178         wfProfileIn( __METHOD__ );
01179 
01180         if ( is_null( $user ) ) {
01181             global $wgUser;
01182             $user = $wgUser;
01183         }
01184 
01185         $dbw = $this->repo->getMasterDB();
01186         $dbw->begin( __METHOD__ );
01187 
01188         if ( !$props ) {
01189             wfProfileIn( __METHOD__ . '-getProps' );
01190             $props = $this->repo->getFileProps( $this->getVirtualUrl() );
01191             wfProfileOut( __METHOD__ . '-getProps' );
01192         }
01193 
01194         if ( $timestamp === false ) {
01195             $timestamp = $dbw->timestamp();
01196         }
01197 
01198         $props['description'] = $comment;
01199         $props['user'] = $user->getId();
01200         $props['user_text'] = $user->getName();
01201         $props['timestamp'] = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
01202         $this->setProps( $props );
01203 
01204         # Fail now if the file isn't there
01205         if ( !$this->fileExists ) {
01206             wfDebug( __METHOD__ . ": File " . $this->getRel() . " went missing!\n" );
01207             wfProfileOut( __METHOD__ );
01208             return false;
01209         }
01210 
01211         $reupload = false;
01212 
01213         # Test to see if the row exists using INSERT IGNORE
01214         # This avoids race conditions by locking the row until the commit, and also
01215         # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
01216         $dbw->insert( 'image',
01217             array(
01218                 'img_name' => $this->getName(),
01219                 'img_size' => $this->size,
01220                 'img_width' => intval( $this->width ),
01221                 'img_height' => intval( $this->height ),
01222                 'img_bits' => $this->bits,
01223                 'img_media_type' => $this->media_type,
01224                 'img_major_mime' => $this->major_mime,
01225                 'img_minor_mime' => $this->minor_mime,
01226                 'img_timestamp' => $timestamp,
01227                 'img_description' => $comment,
01228                 'img_user' => $user->getId(),
01229                 'img_user_text' => $user->getName(),
01230                 'img_metadata' => $dbw->encodeBlob($this->metadata),
01231                 'img_sha1' => $this->sha1
01232             ),
01233             __METHOD__,
01234             'IGNORE'
01235         );
01236         if ( $dbw->affectedRows() == 0 ) {
01237             # (bug 34993) Note: $oldver can be empty here, if the previous
01238             # version of the file was broken. Allow registration of the new
01239             # version to continue anyway, because that's better than having
01240             # an image that's not fixable by user operations.
01241 
01242             $reupload = true;
01243             # Collision, this is an update of a file
01244             # Insert previous contents into oldimage
01245             $dbw->insertSelect( 'oldimage', 'image',
01246                 array(
01247                     'oi_name'         => 'img_name',
01248                     'oi_archive_name' => $dbw->addQuotes( $oldver ),
01249                     'oi_size'         => 'img_size',
01250                     'oi_width'        => 'img_width',
01251                     'oi_height'       => 'img_height',
01252                     'oi_bits'         => 'img_bits',
01253                     'oi_timestamp'    => 'img_timestamp',
01254                     'oi_description'  => 'img_description',
01255                     'oi_user'         => 'img_user',
01256                     'oi_user_text'    => 'img_user_text',
01257                     'oi_metadata'     => 'img_metadata',
01258                     'oi_media_type'   => 'img_media_type',
01259                     'oi_major_mime'   => 'img_major_mime',
01260                     'oi_minor_mime'   => 'img_minor_mime',
01261                     'oi_sha1'         => 'img_sha1'
01262                 ),
01263                 array( 'img_name' => $this->getName() ),
01264                 __METHOD__
01265             );
01266 
01267             # Update the current image row
01268             $dbw->update( 'image',
01269                 array( /* SET */
01270                     'img_size'        => $this->size,
01271                     'img_width'       => intval( $this->width ),
01272                     'img_height'      => intval( $this->height ),
01273                     'img_bits'        => $this->bits,
01274                     'img_media_type'  => $this->media_type,
01275                     'img_major_mime'  => $this->major_mime,
01276                     'img_minor_mime'  => $this->minor_mime,
01277                     'img_timestamp'   => $timestamp,
01278                     'img_description' => $comment,
01279                     'img_user'        => $user->getId(),
01280                     'img_user_text'   => $user->getName(),
01281                     'img_metadata'    => $dbw->encodeBlob($this->metadata),
01282                     'img_sha1'        => $this->sha1
01283                 ),
01284                 array( 'img_name' => $this->getName() ),
01285                 __METHOD__
01286             );
01287         } else {
01288             # This is a new file, so update the image count
01289             DeferredUpdates::addUpdate( SiteStatsUpdate::factory( array( 'images' => 1 ) ) );
01290         }
01291 
01292         $descTitle = $this->getTitle();
01293         $wikiPage = new WikiFilePage( $descTitle );
01294         $wikiPage->setFile( $this );
01295 
01296         # Add the log entry
01297         $action = $reupload ? 'overwrite' : 'upload';
01298 
01299         $logEntry = new ManualLogEntry( 'upload', $action );
01300         $logEntry->setPerformer( $user );
01301         $logEntry->setComment( $comment );
01302         $logEntry->setTarget( $descTitle );
01303 
01304         // Allow people using the api to associate log entries with the upload.
01305         // Log has a timestamp, but sometimes different from upload timestamp.
01306         $logEntry->setParameters(
01307             array(
01308                 'img_sha1' => $this->sha1,
01309                 'img_timestamp' => $timestamp,
01310             )
01311         );
01312         // Note we keep $logId around since during new image
01313         // creation, page doesn't exist yet, so log_page = 0
01314         // but we want it to point to the page we're making,
01315         // so we later modify the log entry.
01316         // For a similar reason, we avoid making an RC entry
01317         // now and wait until the page exists.
01318         $logId = $logEntry->insert();
01319 
01320         $exists = $descTitle->exists();
01321         if ( $exists ) {
01322             // Page exists, do RC entry now (otherwise we wait for later).
01323             $logEntry->publish( $logId );
01324         }
01325         wfProfileIn( __METHOD__ . '-edit' );
01326 
01327         if ( $exists ) {
01328             # Create a null revision
01329             $latest = $descTitle->getLatestRevID();
01330             $editSummary = LogFormatter::newFromEntry( $logEntry )->getPlainActionText();
01331 
01332             $nullRevision = Revision::newNullRevision(
01333                 $dbw,
01334                 $descTitle->getArticleID(),
01335                 $editSummary,
01336                 false
01337             );
01338             if ( !is_null( $nullRevision ) ) {
01339                 $nullRevision->insertOn( $dbw );
01340 
01341                 wfRunHooks( 'NewRevisionFromEditComplete', array( $wikiPage, $nullRevision, $latest, $user ) );
01342                 $wikiPage->updateRevisionOn( $dbw, $nullRevision );
01343             }
01344         }
01345 
01346         # Commit the transaction now, in case something goes wrong later
01347         # The most important thing is that files don't get lost, especially archives
01348         # NOTE: once we have support for nested transactions, the commit may be moved
01349         #       to after $wikiPage->doEdit has been called.
01350         $dbw->commit( __METHOD__ );
01351 
01352         if ( $exists ) {
01353             # Invalidate the cache for the description page
01354             $descTitle->invalidateCache();
01355             $descTitle->purgeSquid();
01356         } else {
01357             # New file; create the description page.
01358             # There's already a log entry, so don't make a second RC entry
01359             # Squid and file cache for the description page are purged by doEditContent.
01360             $content = ContentHandler::makeContent( $pageText, $descTitle );
01361             $status = $wikiPage->doEditContent( $content, $comment, EDIT_NEW | EDIT_SUPPRESS_RC, false, $user );
01362 
01363             $dbw->begin( __METHOD__ ); // XXX; doEdit() uses a transaction
01364             // Now that the page exists, make an RC entry.
01365             $logEntry->publish( $logId );
01366             if ( isset( $status->value['revision'] ) ) {
01367                 $dbw->update( 'logging',
01368                     array( 'log_page' => $status->value['revision']->getPage() ),
01369                     array( 'log_id' => $logId ),
01370                     __METHOD__
01371                 );
01372             }
01373             $dbw->commit( __METHOD__ ); // commit before anything bad can happen
01374         }
01375 
01376 
01377         wfProfileOut( __METHOD__ . '-edit' );
01378 
01379         # Save to cache and purge the squid
01380         # We shall not saveToCache before the commit since otherwise
01381         # in case of a rollback there is an usable file from memcached
01382         # which in fact doesn't really exist (bug 24978)
01383         $this->saveToCache();
01384 
01385         if ( $reupload ) {
01386             # Delete old thumbnails
01387             wfProfileIn( __METHOD__ . '-purge' );
01388             $this->purgeThumbnails();
01389             wfProfileOut( __METHOD__ . '-purge' );
01390 
01391             # Remove the old file from the squid cache
01392             SquidUpdate::purge( array( $this->getURL() ) );
01393         }
01394 
01395         # Hooks, hooks, the magic of hooks...
01396         wfProfileIn( __METHOD__ . '-hooks' );
01397         wfRunHooks( 'FileUpload', array( $this, $reupload, $descTitle->exists() ) );
01398         wfProfileOut( __METHOD__ . '-hooks' );
01399 
01400         # Invalidate cache for all pages using this file
01401         $update = new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' );
01402         $update->doUpdate();
01403         if ( !$reupload ) {
01404             LinksUpdate::queueRecursiveJobsForTable( $this->getTitle(), 'imagelinks' );
01405         }
01406 
01407         # Invalidate cache for all pages that redirects on this page
01408         $redirs = $this->getTitle()->getRedirectsHere();
01409 
01410         foreach ( $redirs as $redir ) {
01411             if ( !$reupload && $redir->getNamespace() === NS_FILE ) {
01412                 LinksUpdate::queueRecursiveJobsForTable( $redir, 'imagelinks' );
01413             }
01414             $update = new HTMLCacheUpdate( $redir, 'imagelinks' );
01415             $update->doUpdate();
01416         }
01417 
01418         wfProfileOut( __METHOD__ );
01419         return true;
01420     }
01421 
01437     function publish( $srcPath, $flags = 0, array $options = array() ) {
01438         return $this->publishTo( $srcPath, $this->getRel(), $flags, $options );
01439     }
01440 
01456     function publishTo( $srcPath, $dstRel, $flags = 0, array $options = array() ) {
01457         if ( $this->getRepo()->getReadOnlyReason() !== false ) {
01458             return $this->readOnlyFatalStatus();
01459         }
01460 
01461         $this->lock(); // begin
01462 
01463         $archiveName = wfTimestamp( TS_MW ) . '!' . $this->getName();
01464         $archiveRel = 'archive/' . $this->getHashPath() . $archiveName;
01465         $flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0;
01466         $status = $this->repo->publish( $srcPath, $dstRel, $archiveRel, $flags, $options );
01467 
01468         if ( $status->value == 'new' ) {
01469             $status->value = '';
01470         } else {
01471             $status->value = $archiveName;
01472         }
01473 
01474         $this->unlock(); // done
01475 
01476         return $status;
01477     }
01478 
01496     function move( $target ) {
01497         if ( $this->getRepo()->getReadOnlyReason() !== false ) {
01498             return $this->readOnlyFatalStatus();
01499         }
01500 
01501         wfDebugLog( 'imagemove', "Got request to move {$this->name} to " . $target->getText() );
01502         $batch = new LocalFileMoveBatch( $this, $target );
01503 
01504         $this->lock(); // begin
01505         $batch->addCurrent();
01506         $archiveNames = $batch->addOlds();
01507         $status = $batch->execute();
01508         $this->unlock(); // done
01509 
01510         wfDebugLog( 'imagemove', "Finished moving {$this->name}" );
01511 
01512         // Purge the source and target files...
01513         $oldTitleFile = wfLocalFile( $this->title );
01514         $newTitleFile = wfLocalFile( $target );
01515         // Hack: the lock()/unlock() pair is nested in a transaction so the locking is not
01516         // tied to BEGIN/COMMIT. To avoid slow purges in the transaction, move them outside.
01517         $this->getRepo()->getMasterDB()->onTransactionIdle(
01518             function() use ( $oldTitleFile, $newTitleFile, $archiveNames ) {
01519                 $oldTitleFile->purgeEverything();
01520                 foreach ( $archiveNames as $archiveName ) {
01521                     $oldTitleFile->purgeOldThumbnails( $archiveName );
01522                 }
01523                 $newTitleFile->purgeEverything();
01524             }
01525         );
01526 
01527         if ( $status->isOK() ) {
01528             // Now switch the object
01529             $this->title = $target;
01530             // Force regeneration of the name and hashpath
01531             unset( $this->name );
01532             unset( $this->hashPath );
01533         }
01534 
01535         return $status;
01536     }
01537 
01550     function delete( $reason, $suppress = false ) {
01551         if ( $this->getRepo()->getReadOnlyReason() !== false ) {
01552             return $this->readOnlyFatalStatus();
01553         }
01554 
01555         $batch = new LocalFileDeleteBatch( $this, $reason, $suppress );
01556 
01557         $this->lock(); // begin
01558         $batch->addCurrent();
01559         # Get old version relative paths
01560         $archiveNames = $batch->addOlds();
01561         $status = $batch->execute();
01562         $this->unlock(); // done
01563 
01564         if ( $status->isOK() ) {
01565             DeferredUpdates::addUpdate( SiteStatsUpdate::factory( array( 'images' => -1 ) ) );
01566         }
01567 
01568         // Hack: the lock()/unlock() pair is nested in a transaction so the locking is not
01569         // tied to BEGIN/COMMIT. To avoid slow purges in the transaction, move them outside.
01570         $file = $this;
01571         $this->getRepo()->getMasterDB()->onTransactionIdle(
01572             function() use ( $file, $archiveNames ) {
01573                 global $wgUseSquid;
01574 
01575                 $file->purgeEverything();
01576                 foreach ( $archiveNames as $archiveName ) {
01577                     $file->purgeOldThumbnails( $archiveName );
01578                 }
01579 
01580                 if ( $wgUseSquid ) {
01581                     // Purge the squid
01582                     $purgeUrls = array();
01583                     foreach ( $archiveNames as $archiveName ) {
01584                         $purgeUrls[] = $file->getArchiveUrl( $archiveName );
01585                     }
01586                     SquidUpdate::purge( $purgeUrls );
01587                 }
01588             }
01589         );
01590 
01591         return $status;
01592     }
01593 
01608     function deleteOld( $archiveName, $reason, $suppress = false ) {
01609         global $wgUseSquid;
01610         if ( $this->getRepo()->getReadOnlyReason() !== false ) {
01611             return $this->readOnlyFatalStatus();
01612         }
01613 
01614         $batch = new LocalFileDeleteBatch( $this, $reason, $suppress );
01615 
01616         $this->lock(); // begin
01617         $batch->addOld( $archiveName );
01618         $status = $batch->execute();
01619         $this->unlock(); // done
01620 
01621         $this->purgeOldThumbnails( $archiveName );
01622         if ( $status->isOK() ) {
01623             $this->purgeDescription();
01624             $this->purgeHistory();
01625         }
01626 
01627         if ( $wgUseSquid ) {
01628             // Purge the squid
01629             SquidUpdate::purge( array( $this->getArchiveUrl( $archiveName ) ) );
01630         }
01631 
01632         return $status;
01633     }
01634 
01646     function restore( $versions = array(), $unsuppress = false ) {
01647         if ( $this->getRepo()->getReadOnlyReason() !== false ) {
01648             return $this->readOnlyFatalStatus();
01649         }
01650 
01651         $batch = new LocalFileRestoreBatch( $this, $unsuppress );
01652 
01653         $this->lock(); // begin
01654         if ( !$versions ) {
01655             $batch->addAll();
01656         } else {
01657             $batch->addIds( $versions );
01658         }
01659         $status = $batch->execute();
01660         if ( $status->isGood() ) {
01661             $cleanupStatus = $batch->cleanup();
01662             $cleanupStatus->successCount = 0;
01663             $cleanupStatus->failCount = 0;
01664             $status->merge( $cleanupStatus );
01665         }
01666         $this->unlock(); // done
01667 
01668         return $status;
01669     }
01670 
01680     function getDescriptionUrl() {
01681         return $this->title->getLocalURL();
01682     }
01683 
01692     function getDescriptionText( $lang = null ) {
01693         $revision = Revision::newFromTitle( $this->title, false, Revision::READ_NORMAL );
01694         if ( !$revision ) {
01695             return false;
01696         }
01697         $content = $revision->getContent();
01698         if ( !$content ) {
01699             return false;
01700         }
01701         $pout = $content->getParserOutput( $this->title, null, new ParserOptions( null, $lang ) );
01702         return $pout->getText();
01703     }
01704 
01708     function getDescription( $audience = self::FOR_PUBLIC, User $user = null ) {
01709         $this->load();
01710         if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
01711             return '';
01712         } elseif ( $audience == self::FOR_THIS_USER
01713             && !$this->userCan( self::DELETED_COMMENT, $user ) )
01714         {
01715             return '';
01716         } else {
01717             return $this->description;
01718         }
01719     }
01720 
01724     function getTimestamp() {
01725         $this->load();
01726         return $this->timestamp;
01727     }
01728 
01732     function getSha1() {
01733         $this->load();
01734         // Initialise now if necessary
01735         if ( $this->sha1 == '' && $this->fileExists ) {
01736             $this->lock(); // begin
01737 
01738             $this->sha1 = $this->repo->getFileSha1( $this->getPath() );
01739             if ( !wfReadOnly() && strval( $this->sha1 ) != '' ) {
01740                 $dbw = $this->repo->getMasterDB();
01741                 $dbw->update( 'image',
01742                     array( 'img_sha1' => $this->sha1 ),
01743                     array( 'img_name' => $this->getName() ),
01744                     __METHOD__ );
01745                 $this->saveToCache();
01746             }
01747 
01748             $this->unlock(); // done
01749         }
01750 
01751         return $this->sha1;
01752     }
01753 
01757     function isCacheable() {
01758         $this->load();
01759         // If extra data (metadata) was not loaded then it must have been large
01760         return $this->extraDataLoaded
01761             && strlen( serialize( $this->metadata ) ) <= self::CACHE_FIELD_MAX_LEN;
01762     }
01763 
01769     function lock() {
01770         $dbw = $this->repo->getMasterDB();
01771 
01772         if ( !$this->locked ) {
01773             if ( !$dbw->trxLevel() ) {
01774                 $dbw->begin( __METHOD__ );
01775                 $this->lockedOwnTrx = true;
01776             }
01777             $this->locked++;
01778             // Bug 54736: use simple lock to handle when the file does not exist.
01779             // SELECT FOR UPDATE only locks records not the gaps where there are none.
01780             $cache = wfGetMainCache();
01781             $key = $this->getCacheKey();
01782             if ( !$cache->lock( $key, 60 ) ) {
01783                 throw new MWException( "Could not acquire lock for '{$this->getName()}.'" );
01784             }
01785             $dbw->onTransactionIdle( function() use ( $cache, $key ) {
01786                 $cache->unlock( $key ); // release on commit
01787             } );
01788         }
01789 
01790         return $dbw->selectField( 'image', '1',
01791             array( 'img_name' => $this->getName() ), __METHOD__, array( 'FOR UPDATE' ) );
01792     }
01793 
01798     function unlock() {
01799         if ( $this->locked ) {
01800             --$this->locked;
01801             if ( !$this->locked && $this->lockedOwnTrx ) {
01802                 $dbw = $this->repo->getMasterDB();
01803                 $dbw->commit( __METHOD__ );
01804                 $this->lockedOwnTrx = false;
01805             }
01806         }
01807     }
01808 
01812     function unlockAndRollback() {
01813         $this->locked = false;
01814         $dbw = $this->repo->getMasterDB();
01815         $dbw->rollback( __METHOD__ );
01816         $this->lockedOwnTrx = false;
01817     }
01818 
01822     protected function readOnlyFatalStatus() {
01823         return $this->getRepo()->newFatal( 'filereadonlyerror', $this->getName(),
01824             $this->getRepo()->getName(), $this->getRepo()->getReadOnlyReason() );
01825     }
01826 } // LocalFile class
01827 
01828 # ------------------------------------------------------------------------------
01829 
01834 class LocalFileDeleteBatch {
01835 
01839     var $file;
01840 
01841     var $reason, $srcRels = array(), $archiveUrls = array(), $deletionBatch, $suppress;
01842     var $status;
01843 
01849     function __construct( File $file, $reason = '', $suppress = false ) {
01850         $this->file = $file;
01851         $this->reason = $reason;
01852         $this->suppress = $suppress;
01853         $this->status = $file->repo->newGood();
01854     }
01855 
01856     function addCurrent() {
01857         $this->srcRels['.'] = $this->file->getRel();
01858     }
01859 
01863     function addOld( $oldName ) {
01864         $this->srcRels[$oldName] = $this->file->getArchiveRel( $oldName );
01865         $this->archiveUrls[] = $this->file->getArchiveUrl( $oldName );
01866     }
01867 
01872     function addOlds() {
01873         $archiveNames = array();
01874 
01875         $dbw = $this->file->repo->getMasterDB();
01876         $result = $dbw->select( 'oldimage',
01877             array( 'oi_archive_name' ),
01878             array( 'oi_name' => $this->file->getName() ),
01879             __METHOD__
01880         );
01881 
01882         foreach ( $result as $row ) {
01883             $this->addOld( $row->oi_archive_name );
01884             $archiveNames[] = $row->oi_archive_name;
01885         }
01886 
01887         return $archiveNames;
01888     }
01889 
01893     function getOldRels() {
01894         if ( !isset( $this->srcRels['.'] ) ) {
01895             $oldRels =& $this->srcRels;
01896             $deleteCurrent = false;
01897         } else {
01898             $oldRels = $this->srcRels;
01899             unset( $oldRels['.'] );
01900             $deleteCurrent = true;
01901         }
01902 
01903         return array( $oldRels, $deleteCurrent );
01904     }
01905 
01909     protected function getHashes() {
01910         $hashes = array();
01911         list( $oldRels, $deleteCurrent ) = $this->getOldRels();
01912 
01913         if ( $deleteCurrent ) {
01914             $hashes['.'] = $this->file->getSha1();
01915         }
01916 
01917         if ( count( $oldRels ) ) {
01918             $dbw = $this->file->repo->getMasterDB();
01919             $res = $dbw->select(
01920                 'oldimage',
01921                 array( 'oi_archive_name', 'oi_sha1' ),
01922                 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')',
01923                 __METHOD__
01924             );
01925 
01926             foreach ( $res as $row ) {
01927                 if ( rtrim( $row->oi_sha1, "\0" ) === '' ) {
01928                     // Get the hash from the file
01929                     $oldUrl = $this->file->getArchiveVirtualUrl( $row->oi_archive_name );
01930                     $props = $this->file->repo->getFileProps( $oldUrl );
01931 
01932                     if ( $props['fileExists'] ) {
01933                         // Upgrade the oldimage row
01934                         $dbw->update( 'oldimage',
01935                             array( 'oi_sha1' => $props['sha1'] ),
01936                             array( 'oi_name' => $this->file->getName(), 'oi_archive_name' => $row->oi_archive_name ),
01937                             __METHOD__ );
01938                         $hashes[$row->oi_archive_name] = $props['sha1'];
01939                     } else {
01940                         $hashes[$row->oi_archive_name] = false;
01941                     }
01942                 } else {
01943                     $hashes[$row->oi_archive_name] = $row->oi_sha1;
01944                 }
01945             }
01946         }
01947 
01948         $missing = array_diff_key( $this->srcRels, $hashes );
01949 
01950         foreach ( $missing as $name => $rel ) {
01951             $this->status->error( 'filedelete-old-unregistered', $name );
01952         }
01953 
01954         foreach ( $hashes as $name => $hash ) {
01955             if ( !$hash ) {
01956                 $this->status->error( 'filedelete-missing', $this->srcRels[$name] );
01957                 unset( $hashes[$name] );
01958             }
01959         }
01960 
01961         return $hashes;
01962     }
01963 
01964     function doDBInserts() {
01965         global $wgUser;
01966 
01967         $dbw = $this->file->repo->getMasterDB();
01968         $encTimestamp = $dbw->addQuotes( $dbw->timestamp() );
01969         $encUserId = $dbw->addQuotes( $wgUser->getId() );
01970         $encReason = $dbw->addQuotes( $this->reason );
01971         $encGroup = $dbw->addQuotes( 'deleted' );
01972         $ext = $this->file->getExtension();
01973         $dotExt = $ext === '' ? '' : ".$ext";
01974         $encExt = $dbw->addQuotes( $dotExt );
01975         list( $oldRels, $deleteCurrent ) = $this->getOldRels();
01976 
01977         // Bitfields to further suppress the content
01978         if ( $this->suppress ) {
01979             $bitfield = 0;
01980             // This should be 15...
01981             $bitfield |= Revision::DELETED_TEXT;
01982             $bitfield |= Revision::DELETED_COMMENT;
01983             $bitfield |= Revision::DELETED_USER;
01984             $bitfield |= Revision::DELETED_RESTRICTED;
01985         } else {
01986             $bitfield = 'oi_deleted';
01987         }
01988 
01989         if ( $deleteCurrent ) {
01990             $concat = $dbw->buildConcat( array( "img_sha1", $encExt ) );
01991             $where = array( 'img_name' => $this->file->getName() );
01992             $dbw->insertSelect( 'filearchive', 'image',
01993                 array(
01994                     'fa_storage_group' => $encGroup,
01995                     'fa_storage_key'   => "CASE WHEN img_sha1='' THEN '' ELSE $concat END",
01996                     'fa_deleted_user'      => $encUserId,
01997                     'fa_deleted_timestamp' => $encTimestamp,
01998                     'fa_deleted_reason'    => $encReason,
01999                     'fa_deleted'           => $this->suppress ? $bitfield : 0,
02000 
02001                     'fa_name'         => 'img_name',
02002                     'fa_archive_name' => 'NULL',
02003                     'fa_size'         => 'img_size',
02004                     'fa_width'        => 'img_width',
02005                     'fa_height'       => 'img_height',
02006                     'fa_metadata'     => 'img_metadata',
02007                     'fa_bits'         => 'img_bits',
02008                     'fa_media_type'   => 'img_media_type',
02009                     'fa_major_mime'   => 'img_major_mime',
02010                     'fa_minor_mime'   => 'img_minor_mime',
02011                     'fa_description'  => 'img_description',
02012                     'fa_user'         => 'img_user',
02013                     'fa_user_text'    => 'img_user_text',
02014                     'fa_timestamp'    => 'img_timestamp',
02015                     'fa_sha1'         => 'img_sha1',
02016                 ), $where, __METHOD__ );
02017         }
02018 
02019         if ( count( $oldRels ) ) {
02020             $concat = $dbw->buildConcat( array( "oi_sha1", $encExt ) );
02021             $where = array(
02022                 'oi_name' => $this->file->getName(),
02023                 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')' );
02024             $dbw->insertSelect( 'filearchive', 'oldimage',
02025                 array(
02026                     'fa_storage_group' => $encGroup,
02027                     'fa_storage_key'   => "CASE WHEN oi_sha1='' THEN '' ELSE $concat END",
02028                     'fa_deleted_user'      => $encUserId,
02029                     'fa_deleted_timestamp' => $encTimestamp,
02030                     'fa_deleted_reason'    => $encReason,
02031                     'fa_deleted'           => $this->suppress ? $bitfield : 'oi_deleted',
02032 
02033                     'fa_name'         => 'oi_name',
02034                     'fa_archive_name' => 'oi_archive_name',
02035                     'fa_size'         => 'oi_size',
02036                     'fa_width'        => 'oi_width',
02037                     'fa_height'       => 'oi_height',
02038                     'fa_metadata'     => 'oi_metadata',
02039                     'fa_bits'         => 'oi_bits',
02040                     'fa_media_type'   => 'oi_media_type',
02041                     'fa_major_mime'   => 'oi_major_mime',
02042                     'fa_minor_mime'   => 'oi_minor_mime',
02043                     'fa_description'  => 'oi_description',
02044                     'fa_user'         => 'oi_user',
02045                     'fa_user_text'    => 'oi_user_text',
02046                     'fa_timestamp'    => 'oi_timestamp',
02047                     'fa_sha1'         => 'oi_sha1',
02048                 ), $where, __METHOD__ );
02049         }
02050     }
02051 
02052     function doDBDeletes() {
02053         $dbw = $this->file->repo->getMasterDB();
02054         list( $oldRels, $deleteCurrent ) = $this->getOldRels();
02055 
02056         if ( count( $oldRels ) ) {
02057             $dbw->delete( 'oldimage',
02058                 array(
02059                     'oi_name' => $this->file->getName(),
02060                     'oi_archive_name' => array_keys( $oldRels )
02061                 ), __METHOD__ );
02062         }
02063 
02064         if ( $deleteCurrent ) {
02065             $dbw->delete( 'image', array( 'img_name' => $this->file->getName() ), __METHOD__ );
02066         }
02067     }
02068 
02073     function execute() {
02074         wfProfileIn( __METHOD__ );
02075 
02076         $this->file->lock();
02077         // Leave private files alone
02078         $privateFiles = array();
02079         list( $oldRels, ) = $this->getOldRels();
02080         $dbw = $this->file->repo->getMasterDB();
02081 
02082         if ( !empty( $oldRels ) ) {
02083             $res = $dbw->select( 'oldimage',
02084                 array( 'oi_archive_name' ),
02085                 array( 'oi_name' => $this->file->getName(),
02086                     'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')',
02087                     $dbw->bitAnd( 'oi_deleted', File::DELETED_FILE ) => File::DELETED_FILE ),
02088                 __METHOD__ );
02089 
02090             foreach ( $res as $row ) {
02091                 $privateFiles[$row->oi_archive_name] = 1;
02092             }
02093         }
02094         // Prepare deletion batch
02095         $hashes = $this->getHashes();
02096         $this->deletionBatch = array();
02097         $ext = $this->file->getExtension();
02098         $dotExt = $ext === '' ? '' : ".$ext";
02099 
02100         foreach ( $this->srcRels as $name => $srcRel ) {
02101             // Skip files that have no hash (missing source).
02102             // Keep private files where they are.
02103             if ( isset( $hashes[$name] ) && !array_key_exists( $name, $privateFiles ) ) {
02104                 $hash = $hashes[$name];
02105                 $key = $hash . $dotExt;
02106                 $dstRel = $this->file->repo->getDeletedHashPath( $key ) . $key;
02107                 $this->deletionBatch[$name] = array( $srcRel, $dstRel );
02108             }
02109         }
02110 
02111         // Lock the filearchive rows so that the files don't get deleted by a cleanup operation
02112         // We acquire this lock by running the inserts now, before the file operations.
02113         //
02114         // This potentially has poor lock contention characteristics -- an alternative
02115         // scheme would be to insert stub filearchive entries with no fa_name and commit
02116         // them in a separate transaction, then run the file ops, then update the fa_name fields.
02117         $this->doDBInserts();
02118 
02119         // Removes non-existent file from the batch, so we don't get errors.
02120         $this->deletionBatch = $this->removeNonexistentFiles( $this->deletionBatch );
02121 
02122         // Execute the file deletion batch
02123         $status = $this->file->repo->deleteBatch( $this->deletionBatch );
02124 
02125         if ( !$status->isGood() ) {
02126             $this->status->merge( $status );
02127         }
02128 
02129         if ( !$this->status->isOK() ) {
02130             // Critical file deletion error
02131             // Roll back inserts, release lock and abort
02132             // TODO: delete the defunct filearchive rows if we are using a non-transactional DB
02133             $this->file->unlockAndRollback();
02134             wfProfileOut( __METHOD__ );
02135             return $this->status;
02136         }
02137 
02138         // Delete image/oldimage rows
02139         $this->doDBDeletes();
02140 
02141         // Commit and return
02142         $this->file->unlock();
02143         wfProfileOut( __METHOD__ );
02144 
02145         return $this->status;
02146     }
02147 
02153     function removeNonexistentFiles( $batch ) {
02154         $files = $newBatch = array();
02155 
02156         foreach ( $batch as $batchItem ) {
02157             list( $src, ) = $batchItem;
02158             $files[$src] = $this->file->repo->getVirtualUrl( 'public' ) . '/' . rawurlencode( $src );
02159         }
02160 
02161         $result = $this->file->repo->fileExistsBatch( $files );
02162 
02163         foreach ( $batch as $batchItem ) {
02164             if ( $result[$batchItem[0]] ) {
02165                 $newBatch[] = $batchItem;
02166             }
02167         }
02168 
02169         return $newBatch;
02170     }
02171 }
02172 
02173 # ------------------------------------------------------------------------------
02174 
02179 class LocalFileRestoreBatch {
02183     var $file;
02184 
02185     var $cleanupBatch, $ids, $all, $unsuppress = false;
02186 
02191     function __construct( File $file, $unsuppress = false ) {
02192         $this->file = $file;
02193         $this->cleanupBatch = $this->ids = array();
02194         $this->ids = array();
02195         $this->unsuppress = $unsuppress;
02196     }
02197 
02201     function addId( $fa_id ) {
02202         $this->ids[] = $fa_id;
02203     }
02204 
02208     function addIds( $ids ) {
02209         $this->ids = array_merge( $this->ids, $ids );
02210     }
02211 
02215     function addAll() {
02216         $this->all = true;
02217     }
02218 
02227     function execute() {
02228         global $wgLang;
02229 
02230         if ( !$this->all && !$this->ids ) {
02231             // Do nothing
02232             return $this->file->repo->newGood();
02233         }
02234 
02235         $exists = $this->file->lock();
02236         $dbw = $this->file->repo->getMasterDB();
02237         $status = $this->file->repo->newGood();
02238 
02239         // Fetch all or selected archived revisions for the file,
02240         // sorted from the most recent to the oldest.
02241         $conditions = array( 'fa_name' => $this->file->getName() );
02242 
02243         if ( !$this->all ) {
02244             $conditions[] = 'fa_id IN (' . $dbw->makeList( $this->ids ) . ')';
02245         }
02246 
02247         $result = $dbw->select(
02248             'filearchive',
02249             ArchivedFile::selectFields(),
02250             $conditions,
02251             __METHOD__,
02252             array( 'ORDER BY' => 'fa_timestamp DESC' )
02253         );
02254 
02255         $idsPresent = array();
02256         $storeBatch = array();
02257         $insertBatch = array();
02258         $insertCurrent = false;
02259         $deleteIds = array();
02260         $first = true;
02261         $archiveNames = array();
02262 
02263         foreach ( $result as $row ) {
02264             $idsPresent[] = $row->fa_id;
02265 
02266             if ( $row->fa_name != $this->file->getName() ) {
02267                 $status->error( 'undelete-filename-mismatch', $wgLang->timeanddate( $row->fa_timestamp ) );
02268                 $status->failCount++;
02269                 continue;
02270             }
02271 
02272             if ( $row->fa_storage_key == '' ) {
02273                 // Revision was missing pre-deletion
02274                 $status->error( 'undelete-bad-store-key', $wgLang->timeanddate( $row->fa_timestamp ) );
02275                 $status->failCount++;
02276                 continue;
02277             }
02278 
02279             $deletedRel = $this->file->repo->getDeletedHashPath( $row->fa_storage_key ) . $row->fa_storage_key;
02280             $deletedUrl = $this->file->repo->getVirtualUrl() . '/deleted/' . $deletedRel;
02281 
02282             if ( isset( $row->fa_sha1 ) ) {
02283                 $sha1 = $row->fa_sha1;
02284             } else {
02285                 // old row, populate from key
02286                 $sha1 = LocalRepo::getHashFromKey( $row->fa_storage_key );
02287             }
02288 
02289             # Fix leading zero
02290             if ( strlen( $sha1 ) == 32 && $sha1[0] == '0' ) {
02291                 $sha1 = substr( $sha1, 1 );
02292             }
02293 
02294             if ( is_null( $row->fa_major_mime ) || $row->fa_major_mime == 'unknown'
02295                 || is_null( $row->fa_minor_mime ) || $row->fa_minor_mime == 'unknown'
02296                 || is_null( $row->fa_media_type ) || $row->fa_media_type == 'UNKNOWN'
02297                 || is_null( $row->fa_metadata ) ) {
02298                 // Refresh our metadata
02299                 // Required for a new current revision; nice for older ones too. :)
02300                 $props = RepoGroup::singleton()->getFileProps( $deletedUrl );
02301             } else {
02302                 $props = array(
02303                     'minor_mime' => $row->fa_minor_mime,
02304                     'major_mime' => $row->fa_major_mime,
02305                     'media_type' => $row->fa_media_type,
02306                     'metadata'   => $row->fa_metadata
02307                 );
02308             }
02309 
02310             if ( $first && !$exists ) {
02311                 // This revision will be published as the new current version
02312                 $destRel = $this->file->getRel();
02313                 $insertCurrent = array(
02314                     'img_name'        => $row->fa_name,
02315                     'img_size'        => $row->fa_size,
02316                     'img_width'       => $row->fa_width,
02317                     'img_height'      => $row->fa_height,
02318                     'img_metadata'    => $props['metadata'],
02319                     'img_bits'        => $row->fa_bits,
02320                     'img_media_type'  => $props['media_type'],
02321                     'img_major_mime'  => $props['major_mime'],
02322                     'img_minor_mime'  => $props['minor_mime'],
02323                     'img_description' => $row->fa_description,
02324                     'img_user'        => $row->fa_user,
02325                     'img_user_text'   => $row->fa_user_text,
02326                     'img_timestamp'   => $row->fa_timestamp,
02327                     'img_sha1'        => $sha1
02328                 );
02329 
02330                 // The live (current) version cannot be hidden!
02331                 if ( !$this->unsuppress && $row->fa_deleted ) {
02332                     $storeBatch[] = array( $deletedUrl, 'public', $destRel );
02333                     $this->cleanupBatch[] = $row->fa_storage_key;
02334                 }
02335             } else {
02336                 $archiveName = $row->fa_archive_name;
02337 
02338                 if ( $archiveName == '' ) {
02339                     // This was originally a current version; we
02340                     // have to devise a new archive name for it.
02341                     // Format is <timestamp of archiving>!<name>
02342                     $timestamp = wfTimestamp( TS_UNIX, $row->fa_deleted_timestamp );
02343 
02344                     do {
02345                         $archiveName = wfTimestamp( TS_MW, $timestamp ) . '!' . $row->fa_name;
02346                         $timestamp++;
02347                     } while ( isset( $archiveNames[$archiveName] ) );
02348                 }
02349 
02350                 $archiveNames[$archiveName] = true;
02351                 $destRel = $this->file->getArchiveRel( $archiveName );
02352                 $insertBatch[] = array(
02353                     'oi_name'         => $row->fa_name,
02354                     'oi_archive_name' => $archiveName,
02355                     'oi_size'         => $row->fa_size,
02356                     'oi_width'        => $row->fa_width,
02357                     'oi_height'       => $row->fa_height,
02358                     'oi_bits'         => $row->fa_bits,
02359                     'oi_description'  => $row->fa_description,
02360                     'oi_user'         => $row->fa_user,
02361                     'oi_user_text'    => $row->fa_user_text,
02362                     'oi_timestamp'    => $row->fa_timestamp,
02363                     'oi_metadata'     => $props['metadata'],
02364                     'oi_media_type'   => $props['media_type'],
02365                     'oi_major_mime'   => $props['major_mime'],
02366                     'oi_minor_mime'   => $props['minor_mime'],
02367                     'oi_deleted'      => $this->unsuppress ? 0 : $row->fa_deleted,
02368                     'oi_sha1'         => $sha1 );
02369             }
02370 
02371             $deleteIds[] = $row->fa_id;
02372 
02373             if ( !$this->unsuppress && $row->fa_deleted & File::DELETED_FILE ) {
02374                 // private files can stay where they are
02375                 $status->successCount++;
02376             } else {
02377                 $storeBatch[] = array( $deletedUrl, 'public', $destRel );
02378                 $this->cleanupBatch[] = $row->fa_storage_key;
02379             }
02380 
02381             $first = false;
02382         }
02383 
02384         unset( $result );
02385 
02386         // Add a warning to the status object for missing IDs
02387         $missingIds = array_diff( $this->ids, $idsPresent );
02388 
02389         foreach ( $missingIds as $id ) {
02390             $status->error( 'undelete-missing-filearchive', $id );
02391         }
02392 
02393         // Remove missing files from batch, so we don't get errors when undeleting them
02394         $storeBatch = $this->removeNonexistentFiles( $storeBatch );
02395 
02396         // Run the store batch
02397         // Use the OVERWRITE_SAME flag to smooth over a common error
02398         $storeStatus = $this->file->repo->storeBatch( $storeBatch, FileRepo::OVERWRITE_SAME );
02399         $status->merge( $storeStatus );
02400 
02401         if ( !$status->isGood() ) {
02402             // Even if some files could be copied, fail entirely as that is the
02403             // easiest thing to do without data loss
02404             $this->cleanupFailedBatch( $storeStatus, $storeBatch );
02405             $status->ok = false;
02406             $this->file->unlock();
02407 
02408             return $status;
02409         }
02410 
02411         // Run the DB updates
02412         // Because we have locked the image row, key conflicts should be rare.
02413         // If they do occur, we can roll back the transaction at this time with
02414         // no data loss, but leaving unregistered files scattered throughout the
02415         // public zone.
02416         // This is not ideal, which is why it's important to lock the image row.
02417         if ( $insertCurrent ) {
02418             $dbw->insert( 'image', $insertCurrent, __METHOD__ );
02419         }
02420 
02421         if ( $insertBatch ) {
02422             $dbw->insert( 'oldimage', $insertBatch, __METHOD__ );
02423         }
02424 
02425         if ( $deleteIds ) {
02426             $dbw->delete( 'filearchive',
02427                 array( 'fa_id IN (' . $dbw->makeList( $deleteIds ) . ')' ),
02428                 __METHOD__ );
02429         }
02430 
02431         // If store batch is empty (all files are missing), deletion is to be considered successful
02432         if ( $status->successCount > 0 || !$storeBatch ) {
02433             if ( !$exists ) {
02434                 wfDebug( __METHOD__ . " restored {$status->successCount} items, creating a new current\n" );
02435 
02436                 DeferredUpdates::addUpdate( SiteStatsUpdate::factory( array( 'images' => 1 ) ) );
02437 
02438                 $this->file->purgeEverything();
02439             } else {
02440                 wfDebug( __METHOD__ . " restored {$status->successCount} as archived versions\n" );
02441                 $this->file->purgeDescription();
02442                 $this->file->purgeHistory();
02443             }
02444         }
02445 
02446         $this->file->unlock();
02447 
02448         return $status;
02449     }
02450 
02456     function removeNonexistentFiles( $triplets ) {
02457         $files = $filteredTriplets = array();
02458         foreach ( $triplets as $file ) {
02459             $files[$file[0]] = $file[0];
02460         }
02461 
02462         $result = $this->file->repo->fileExistsBatch( $files );
02463 
02464         foreach ( $triplets as $file ) {
02465             if ( $result[$file[0]] ) {
02466                 $filteredTriplets[] = $file;
02467             }
02468         }
02469 
02470         return $filteredTriplets;
02471     }
02472 
02478     function removeNonexistentFromCleanup( $batch ) {
02479         $files = $newBatch = array();
02480         $repo = $this->file->repo;
02481 
02482         foreach ( $batch as $file ) {
02483             $files[$file] = $repo->getVirtualUrl( 'deleted' ) . '/' .
02484                 rawurlencode( $repo->getDeletedHashPath( $file ) . $file );
02485         }
02486 
02487         $result = $repo->fileExistsBatch( $files );
02488 
02489         foreach ( $batch as $file ) {
02490             if ( $result[$file] ) {
02491                 $newBatch[] = $file;
02492             }
02493         }
02494 
02495         return $newBatch;
02496     }
02497 
02503     function cleanup() {
02504         if ( !$this->cleanupBatch ) {
02505             return $this->file->repo->newGood();
02506         }
02507 
02508         $this->cleanupBatch = $this->removeNonexistentFromCleanup( $this->cleanupBatch );
02509 
02510         $status = $this->file->repo->cleanupDeletedBatch( $this->cleanupBatch );
02511 
02512         return $status;
02513     }
02514 
02522     function cleanupFailedBatch( $storeStatus, $storeBatch ) {
02523         $cleanupBatch = array();
02524 
02525         foreach ( $storeStatus->success as $i => $success ) {
02526             // Check if this item of the batch was successfully copied
02527             if ( $success ) {
02528                 // Item was successfully copied and needs to be removed again
02529                 // Extract ($dstZone, $dstRel) from the batch
02530                 $cleanupBatch[] = array( $storeBatch[$i][1], $storeBatch[$i][2] );
02531             }
02532         }
02533         $this->file->repo->cleanupBatch( $cleanupBatch );
02534     }
02535 }
02536 
02537 # ------------------------------------------------------------------------------
02538 
02543 class LocalFileMoveBatch {
02544 
02548     var $file;
02549 
02553     var $target;
02554 
02555     var $cur, $olds, $oldCount, $archive;
02556 
02560     var $db;
02561 
02566     function __construct( File $file, Title $target ) {
02567         $this->file = $file;
02568         $this->target = $target;
02569         $this->oldHash = $this->file->repo->getHashPath( $this->file->getName() );
02570         $this->newHash = $this->file->repo->getHashPath( $this->target->getDBkey() );
02571         $this->oldName = $this->file->getName();
02572         $this->newName = $this->file->repo->getNameFromTitle( $this->target );
02573         $this->oldRel = $this->oldHash . $this->oldName;
02574         $this->newRel = $this->newHash . $this->newName;
02575         $this->db = $file->getRepo()->getMasterDb();
02576     }
02577 
02581     function addCurrent() {
02582         $this->cur = array( $this->oldRel, $this->newRel );
02583     }
02584 
02589     function addOlds() {
02590         $archiveBase = 'archive';
02591         $this->olds = array();
02592         $this->oldCount = 0;
02593         $archiveNames = array();
02594 
02595         $result = $this->db->select( 'oldimage',
02596             array( 'oi_archive_name', 'oi_deleted' ),
02597             array( 'oi_name' => $this->oldName ),
02598             __METHOD__
02599         );
02600 
02601         foreach ( $result as $row ) {
02602             $archiveNames[] = $row->oi_archive_name;
02603             $oldName = $row->oi_archive_name;
02604             $bits = explode( '!', $oldName, 2 );
02605 
02606             if ( count( $bits ) != 2 ) {
02607                 wfDebug( "Old file name missing !: '$oldName' \n" );
02608                 continue;
02609             }
02610 
02611             list( $timestamp, $filename ) = $bits;
02612 
02613             if ( $this->oldName != $filename ) {
02614                 wfDebug( "Old file name doesn't match: '$oldName' \n" );
02615                 continue;
02616             }
02617 
02618             $this->oldCount++;
02619 
02620             // Do we want to add those to oldCount?
02621             if ( $row->oi_deleted & File::DELETED_FILE ) {
02622                 continue;
02623             }
02624 
02625             $this->olds[] = array(
02626                 "{$archiveBase}/{$this->oldHash}{$oldName}",
02627                 "{$archiveBase}/{$this->newHash}{$timestamp}!{$this->newName}"
02628             );
02629         }
02630 
02631         return $archiveNames;
02632     }
02633 
02638     function execute() {
02639         $repo = $this->file->repo;
02640         $status = $repo->newGood();
02641 
02642         $triplets = $this->getMoveTriplets();
02643         $triplets = $this->removeNonexistentFiles( $triplets );
02644 
02645         $this->file->lock(); // begin
02646         // Rename the file versions metadata in the DB.
02647         // This implicitly locks the destination file, which avoids race conditions.
02648         // If we moved the files from A -> C before DB updates, another process could
02649         // move files from B -> C at this point, causing storeBatch() to fail and thus
02650         // cleanupTarget() to trigger. It would delete the C files and cause data loss.
02651         $statusDb = $this->doDBUpdates();
02652         if ( !$statusDb->isGood() ) {
02653             $this->file->unlockAndRollback();
02654             $statusDb->ok = false;
02655             return $statusDb;
02656         }
02657         wfDebugLog( 'imagemove', "Renamed {$this->file->getName()} in database: {$statusDb->successCount} successes, {$statusDb->failCount} failures" );
02658 
02659         // Copy the files into their new location.
02660         // If a prior process fataled copying or cleaning up files we tolerate any
02661         // of the existing files if they are identical to the ones being stored.
02662         $statusMove = $repo->storeBatch( $triplets, FileRepo::OVERWRITE_SAME );
02663         wfDebugLog( 'imagemove', "Moved files for {$this->file->getName()}: {$statusMove->successCount} successes, {$statusMove->failCount} failures" );
02664         if ( !$statusMove->isGood() ) {
02665             // Delete any files copied over (while the destination is still locked)
02666             $this->cleanupTarget( $triplets );
02667             $this->file->unlockAndRollback(); // unlocks the destination
02668             wfDebugLog( 'imagemove', "Error in moving files: " . $statusMove->getWikiText() );
02669             $statusMove->ok = false;
02670             return $statusMove;
02671         }
02672         $this->file->unlock(); // done
02673 
02674         // Everything went ok, remove the source files
02675         $this->cleanupSource( $triplets );
02676 
02677         $status->merge( $statusDb );
02678         $status->merge( $statusMove );
02679 
02680         return $status;
02681     }
02682 
02689     function doDBUpdates() {
02690         $repo = $this->file->repo;
02691         $status = $repo->newGood();
02692         $dbw = $this->db;
02693 
02694         // Update current image
02695         $dbw->update(
02696             'image',
02697             array( 'img_name' => $this->newName ),
02698             array( 'img_name' => $this->oldName ),
02699             __METHOD__
02700         );
02701 
02702         if ( $dbw->affectedRows() ) {
02703             $status->successCount++;
02704         } else {
02705             $status->failCount++;
02706             $status->fatal( 'imageinvalidfilename' );
02707             return $status;
02708         }
02709 
02710         // Update old images
02711         $dbw->update(
02712             'oldimage',
02713             array(
02714                 'oi_name' => $this->newName,
02715                 'oi_archive_name = ' . $dbw->strreplace( 'oi_archive_name',
02716                     $dbw->addQuotes( $this->oldName ), $dbw->addQuotes( $this->newName ) ),
02717             ),
02718             array( 'oi_name' => $this->oldName ),
02719             __METHOD__
02720         );
02721 
02722         $affected = $dbw->affectedRows();
02723         $total = $this->oldCount;
02724         $status->successCount += $affected;
02725         // Bug 34934: $total is based on files that actually exist.
02726         // There may be more DB rows than such files, in which case $affected
02727         // can be greater than $total. We use max() to avoid negatives here.
02728         $status->failCount += max( 0, $total - $affected );
02729         if ( $status->failCount ) {
02730             $status->error( 'imageinvalidfilename' );
02731         }
02732 
02733         return $status;
02734     }
02735 
02740     function getMoveTriplets() {
02741         $moves = array_merge( array( $this->cur ), $this->olds );
02742         $triplets = array(); // The format is: (srcUrl, destZone, destUrl)
02743 
02744         foreach ( $moves as $move ) {
02745             // $move: (oldRelativePath, newRelativePath)
02746             $srcUrl = $this->file->repo->getVirtualUrl() . '/public/' . rawurlencode( $move[0] );
02747             $triplets[] = array( $srcUrl, 'public', $move[1] );
02748             wfDebugLog( 'imagemove', "Generated move triplet for {$this->file->getName()}: {$srcUrl} :: public :: {$move[1]}" );
02749         }
02750 
02751         return $triplets;
02752     }
02753 
02759     function removeNonexistentFiles( $triplets ) {
02760         $files = array();
02761 
02762         foreach ( $triplets as $file ) {
02763             $files[$file[0]] = $file[0];
02764         }
02765 
02766         $result = $this->file->repo->fileExistsBatch( $files );
02767         $filteredTriplets = array();
02768 
02769         foreach ( $triplets as $file ) {
02770             if ( $result[$file[0]] ) {
02771                 $filteredTriplets[] = $file;
02772             } else {
02773                 wfDebugLog( 'imagemove', "File {$file[0]} does not exist" );
02774             }
02775         }
02776 
02777         return $filteredTriplets;
02778     }
02779 
02784     function cleanupTarget( $triplets ) {
02785         // Create dest pairs from the triplets
02786         $pairs = array();
02787         foreach ( $triplets as $triplet ) {
02788             // $triplet: (old source virtual URL, dst zone, dest rel)
02789             $pairs[] = array( $triplet[1], $triplet[2] );
02790         }
02791 
02792         $this->file->repo->cleanupBatch( $pairs );
02793     }
02794 
02799     function cleanupSource( $triplets ) {
02800         // Create source file names from the triplets
02801         $files = array();
02802         foreach ( $triplets as $triplet ) {
02803             $files[] = $triplet[0];
02804         }
02805 
02806         $this->file->repo->cleanupBatch( $files );
02807     }
02808 }