MediaWiki  REL1_21
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                         throw new MWException( "Could not find data for image '{$this->getName()}'." );
00386                 }
00387 
00388                 wfProfileOut( $fname );
00389         }
00390 
00396         protected function unprefixRow( $row, $prefix = 'img_' ) {
00397                 $array = (array)$row;
00398                 $prefixLength = strlen( $prefix );
00399 
00400                 // Sanity check prefix once
00401                 if ( substr( key( $array ), 0, $prefixLength ) !== $prefix ) {
00402                         throw new MWException( __METHOD__ . ': incorrect $prefix parameter' );
00403                 }
00404 
00405                 $decoded = array();
00406                 foreach ( $array as $name => $value ) {
00407                         $decoded[substr( $name, $prefixLength )] = $value;
00408                 }
00409                 return $decoded;
00410         }
00411 
00420         function decodeRow( $row, $prefix = 'img_' ) {
00421                 $decoded = $this->unprefixRow( $row, $prefix );
00422 
00423                 $decoded['timestamp'] = wfTimestamp( TS_MW, $decoded['timestamp'] );
00424 
00425                 if ( empty( $decoded['major_mime'] ) ) {
00426                         $decoded['mime'] = 'unknown/unknown';
00427                 } else {
00428                         if ( !$decoded['minor_mime'] ) {
00429                                 $decoded['minor_mime'] = 'unknown';
00430                         }
00431                         $decoded['mime'] = $decoded['major_mime'] . '/' . $decoded['minor_mime'];
00432                 }
00433 
00434                 # Trim zero padding from char/binary field
00435                 $decoded['sha1'] = rtrim( $decoded['sha1'], "\0" );
00436 
00437                 return $decoded;
00438         }
00439 
00443         function loadFromRow( $row, $prefix = 'img_' ) {
00444                 $this->dataLoaded = true;
00445                 $this->extraDataLoaded = true;
00446 
00447                 $array = $this->decodeRow( $row, $prefix );
00448 
00449                 foreach ( $array as $name => $value ) {
00450                         $this->$name = $value;
00451                 }
00452 
00453                 $this->fileExists = true;
00454                 $this->maybeUpgradeRow();
00455         }
00456 
00461         function load( $flags = 0 ) {
00462                 if ( !$this->dataLoaded ) {
00463                         if ( !$this->loadFromCache() ) {
00464                                 $this->loadFromDB();
00465                                 $this->saveToCache();
00466                         }
00467                         $this->dataLoaded = true;
00468                 }
00469                 if ( ( $flags & self::LOAD_ALL ) && !$this->extraDataLoaded ) {
00470                         $this->loadExtraFromDB();
00471                 }
00472         }
00473 
00477         function maybeUpgradeRow() {
00478                 global $wgUpdateCompatibleMetadata;
00479                 if ( wfReadOnly() ) {
00480                         return;
00481                 }
00482 
00483                 if ( is_null( $this->media_type ) ||
00484                         $this->mime == 'image/svg'
00485                 ) {
00486                         $this->upgradeRow();
00487                         $this->upgraded = true;
00488                 } else {
00489                         $handler = $this->getHandler();
00490                         if ( $handler ) {
00491                                 $validity = $handler->isMetadataValid( $this, $this->getMetadata() );
00492                                 if ( $validity === MediaHandler::METADATA_BAD
00493                                         || ( $validity === MediaHandler::METADATA_COMPATIBLE && $wgUpdateCompatibleMetadata )
00494                                 ) {
00495                                         $this->upgradeRow();
00496                                         $this->upgraded = true;
00497                                 }
00498                         }
00499                 }
00500         }
00501 
00502         function getUpgraded() {
00503                 return $this->upgraded;
00504         }
00505 
00509         function upgradeRow() {
00510                 wfProfileIn( __METHOD__ );
00511 
00512                 $this->lock(); // begin
00513 
00514                 $this->loadFromFile();
00515 
00516                 # Don't destroy file info of missing files
00517                 if ( !$this->fileExists ) {
00518                         wfDebug( __METHOD__ . ": file does not exist, aborting\n" );
00519                         wfProfileOut( __METHOD__ );
00520                         return;
00521                 }
00522 
00523                 $dbw = $this->repo->getMasterDB();
00524                 list( $major, $minor ) = self::splitMime( $this->mime );
00525 
00526                 if ( wfReadOnly() ) {
00527                         wfProfileOut( __METHOD__ );
00528                         return;
00529                 }
00530                 wfDebug( __METHOD__ . ': upgrading ' . $this->getName() . " to the current schema\n" );
00531 
00532                 $dbw->update( 'image',
00533                         array(
00534                                 'img_size'       => $this->size, // sanity
00535                                 'img_width'      => $this->width,
00536                                 'img_height'     => $this->height,
00537                                 'img_bits'       => $this->bits,
00538                                 'img_media_type' => $this->media_type,
00539                                 'img_major_mime' => $major,
00540                                 'img_minor_mime' => $minor,
00541                                 'img_metadata'   => $this->metadata,
00542                                 'img_sha1'       => $this->sha1,
00543                         ),
00544                         array( 'img_name' => $this->getName() ),
00545                         __METHOD__
00546                 );
00547 
00548                 $this->saveToCache();
00549 
00550                 $this->unlock(); // done
00551 
00552                 wfProfileOut( __METHOD__ );
00553         }
00554 
00563         function setProps( $info ) {
00564                 $this->dataLoaded = true;
00565                 $fields = $this->getCacheFields( '' );
00566                 $fields[] = 'fileExists';
00567 
00568                 foreach ( $fields as $field ) {
00569                         if ( isset( $info[$field] ) ) {
00570                                 $this->$field = $info[$field];
00571                         }
00572                 }
00573 
00574                 // Fix up mime fields
00575                 if ( isset( $info['major_mime'] ) ) {
00576                         $this->mime = "{$info['major_mime']}/{$info['minor_mime']}";
00577                 } elseif ( isset( $info['mime'] ) ) {
00578                         $this->mime = $info['mime'];
00579                         list( $this->major_mime, $this->minor_mime ) = self::splitMime( $this->mime );
00580                 }
00581         }
00582 
00594         function isMissing() {
00595                 if ( $this->missing === null ) {
00596                         list( $fileExists ) = $this->repo->fileExists( $this->getVirtualUrl() );
00597                         $this->missing = !$fileExists;
00598                 }
00599                 return $this->missing;
00600         }
00601 
00608         public function getWidth( $page = 1 ) {
00609                 $this->load();
00610 
00611                 if ( $this->isMultipage() ) {
00612                         $dim = $this->getHandler()->getPageDimensions( $this, $page );
00613                         if ( $dim ) {
00614                                 return $dim['width'];
00615                         } else {
00616                                 return false;
00617                         }
00618                 } else {
00619                         return $this->width;
00620                 }
00621         }
00622 
00629         public function getHeight( $page = 1 ) {
00630                 $this->load();
00631 
00632                 if ( $this->isMultipage() ) {
00633                         $dim = $this->getHandler()->getPageDimensions( $this, $page );
00634                         if ( $dim ) {
00635                                 return $dim['height'];
00636                         } else {
00637                                 return false;
00638                         }
00639                 } else {
00640                         return $this->height;
00641                 }
00642         }
00643 
00650         function getUser( $type = 'text' ) {
00651                 $this->load();
00652 
00653                 if ( $type == 'text' ) {
00654                         return $this->user_text;
00655                 } elseif ( $type == 'id' ) {
00656                         return $this->user;
00657                 }
00658         }
00659 
00664         function getMetadata() {
00665                 $this->load( self::LOAD_ALL ); // large metadata is loaded in another step
00666                 return $this->metadata;
00667         }
00668 
00672         function getBitDepth() {
00673                 $this->load();
00674                 return $this->bits;
00675         }
00676 
00681         public function getSize() {
00682                 $this->load();
00683                 return $this->size;
00684         }
00685 
00690         function getMimeType() {
00691                 $this->load();
00692                 return $this->mime;
00693         }
00694 
00700         function getMediaType() {
00701                 $this->load();
00702                 return $this->media_type;
00703         }
00704 
00715         public function exists() {
00716                 $this->load();
00717                 return $this->fileExists;
00718         }
00719 
00732         function migrateThumbFile( $thumbName ) {
00733                 /* Old code for bug 2532
00734                 $thumbDir = $this->getThumbPath();
00735                 $thumbPath = "$thumbDir/$thumbName";
00736                 if ( is_dir( $thumbPath ) ) {
00737                         // Directory where file should be
00738                         // This happened occasionally due to broken migration code in 1.5
00739                         // Rename to broken-*
00740                         for ( $i = 0; $i < 100; $i++ ) {
00741                                 $broken = $this->repo->getZonePath( 'public' ) . "/broken-$i-$thumbName";
00742                                 if ( !file_exists( $broken ) ) {
00743                                         rename( $thumbPath, $broken );
00744                                         break;
00745                                 }
00746                         }
00747                         // Doesn't exist anymore
00748                         clearstatcache();
00749                 }
00750                 */
00751 
00752                 /*
00753                 if ( $this->repo->fileExists( $thumbDir ) ) {
00754                         // Delete file where directory should be
00755                         $this->repo->cleanupBatch( array( $thumbDir ) );
00756                 }
00757                 */
00758         }
00759 
00769         function getThumbnails( $archiveName = false ) {
00770                 if ( $archiveName ) {
00771                         $dir = $this->getArchiveThumbPath( $archiveName );
00772                 } else {
00773                         $dir = $this->getThumbPath();
00774                 }
00775 
00776                 $backend = $this->repo->getBackend();
00777                 $files = array( $dir );
00778                 $iterator = $backend->getFileList( array( 'dir' => $dir ) );
00779                 foreach ( $iterator as $file ) {
00780                         $files[] = $file;
00781                 }
00782 
00783                 return $files;
00784         }
00785 
00789         function purgeMetadataCache() {
00790                 $this->loadFromDB();
00791                 $this->saveToCache();
00792                 $this->purgeHistory();
00793         }
00794 
00798         function purgeHistory() {
00799                 global $wgMemc;
00800 
00801                 $hashedName = md5( $this->getName() );
00802                 $oldKey = $this->repo->getSharedCacheKey( 'oldfile', $hashedName );
00803 
00804                 // Must purge thumbnails for old versions too! bug 30192
00805                 foreach( $this->getHistory() as $oldFile ) {
00806                         $oldFile->purgeThumbnails();
00807                 }
00808 
00809                 if ( $oldKey ) {
00810                         $wgMemc->delete( $oldKey );
00811                 }
00812         }
00813 
00817         function purgeCache( $options = array() ) {
00818                 // Refresh metadata cache
00819                 $this->purgeMetadataCache();
00820 
00821                 // Delete thumbnails
00822                 $this->purgeThumbnails( $options );
00823 
00824                 // Purge squid cache for this file
00825                 SquidUpdate::purge( array( $this->getURL() ) );
00826         }
00827 
00832         function purgeOldThumbnails( $archiveName ) {
00833                 global $wgUseSquid;
00834                 wfProfileIn( __METHOD__ );
00835 
00836                 // Get a list of old thumbnails and URLs
00837                 $files = $this->getThumbnails( $archiveName );
00838                 $dir = array_shift( $files );
00839                 $this->purgeThumbList( $dir, $files );
00840 
00841                 // Purge any custom thumbnail caches
00842                 wfRunHooks( 'LocalFilePurgeThumbnails', array( $this, $archiveName ) );
00843 
00844                 // Purge the squid
00845                 if ( $wgUseSquid ) {
00846                         $urls = array();
00847                         foreach( $files as $file ) {
00848                                 $urls[] = $this->getArchiveThumbUrl( $archiveName, $file );
00849                         }
00850                         SquidUpdate::purge( $urls );
00851                 }
00852 
00853                 wfProfileOut( __METHOD__ );
00854         }
00855 
00859         function purgeThumbnails( $options = array() ) {
00860                 global $wgUseSquid;
00861                 wfProfileIn( __METHOD__ );
00862 
00863                 // Delete thumbnails
00864                 $files = $this->getThumbnails();
00865                 // Always purge all files from squid regardless of handler filters
00866                 if ( $wgUseSquid ) {
00867                         $urls = array();
00868                         foreach( $files as $file ) {
00869                                 $urls[] = $this->getThumbUrl( $file );
00870                         }
00871                         array_shift( $urls ); // don't purge directory
00872                 }
00873 
00874                 // Give media handler a chance to filter the file purge list
00875                 if ( !empty( $options['forThumbRefresh'] ) ) {
00876                         $handler = $this->getHandler();
00877                         if ( $handler ) {
00878                                 $handler->filterThumbnailPurgeList( $files, $options );
00879                         }
00880                 }
00881 
00882                 $dir = array_shift( $files );
00883                 $this->purgeThumbList( $dir, $files );
00884 
00885                 // Purge any custom thumbnail caches
00886                 wfRunHooks( 'LocalFilePurgeThumbnails', array( $this, false ) );
00887 
00888                 // Purge the squid
00889                 if ( $wgUseSquid ) {
00890                         SquidUpdate::purge( $urls );
00891                 }
00892 
00893                 wfProfileOut( __METHOD__ );
00894         }
00895 
00901         protected function purgeThumbList( $dir, $files ) {
00902                 $fileListDebug = strtr(
00903                         var_export( $files, true ),
00904                         array( "\n" => '' )
00905                 );
00906                 wfDebug( __METHOD__ . ": $fileListDebug\n" );
00907 
00908                 $purgeList = array();
00909                 foreach ( $files as $file ) {
00910                         # Check that the base file name is part of the thumb name
00911                         # This is a basic sanity check to avoid erasing unrelated directories
00912                         if ( strpos( $file, $this->getName() ) !== false
00913                                 || strpos( $file, "-thumbnail" ) !== false // "short" thumb name
00914                         ) {
00915                                 $purgeList[] = "{$dir}/{$file}";
00916                         }
00917                 }
00918 
00919                 # Delete the thumbnails
00920                 $this->repo->quickPurgeBatch( $purgeList );
00921                 # Clear out the thumbnail directory if empty
00922                 $this->repo->quickCleanDir( $dir );
00923         }
00924 
00935         function getHistory( $limit = null, $start = null, $end = null, $inc = true ) {
00936                 $dbr = $this->repo->getSlaveDB();
00937                 $tables = array( 'oldimage' );
00938                 $fields = OldLocalFile::selectFields();
00939                 $conds = $opts = $join_conds = array();
00940                 $eq = $inc ? '=' : '';
00941                 $conds[] = "oi_name = " . $dbr->addQuotes( $this->title->getDBkey() );
00942 
00943                 if ( $start ) {
00944                         $conds[] = "oi_timestamp <$eq " . $dbr->addQuotes( $dbr->timestamp( $start ) );
00945                 }
00946 
00947                 if ( $end ) {
00948                         $conds[] = "oi_timestamp >$eq " . $dbr->addQuotes( $dbr->timestamp( $end ) );
00949                 }
00950 
00951                 if ( $limit ) {
00952                         $opts['LIMIT'] = $limit;
00953                 }
00954 
00955                 // Search backwards for time > x queries
00956                 $order = ( !$start && $end !== null ) ? 'ASC' : 'DESC';
00957                 $opts['ORDER BY'] = "oi_timestamp $order";
00958                 $opts['USE INDEX'] = array( 'oldimage' => 'oi_name_timestamp' );
00959 
00960                 wfRunHooks( 'LocalFile::getHistory', array( &$this, &$tables, &$fields,
00961                         &$conds, &$opts, &$join_conds ) );
00962 
00963                 $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $opts, $join_conds );
00964                 $r = array();
00965 
00966                 foreach ( $res as $row ) {
00967                         if ( $this->repo->oldFileFromRowFactory ) {
00968                                 $r[] = call_user_func( $this->repo->oldFileFromRowFactory, $row, $this->repo );
00969                         } else {
00970                                 $r[] = OldLocalFile::newFromRow( $row, $this->repo );
00971                         }
00972                 }
00973 
00974                 if ( $order == 'ASC' ) {
00975                         $r = array_reverse( $r ); // make sure it ends up descending
00976                 }
00977 
00978                 return $r;
00979         }
00980 
00990         public function nextHistoryLine() {
00991                 # Polymorphic function name to distinguish foreign and local fetches
00992                 $fname = get_class( $this ) . '::' . __FUNCTION__;
00993 
00994                 $dbr = $this->repo->getSlaveDB();
00995 
00996                 if ( $this->historyLine == 0 ) {// called for the first time, return line from cur
00997                         $this->historyRes = $dbr->select( 'image',
00998                                 array(
00999                                         '*',
01000                                         "'' AS oi_archive_name",
01001                                         '0 as oi_deleted',
01002                                         'img_sha1'
01003                                 ),
01004                                 array( 'img_name' => $this->title->getDBkey() ),
01005                                 $fname
01006                         );
01007 
01008                         if ( 0 == $dbr->numRows( $this->historyRes ) ) {
01009                                 $this->historyRes = null;
01010                                 return false;
01011                         }
01012                 } elseif ( $this->historyLine == 1 ) {
01013                         $this->historyRes = $dbr->select( 'oldimage', '*',
01014                                 array( 'oi_name' => $this->title->getDBkey() ),
01015                                 $fname,
01016                                 array( 'ORDER BY' => 'oi_timestamp DESC' )
01017                         );
01018                 }
01019                 $this->historyLine ++;
01020 
01021                 return $dbr->fetchObject( $this->historyRes );
01022         }
01023 
01027         public function resetHistory() {
01028                 $this->historyLine = 0;
01029 
01030                 if ( !is_null( $this->historyRes ) ) {
01031                         $this->historyRes = null;
01032                 }
01033         }
01034 
01063         function upload( $srcPath, $comment, $pageText, $flags = 0, $props = false, $timestamp = false, $user = null ) {
01064                 global $wgContLang;
01065 
01066                 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
01067                         return $this->readOnlyFatalStatus();
01068                 }
01069 
01070                 if ( !$props ) {
01071                         wfProfileIn( __METHOD__ . '-getProps' );
01072                         if ( $this->repo->isVirtualUrl( $srcPath )
01073                                 || FileBackend::isStoragePath( $srcPath ) )
01074                         {
01075                                 $props = $this->repo->getFileProps( $srcPath );
01076                         } else {
01077                                 $props = FSFile::getPropsFromPath( $srcPath );
01078                         }
01079                         wfProfileOut( __METHOD__ . '-getProps' );
01080                 }
01081 
01082                 $options = array();
01083                 $handler = MediaHandler::getHandler( $props['mime'] );
01084                 if ( $handler ) {
01085                         $options['headers'] = $handler->getStreamHeaders( $props['metadata'] );
01086                 } else {
01087                         $options['headers'] = array();
01088                 }
01089 
01090                 // Trim spaces on user supplied text
01091                 $comment = trim( $comment );
01092 
01093                 // truncate nicely or the DB will do it for us
01094                 // non-nicely (dangling multi-byte chars, non-truncated version in cache).
01095                 $comment = $wgContLang->truncate( $comment, 255 );
01096                 $this->lock(); // begin
01097                 $status = $this->publish( $srcPath, $flags, $options );
01098 
01099                 if ( $status->successCount > 0 ) {
01100                         # Essentially we are displacing any existing current file and saving
01101                         # a new current file at the old location. If just the first succeeded,
01102                         # we still need to displace the current DB entry and put in a new one.
01103                         if ( !$this->recordUpload2( $status->value, $comment, $pageText, $props, $timestamp, $user ) ) {
01104                                 $status->fatal( 'filenotfound', $srcPath );
01105                         }
01106                 }
01107 
01108                 $this->unlock(); // done
01109 
01110                 return $status;
01111         }
01112 
01125         function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
01126                 $watch = false, $timestamp = false, User $user = null )
01127         {
01128                 if ( !$user ) {
01129                         global $wgUser;
01130                         $user = $wgUser;
01131                 }
01132 
01133                 $pageText = SpecialUpload::getInitialPageText( $desc, $license, $copyStatus, $source );
01134 
01135                 if ( !$this->recordUpload2( $oldver, $desc, $pageText, false, $timestamp, $user ) ) {
01136                         return false;
01137                 }
01138 
01139                 if ( $watch ) {
01140                         $user->addWatch( $this->getTitle() );
01141                 }
01142                 return true;
01143         }
01144 
01155         function recordUpload2(
01156                 $oldver, $comment, $pageText, $props = false, $timestamp = false, $user = null
01157         ) {
01158                 wfProfileIn( __METHOD__ );
01159 
01160                 if ( is_null( $user ) ) {
01161                         global $wgUser;
01162                         $user = $wgUser;
01163                 }
01164 
01165                 $dbw = $this->repo->getMasterDB();
01166                 $dbw->begin( __METHOD__ );
01167 
01168                 if ( !$props ) {
01169                         wfProfileIn( __METHOD__ . '-getProps' );
01170                         $props = $this->repo->getFileProps( $this->getVirtualUrl() );
01171                         wfProfileOut( __METHOD__ . '-getProps' );
01172                 }
01173 
01174                 if ( $timestamp === false ) {
01175                         $timestamp = $dbw->timestamp();
01176                 }
01177 
01178                 $props['description'] = $comment;
01179                 $props['user'] = $user->getId();
01180                 $props['user_text'] = $user->getName();
01181                 $props['timestamp'] = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
01182                 $this->setProps( $props );
01183 
01184                 # Fail now if the file isn't there
01185                 if ( !$this->fileExists ) {
01186                         wfDebug( __METHOD__ . ": File " . $this->getRel() . " went missing!\n" );
01187                         wfProfileOut( __METHOD__ );
01188                         return false;
01189                 }
01190 
01191                 $reupload = false;
01192 
01193                 # Test to see if the row exists using INSERT IGNORE
01194                 # This avoids race conditions by locking the row until the commit, and also
01195                 # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
01196                 $dbw->insert( 'image',
01197                         array(
01198                                 'img_name'        => $this->getName(),
01199                                 'img_size'        => $this->size,
01200                                 'img_width'       => intval( $this->width ),
01201                                 'img_height'      => intval( $this->height ),
01202                                 'img_bits'        => $this->bits,
01203                                 'img_media_type'  => $this->media_type,
01204                                 'img_major_mime'  => $this->major_mime,
01205                                 'img_minor_mime'  => $this->minor_mime,
01206                                 'img_timestamp'   => $timestamp,
01207                                 'img_description' => $comment,
01208                                 'img_user'        => $user->getId(),
01209                                 'img_user_text'   => $user->getName(),
01210                                 'img_metadata'    => $this->metadata,
01211                                 'img_sha1'        => $this->sha1
01212                         ),
01213                         __METHOD__,
01214                         'IGNORE'
01215                 );
01216                 if ( $dbw->affectedRows() == 0 ) {
01217                         # (bug 34993) Note: $oldver can be empty here, if the previous
01218                         # version of the file was broken. Allow registration of the new
01219                         # version to continue anyway, because that's better than having
01220                         # an image that's not fixable by user operations.
01221 
01222                         $reupload = true;
01223                         # Collision, this is an update of a file
01224                         # Insert previous contents into oldimage
01225                         $dbw->insertSelect( 'oldimage', 'image',
01226                                 array(
01227                                         'oi_name'         => 'img_name',
01228                                         'oi_archive_name' => $dbw->addQuotes( $oldver ),
01229                                         'oi_size'         => 'img_size',
01230                                         'oi_width'        => 'img_width',
01231                                         'oi_height'       => 'img_height',
01232                                         'oi_bits'         => 'img_bits',
01233                                         'oi_timestamp'    => 'img_timestamp',
01234                                         'oi_description'  => 'img_description',
01235                                         'oi_user'         => 'img_user',
01236                                         'oi_user_text'    => 'img_user_text',
01237                                         'oi_metadata'     => 'img_metadata',
01238                                         'oi_media_type'   => 'img_media_type',
01239                                         'oi_major_mime'   => 'img_major_mime',
01240                                         'oi_minor_mime'   => 'img_minor_mime',
01241                                         'oi_sha1'         => 'img_sha1'
01242                                 ),
01243                                 array( 'img_name' => $this->getName() ),
01244                                 __METHOD__
01245                         );
01246 
01247                         # Update the current image row
01248                         $dbw->update( 'image',
01249                                 array( /* SET */
01250                                         'img_size'        => $this->size,
01251                                         'img_width'       => intval( $this->width ),
01252                                         'img_height'      => intval( $this->height ),
01253                                         'img_bits'        => $this->bits,
01254                                         'img_media_type'  => $this->media_type,
01255                                         'img_major_mime'  => $this->major_mime,
01256                                         'img_minor_mime'  => $this->minor_mime,
01257                                         'img_timestamp'   => $timestamp,
01258                                         'img_description' => $comment,
01259                                         'img_user'        => $user->getId(),
01260                                         'img_user_text'   => $user->getName(),
01261                                         'img_metadata'    => $this->metadata,
01262                                         'img_sha1'        => $this->sha1
01263                                 ),
01264                                 array( 'img_name' => $this->getName() ),
01265                                 __METHOD__
01266                         );
01267                 } else {
01268                         # This is a new file, so update the image count
01269                         DeferredUpdates::addUpdate( SiteStatsUpdate::factory( array( 'images' => 1 ) ) );
01270                 }
01271 
01272                 $descTitle = $this->getTitle();
01273                 $wikiPage = new WikiFilePage( $descTitle );
01274                 $wikiPage->setFile( $this );
01275 
01276                 # Add the log entry
01277                 $log = new LogPage( 'upload' );
01278                 $action = $reupload ? 'overwrite' : 'upload';
01279                 $logId = $log->addEntry( $action, $descTitle, $comment, array(), $user );
01280 
01281                 wfProfileIn( __METHOD__ . '-edit' );
01282                 $exists = $descTitle->exists();
01283 
01284                 if ( $exists ) {
01285                         # Create a null revision
01286                         $latest = $descTitle->getLatestRevID();
01287                         $nullRevision = Revision::newNullRevision(
01288                                 $dbw,
01289                                 $descTitle->getArticleID(),
01290                                 $log->getRcComment(),
01291                                 false
01292                         );
01293                         if ( !is_null( $nullRevision ) ) {
01294                                 $nullRevision->insertOn( $dbw );
01295 
01296                                 wfRunHooks( 'NewRevisionFromEditComplete', array( $wikiPage, $nullRevision, $latest, $user ) );
01297                                 $wikiPage->updateRevisionOn( $dbw, $nullRevision );
01298                         }
01299                 }
01300 
01301                 # Commit the transaction now, in case something goes wrong later
01302                 # The most important thing is that files don't get lost, especially archives
01303                 # NOTE: once we have support for nested transactions, the commit may be moved
01304                 #       to after $wikiPage->doEdit has been called.
01305                 $dbw->commit( __METHOD__ );
01306 
01307                 if ( $exists ) {
01308                         # Invalidate the cache for the description page
01309                         $descTitle->invalidateCache();
01310                         $descTitle->purgeSquid();
01311                 } else {
01312                         # New file; create the description page.
01313                         # There's already a log entry, so don't make a second RC entry
01314                         # Squid and file cache for the description page are purged by doEditContent.
01315                         $content = ContentHandler::makeContent( $pageText, $descTitle );
01316                         $status = $wikiPage->doEditContent( $content, $comment, EDIT_NEW | EDIT_SUPPRESS_RC, false, $user );
01317 
01318                         if ( isset( $status->value['revision'] ) ) { // XXX; doEdit() uses a transaction
01319                                 $dbw->begin( __METHOD__ );
01320                                 $dbw->update( 'logging',
01321                                         array( 'log_page' => $status->value['revision']->getPage() ),
01322                                         array( 'log_id' => $logId ),
01323                                         __METHOD__
01324                                 );
01325                                 $dbw->commit( __METHOD__ ); // commit before anything bad can happen
01326                         }
01327                 }
01328                 wfProfileOut( __METHOD__ . '-edit' );
01329 
01330                 # Save to cache and purge the squid
01331                 # We shall not saveToCache before the commit since otherwise
01332                 # in case of a rollback there is an usable file from memcached
01333                 # which in fact doesn't really exist (bug 24978)
01334                 $this->saveToCache();
01335 
01336                 if ( $reupload ) {
01337                         # Delete old thumbnails
01338                         wfProfileIn( __METHOD__ . '-purge' );
01339                         $this->purgeThumbnails();
01340                         wfProfileOut( __METHOD__ . '-purge' );
01341 
01342                         # Remove the old file from the squid cache
01343                         SquidUpdate::purge( array( $this->getURL() ) );
01344                 }
01345 
01346                 # Hooks, hooks, the magic of hooks...
01347                 wfProfileIn( __METHOD__ . '-hooks' );
01348                 wfRunHooks( 'FileUpload', array( $this, $reupload, $descTitle->exists() ) );
01349                 wfProfileOut( __METHOD__ . '-hooks' );
01350 
01351                 # Invalidate cache for all pages using this file
01352                 $update = new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' );
01353                 $update->doUpdate();
01354 
01355                 # Invalidate cache for all pages that redirects on this page
01356                 $redirs = $this->getTitle()->getRedirectsHere();
01357 
01358                 foreach ( $redirs as $redir ) {
01359                         $update = new HTMLCacheUpdate( $redir, 'imagelinks' );
01360                         $update->doUpdate();
01361                 }
01362 
01363                 wfProfileOut( __METHOD__ );
01364                 return true;
01365         }
01366 
01382         function publish( $srcPath, $flags = 0, array $options = array() ) {
01383                 return $this->publishTo( $srcPath, $this->getRel(), $flags, $options );
01384         }
01385 
01401         function publishTo( $srcPath, $dstRel, $flags = 0, array $options = array() ) {
01402                 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
01403                         return $this->readOnlyFatalStatus();
01404                 }
01405 
01406                 $this->lock(); // begin
01407 
01408                 $archiveName = wfTimestamp( TS_MW ) . '!'. $this->getName();
01409                 $archiveRel = 'archive/' . $this->getHashPath() . $archiveName;
01410                 $flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0;
01411                 $status = $this->repo->publish( $srcPath, $dstRel, $archiveRel, $flags, $options );
01412 
01413                 if ( $status->value == 'new' ) {
01414                         $status->value = '';
01415                 } else {
01416                         $status->value = $archiveName;
01417                 }
01418 
01419                 $this->unlock(); // done
01420 
01421                 return $status;
01422         }
01423 
01441         function move( $target ) {
01442                 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
01443                         return $this->readOnlyFatalStatus();
01444                 }
01445 
01446                 wfDebugLog( 'imagemove', "Got request to move {$this->name} to " . $target->getText() );
01447                 $batch = new LocalFileMoveBatch( $this, $target );
01448 
01449                 $this->lock(); // begin
01450                 $batch->addCurrent();
01451                 $archiveNames = $batch->addOlds();
01452                 $status = $batch->execute();
01453                 $this->unlock(); // done
01454 
01455                 wfDebugLog( 'imagemove', "Finished moving {$this->name}" );
01456 
01457                 $this->purgeEverything();
01458                 foreach ( $archiveNames as $archiveName ) {
01459                         $this->purgeOldThumbnails( $archiveName );
01460                 }
01461                 if ( $status->isOK() ) {
01462                         // Now switch the object
01463                         $this->title = $target;
01464                         // Force regeneration of the name and hashpath
01465                         unset( $this->name );
01466                         unset( $this->hashPath );
01467                         // Purge the new image
01468                         $this->purgeEverything();
01469                 }
01470 
01471                 return $status;
01472         }
01473 
01486         function delete( $reason, $suppress = false ) {
01487                 global $wgUseSquid;
01488                 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
01489                         return $this->readOnlyFatalStatus();
01490                 }
01491 
01492                 $batch = new LocalFileDeleteBatch( $this, $reason, $suppress );
01493 
01494                 $this->lock(); // begin
01495                 $batch->addCurrent();
01496                 # Get old version relative paths
01497                 $archiveNames = $batch->addOlds();
01498                 $status = $batch->execute();
01499                 $this->unlock(); // done
01500 
01501                 if ( $status->isOK() ) {
01502                         DeferredUpdates::addUpdate( SiteStatsUpdate::factory( array( 'images' => -1 ) ) );
01503                 }
01504 
01505                 $this->purgeEverything();
01506                 foreach ( $archiveNames as $archiveName ) {
01507                         $this->purgeOldThumbnails( $archiveName );
01508                 }
01509 
01510                 if ( $wgUseSquid ) {
01511                         // Purge the squid
01512                         $purgeUrls = array();
01513                         foreach ($archiveNames as $archiveName ) {
01514                                 $purgeUrls[] = $this->getArchiveUrl( $archiveName );
01515                         }
01516                         SquidUpdate::purge( $purgeUrls );
01517                 }
01518 
01519                 return $status;
01520         }
01521 
01536         function deleteOld( $archiveName, $reason, $suppress = false ) {
01537                 global $wgUseSquid;
01538                 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
01539                         return $this->readOnlyFatalStatus();
01540                 }
01541 
01542                 $batch = new LocalFileDeleteBatch( $this, $reason, $suppress );
01543 
01544                 $this->lock(); // begin
01545                 $batch->addOld( $archiveName );
01546                 $status = $batch->execute();
01547                 $this->unlock(); // done
01548 
01549                 $this->purgeOldThumbnails( $archiveName );
01550                 if ( $status->isOK() ) {
01551                         $this->purgeDescription();
01552                         $this->purgeHistory();
01553                 }
01554 
01555                 if ( $wgUseSquid ) {
01556                         // Purge the squid
01557                         SquidUpdate::purge( array( $this->getArchiveUrl( $archiveName ) ) );
01558                 }
01559 
01560                 return $status;
01561         }
01562 
01574         function restore( $versions = array(), $unsuppress = false ) {
01575                 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
01576                         return $this->readOnlyFatalStatus();
01577                 }
01578 
01579                 $batch = new LocalFileRestoreBatch( $this, $unsuppress );
01580 
01581                 $this->lock(); // begin
01582                 if ( !$versions ) {
01583                         $batch->addAll();
01584                 } else {
01585                         $batch->addIds( $versions );
01586                 }
01587                 $status = $batch->execute();
01588                 if ( $status->isGood() ) {
01589                         $cleanupStatus = $batch->cleanup();
01590                         $cleanupStatus->successCount = 0;
01591                         $cleanupStatus->failCount = 0;
01592                         $status->merge( $cleanupStatus );
01593                 }
01594                 $this->unlock(); // done
01595 
01596                 return $status;
01597         }
01598 
01608         function getDescriptionUrl() {
01609                 return $this->title->getLocalUrl();
01610         }
01611 
01618         function getDescriptionText() {
01619                 $revision = Revision::newFromTitle( $this->title, false, Revision::READ_NORMAL );
01620                 if ( !$revision ) return false;
01621                 $content = $revision->getContent();
01622                 if ( !$content ) return false;
01623                 $pout = $content->getParserOutput( $this->title, null, new ParserOptions() );
01624                 return $pout->getText();
01625         }
01626 
01630         function getDescription( $audience = self::FOR_PUBLIC, User $user = null ) {
01631                 $this->load();
01632                 if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
01633                         return '';
01634                 } elseif ( $audience == self::FOR_THIS_USER
01635                         && !$this->userCan( self::DELETED_COMMENT, $user ) )
01636                 {
01637                         return '';
01638                 } else {
01639                         return $this->description;
01640                 }
01641         }
01642 
01646         function getTimestamp() {
01647                 $this->load();
01648                 return $this->timestamp;
01649         }
01650 
01654         function getSha1() {
01655                 $this->load();
01656                 // Initialise now if necessary
01657                 if ( $this->sha1 == '' && $this->fileExists ) {
01658                         $this->lock(); // begin
01659 
01660                         $this->sha1 = $this->repo->getFileSha1( $this->getPath() );
01661                         if ( !wfReadOnly() && strval( $this->sha1 ) != '' ) {
01662                                 $dbw = $this->repo->getMasterDB();
01663                                 $dbw->update( 'image',
01664                                         array( 'img_sha1' => $this->sha1 ),
01665                                         array( 'img_name' => $this->getName() ),
01666                                         __METHOD__ );
01667                                 $this->saveToCache();
01668                         }
01669 
01670                         $this->unlock(); // done
01671                 }
01672 
01673                 return $this->sha1;
01674         }
01675 
01679         function isCacheable() {
01680                 $this->load();
01681                 return strlen( $this->metadata ) <= self::CACHE_FIELD_MAX_LEN; // avoid OOMs
01682         }
01683 
01689         function lock() {
01690                 $dbw = $this->repo->getMasterDB();
01691 
01692                 if ( !$this->locked ) {
01693                         if ( !$dbw->trxLevel() ) {
01694                                 $dbw->begin( __METHOD__ );
01695                                 $this->lockedOwnTrx = true;
01696                         }
01697                         $this->locked++;
01698                 }
01699 
01700                 return $dbw->selectField( 'image', '1',
01701                         array( 'img_name' => $this->getName() ), __METHOD__, array( 'FOR UPDATE' ) );
01702         }
01703 
01708         function unlock() {
01709                 if ( $this->locked ) {
01710                         --$this->locked;
01711                         if ( !$this->locked && $this->lockedOwnTrx ) {
01712                                 $dbw = $this->repo->getMasterDB();
01713                                 $dbw->commit( __METHOD__ );
01714                                 $this->lockedOwnTrx = false;
01715                         }
01716                 }
01717         }
01718 
01722         function unlockAndRollback() {
01723                 $this->locked = false;
01724                 $dbw = $this->repo->getMasterDB();
01725                 $dbw->rollback( __METHOD__ );
01726                 $this->lockedOwnTrx = false;
01727         }
01728 
01732         protected function readOnlyFatalStatus() {
01733                 return $this->getRepo()->newFatal( 'filereadonlyerror', $this->getName(),
01734                         $this->getRepo()->getName(), $this->getRepo()->getReadOnlyReason() );
01735         }
01736 } // LocalFile class
01737 
01738 # ------------------------------------------------------------------------------
01739 
01744 class LocalFileDeleteBatch {
01745 
01749         var $file;
01750 
01751         var $reason, $srcRels = array(), $archiveUrls = array(), $deletionBatch, $suppress;
01752         var $status;
01753 
01759         function __construct( File $file, $reason = '', $suppress = false ) {
01760                 $this->file = $file;
01761                 $this->reason = $reason;
01762                 $this->suppress = $suppress;
01763                 $this->status = $file->repo->newGood();
01764         }
01765 
01766         function addCurrent() {
01767                 $this->srcRels['.'] = $this->file->getRel();
01768         }
01769 
01773         function addOld( $oldName ) {
01774                 $this->srcRels[$oldName] = $this->file->getArchiveRel( $oldName );
01775                 $this->archiveUrls[] = $this->file->getArchiveUrl( $oldName );
01776         }
01777 
01782         function addOlds() {
01783                 $archiveNames = array();
01784 
01785                 $dbw = $this->file->repo->getMasterDB();
01786                 $result = $dbw->select( 'oldimage',
01787                         array( 'oi_archive_name' ),
01788                         array( 'oi_name' => $this->file->getName() ),
01789                         __METHOD__
01790                 );
01791 
01792                 foreach ( $result as $row ) {
01793                         $this->addOld( $row->oi_archive_name );
01794                         $archiveNames[] = $row->oi_archive_name;
01795                 }
01796 
01797                 return $archiveNames;
01798         }
01799 
01803         function getOldRels() {
01804                 if ( !isset( $this->srcRels['.'] ) ) {
01805                         $oldRels =& $this->srcRels;
01806                         $deleteCurrent = false;
01807                 } else {
01808                         $oldRels = $this->srcRels;
01809                         unset( $oldRels['.'] );
01810                         $deleteCurrent = true;
01811                 }
01812 
01813                 return array( $oldRels, $deleteCurrent );
01814         }
01815 
01819         protected function getHashes() {
01820                 $hashes = array();
01821                 list( $oldRels, $deleteCurrent ) = $this->getOldRels();
01822 
01823                 if ( $deleteCurrent ) {
01824                         $hashes['.'] = $this->file->getSha1();
01825                 }
01826 
01827                 if ( count( $oldRels ) ) {
01828                         $dbw = $this->file->repo->getMasterDB();
01829                         $res = $dbw->select(
01830                                 'oldimage',
01831                                 array( 'oi_archive_name', 'oi_sha1' ),
01832                                 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')',
01833                                 __METHOD__
01834                         );
01835 
01836                         foreach ( $res as $row ) {
01837                                 if ( rtrim( $row->oi_sha1, "\0" ) === '' ) {
01838                                         // Get the hash from the file
01839                                         $oldUrl = $this->file->getArchiveVirtualUrl( $row->oi_archive_name );
01840                                         $props = $this->file->repo->getFileProps( $oldUrl );
01841 
01842                                         if ( $props['fileExists'] ) {
01843                                                 // Upgrade the oldimage row
01844                                                 $dbw->update( 'oldimage',
01845                                                         array( 'oi_sha1' => $props['sha1'] ),
01846                                                         array( 'oi_name' => $this->file->getName(), 'oi_archive_name' => $row->oi_archive_name ),
01847                                                         __METHOD__ );
01848                                                 $hashes[$row->oi_archive_name] = $props['sha1'];
01849                                         } else {
01850                                                 $hashes[$row->oi_archive_name] = false;
01851                                         }
01852                                 } else {
01853                                         $hashes[$row->oi_archive_name] = $row->oi_sha1;
01854                                 }
01855                         }
01856                 }
01857 
01858                 $missing = array_diff_key( $this->srcRels, $hashes );
01859 
01860                 foreach ( $missing as $name => $rel ) {
01861                         $this->status->error( 'filedelete-old-unregistered', $name );
01862                 }
01863 
01864                 foreach ( $hashes as $name => $hash ) {
01865                         if ( !$hash ) {
01866                                 $this->status->error( 'filedelete-missing', $this->srcRels[$name] );
01867                                 unset( $hashes[$name] );
01868                         }
01869                 }
01870 
01871                 return $hashes;
01872         }
01873 
01874         function doDBInserts() {
01875                 global $wgUser;
01876 
01877                 $dbw = $this->file->repo->getMasterDB();
01878                 $encTimestamp = $dbw->addQuotes( $dbw->timestamp() );
01879                 $encUserId = $dbw->addQuotes( $wgUser->getId() );
01880                 $encReason = $dbw->addQuotes( $this->reason );
01881                 $encGroup = $dbw->addQuotes( 'deleted' );
01882                 $ext = $this->file->getExtension();
01883                 $dotExt = $ext === '' ? '' : ".$ext";
01884                 $encExt = $dbw->addQuotes( $dotExt );
01885                 list( $oldRels, $deleteCurrent ) = $this->getOldRels();
01886 
01887                 // Bitfields to further suppress the content
01888                 if ( $this->suppress ) {
01889                         $bitfield = 0;
01890                         // This should be 15...
01891                         $bitfield |= Revision::DELETED_TEXT;
01892                         $bitfield |= Revision::DELETED_COMMENT;
01893                         $bitfield |= Revision::DELETED_USER;
01894                         $bitfield |= Revision::DELETED_RESTRICTED;
01895                 } else {
01896                         $bitfield = 'oi_deleted';
01897                 }
01898 
01899                 if ( $deleteCurrent ) {
01900                         $concat = $dbw->buildConcat( array( "img_sha1", $encExt ) );
01901                         $where = array( 'img_name' => $this->file->getName() );
01902                         $dbw->insertSelect( 'filearchive', 'image',
01903                                 array(
01904                                         'fa_storage_group' => $encGroup,
01905                                         'fa_storage_key'   => "CASE WHEN img_sha1='' THEN '' ELSE $concat END",
01906                                         'fa_deleted_user'      => $encUserId,
01907                                         'fa_deleted_timestamp' => $encTimestamp,
01908                                         'fa_deleted_reason'    => $encReason,
01909                                         'fa_deleted'           => $this->suppress ? $bitfield : 0,
01910 
01911                                         'fa_name'         => 'img_name',
01912                                         'fa_archive_name' => 'NULL',
01913                                         'fa_size'         => 'img_size',
01914                                         'fa_width'        => 'img_width',
01915                                         'fa_height'       => 'img_height',
01916                                         'fa_metadata'     => 'img_metadata',
01917                                         'fa_bits'         => 'img_bits',
01918                                         'fa_media_type'   => 'img_media_type',
01919                                         'fa_major_mime'   => 'img_major_mime',
01920                                         'fa_minor_mime'   => 'img_minor_mime',
01921                                         'fa_description'  => 'img_description',
01922                                         'fa_user'         => 'img_user',
01923                                         'fa_user_text'    => 'img_user_text',
01924                                         'fa_timestamp'    => 'img_timestamp',
01925                                         'fa_sha1'         => 'img_sha1',
01926                                 ), $where, __METHOD__ );
01927                 }
01928 
01929                 if ( count( $oldRels ) ) {
01930                         $concat = $dbw->buildConcat( array( "oi_sha1", $encExt ) );
01931                         $where = array(
01932                                 'oi_name' => $this->file->getName(),
01933                                 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')' );
01934                         $dbw->insertSelect( 'filearchive', 'oldimage',
01935                                 array(
01936                                         'fa_storage_group' => $encGroup,
01937                                         'fa_storage_key'   => "CASE WHEN oi_sha1='' THEN '' ELSE $concat END",
01938                                         'fa_deleted_user'      => $encUserId,
01939                                         'fa_deleted_timestamp' => $encTimestamp,
01940                                         'fa_deleted_reason'    => $encReason,
01941                                         'fa_deleted'           => $this->suppress ? $bitfield : 'oi_deleted',
01942 
01943                                         'fa_name'         => 'oi_name',
01944                                         'fa_archive_name' => 'oi_archive_name',
01945                                         'fa_size'         => 'oi_size',
01946                                         'fa_width'        => 'oi_width',
01947                                         'fa_height'       => 'oi_height',
01948                                         'fa_metadata'     => 'oi_metadata',
01949                                         'fa_bits'         => 'oi_bits',
01950                                         'fa_media_type'   => 'oi_media_type',
01951                                         'fa_major_mime'   => 'oi_major_mime',
01952                                         'fa_minor_mime'   => 'oi_minor_mime',
01953                                         'fa_description'  => 'oi_description',
01954                                         'fa_user'         => 'oi_user',
01955                                         'fa_user_text'    => 'oi_user_text',
01956                                         'fa_timestamp'    => 'oi_timestamp',
01957                                         'fa_sha1'         => 'oi_sha1',
01958                                 ), $where, __METHOD__ );
01959                 }
01960         }
01961 
01962         function doDBDeletes() {
01963                 $dbw = $this->file->repo->getMasterDB();
01964                 list( $oldRels, $deleteCurrent ) = $this->getOldRels();
01965 
01966                 if ( count( $oldRels ) ) {
01967                         $dbw->delete( 'oldimage',
01968                                 array(
01969                                         'oi_name' => $this->file->getName(),
01970                                         'oi_archive_name' => array_keys( $oldRels )
01971                                 ), __METHOD__ );
01972                 }
01973 
01974                 if ( $deleteCurrent ) {
01975                         $dbw->delete( 'image', array( 'img_name' => $this->file->getName() ), __METHOD__ );
01976                 }
01977         }
01978 
01983         function execute() {
01984                 wfProfileIn( __METHOD__ );
01985 
01986                 $this->file->lock();
01987                 // Leave private files alone
01988                 $privateFiles = array();
01989                 list( $oldRels, ) = $this->getOldRels();
01990                 $dbw = $this->file->repo->getMasterDB();
01991 
01992                 if ( !empty( $oldRels ) ) {
01993                         $res = $dbw->select( 'oldimage',
01994                                 array( 'oi_archive_name' ),
01995                                 array( 'oi_name' => $this->file->getName(),
01996                                         'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')',
01997                                         $dbw->bitAnd( 'oi_deleted', File::DELETED_FILE ) => File::DELETED_FILE ),
01998                                 __METHOD__ );
01999 
02000                         foreach ( $res as $row ) {
02001                                 $privateFiles[$row->oi_archive_name] = 1;
02002                         }
02003                 }
02004                 // Prepare deletion batch
02005                 $hashes = $this->getHashes();
02006                 $this->deletionBatch = array();
02007                 $ext = $this->file->getExtension();
02008                 $dotExt = $ext === '' ? '' : ".$ext";
02009 
02010                 foreach ( $this->srcRels as $name => $srcRel ) {
02011                         // Skip files that have no hash (missing source).
02012                         // Keep private files where they are.
02013                         if ( isset( $hashes[$name] ) && !array_key_exists( $name, $privateFiles ) ) {
02014                                 $hash = $hashes[$name];
02015                                 $key = $hash . $dotExt;
02016                                 $dstRel = $this->file->repo->getDeletedHashPath( $key ) . $key;
02017                                 $this->deletionBatch[$name] = array( $srcRel, $dstRel );
02018                         }
02019                 }
02020 
02021                 // Lock the filearchive rows so that the files don't get deleted by a cleanup operation
02022                 // We acquire this lock by running the inserts now, before the file operations.
02023                 //
02024                 // This potentially has poor lock contention characteristics -- an alternative
02025                 // scheme would be to insert stub filearchive entries with no fa_name and commit
02026                 // them in a separate transaction, then run the file ops, then update the fa_name fields.
02027                 $this->doDBInserts();
02028 
02029                 // Removes non-existent file from the batch, so we don't get errors.
02030                 $this->deletionBatch = $this->removeNonexistentFiles( $this->deletionBatch );
02031 
02032                 // Execute the file deletion batch
02033                 $status = $this->file->repo->deleteBatch( $this->deletionBatch );
02034 
02035                 if ( !$status->isGood() ) {
02036                         $this->status->merge( $status );
02037                 }
02038 
02039                 if ( !$this->status->isOK() ) {
02040                         // Critical file deletion error
02041                         // Roll back inserts, release lock and abort
02042                         // TODO: delete the defunct filearchive rows if we are using a non-transactional DB
02043                         $this->file->unlockAndRollback();
02044                         wfProfileOut( __METHOD__ );
02045                         return $this->status;
02046                 }
02047 
02048                 // Delete image/oldimage rows
02049                 $this->doDBDeletes();
02050 
02051                 // Commit and return
02052                 $this->file->unlock();
02053                 wfProfileOut( __METHOD__ );
02054 
02055                 return $this->status;
02056         }
02057 
02063         function removeNonexistentFiles( $batch ) {
02064                 $files = $newBatch = array();
02065 
02066                 foreach ( $batch as $batchItem ) {
02067                         list( $src, ) = $batchItem;
02068                         $files[$src] = $this->file->repo->getVirtualUrl( 'public' ) . '/' . rawurlencode( $src );
02069                 }
02070 
02071                 $result = $this->file->repo->fileExistsBatch( $files );
02072 
02073                 foreach ( $batch as $batchItem ) {
02074                         if ( $result[$batchItem[0]] ) {
02075                                 $newBatch[] = $batchItem;
02076                         }
02077                 }
02078 
02079                 return $newBatch;
02080         }
02081 }
02082 
02083 # ------------------------------------------------------------------------------
02084 
02089 class LocalFileRestoreBatch {
02093         var $file;
02094 
02095         var $cleanupBatch, $ids, $all, $unsuppress = false;
02096 
02101         function __construct( File $file, $unsuppress = false ) {
02102                 $this->file = $file;
02103                 $this->cleanupBatch = $this->ids = array();
02104                 $this->ids = array();
02105                 $this->unsuppress = $unsuppress;
02106         }
02107 
02111         function addId( $fa_id ) {
02112                 $this->ids[] = $fa_id;
02113         }
02114 
02118         function addIds( $ids ) {
02119                 $this->ids = array_merge( $this->ids, $ids );
02120         }
02121 
02125         function addAll() {
02126                 $this->all = true;
02127         }
02128 
02137         function execute() {
02138                 global $wgLang;
02139 
02140                 if ( !$this->all && !$this->ids ) {
02141                         // Do nothing
02142                         return $this->file->repo->newGood();
02143                 }
02144 
02145                 $exists = $this->file->lock();
02146                 $dbw = $this->file->repo->getMasterDB();
02147                 $status = $this->file->repo->newGood();
02148 
02149                 // Fetch all or selected archived revisions for the file,
02150                 // sorted from the most recent to the oldest.
02151                 $conditions = array( 'fa_name' => $this->file->getName() );
02152 
02153                 if ( !$this->all ) {
02154                         $conditions[] = 'fa_id IN (' . $dbw->makeList( $this->ids ) . ')';
02155                 }
02156 
02157                 $result = $dbw->select(
02158                         'filearchive',
02159                         ArchivedFile::selectFields(),
02160                         $conditions,
02161                         __METHOD__,
02162                         array( 'ORDER BY' => 'fa_timestamp DESC' )
02163                 );
02164 
02165                 $idsPresent = array();
02166                 $storeBatch = array();
02167                 $insertBatch = array();
02168                 $insertCurrent = false;
02169                 $deleteIds = array();
02170                 $first = true;
02171                 $archiveNames = array();
02172 
02173                 foreach ( $result as $row ) {
02174                         $idsPresent[] = $row->fa_id;
02175 
02176                         if ( $row->fa_name != $this->file->getName() ) {
02177                                 $status->error( 'undelete-filename-mismatch', $wgLang->timeanddate( $row->fa_timestamp ) );
02178                                 $status->failCount++;
02179                                 continue;
02180                         }
02181 
02182                         if ( $row->fa_storage_key == '' ) {
02183                                 // Revision was missing pre-deletion
02184                                 $status->error( 'undelete-bad-store-key', $wgLang->timeanddate( $row->fa_timestamp ) );
02185                                 $status->failCount++;
02186                                 continue;
02187                         }
02188 
02189                         $deletedRel = $this->file->repo->getDeletedHashPath( $row->fa_storage_key ) . $row->fa_storage_key;
02190                         $deletedUrl = $this->file->repo->getVirtualUrl() . '/deleted/' . $deletedRel;
02191 
02192                         if( isset( $row->fa_sha1 ) ) {
02193                                 $sha1 = $row->fa_sha1;
02194                         } else {
02195                                 // old row, populate from key
02196                                 $sha1 = LocalRepo::getHashFromKey( $row->fa_storage_key );
02197                         }
02198 
02199                         # Fix leading zero
02200                         if ( strlen( $sha1 ) == 32 && $sha1[0] == '0' ) {
02201                                 $sha1 = substr( $sha1, 1 );
02202                         }
02203 
02204                         if ( is_null( $row->fa_major_mime ) || $row->fa_major_mime == 'unknown'
02205                                 || is_null( $row->fa_minor_mime ) || $row->fa_minor_mime == 'unknown'
02206                                 || is_null( $row->fa_media_type ) || $row->fa_media_type == 'UNKNOWN'
02207                                 || is_null( $row->fa_metadata ) ) {
02208                                 // Refresh our metadata
02209                                 // Required for a new current revision; nice for older ones too. :)
02210                                 $props = RepoGroup::singleton()->getFileProps( $deletedUrl );
02211                         } else {
02212                                 $props = array(
02213                                         'minor_mime' => $row->fa_minor_mime,
02214                                         'major_mime' => $row->fa_major_mime,
02215                                         'media_type' => $row->fa_media_type,
02216                                         'metadata'   => $row->fa_metadata
02217                                 );
02218                         }
02219 
02220                         if ( $first && !$exists ) {
02221                                 // This revision will be published as the new current version
02222                                 $destRel = $this->file->getRel();
02223                                 $insertCurrent = array(
02224                                         'img_name'        => $row->fa_name,
02225                                         'img_size'        => $row->fa_size,
02226                                         'img_width'       => $row->fa_width,
02227                                         'img_height'      => $row->fa_height,
02228                                         'img_metadata'    => $props['metadata'],
02229                                         'img_bits'        => $row->fa_bits,
02230                                         'img_media_type'  => $props['media_type'],
02231                                         'img_major_mime'  => $props['major_mime'],
02232                                         'img_minor_mime'  => $props['minor_mime'],
02233                                         'img_description' => $row->fa_description,
02234                                         'img_user'        => $row->fa_user,
02235                                         'img_user_text'   => $row->fa_user_text,
02236                                         'img_timestamp'   => $row->fa_timestamp,
02237                                         'img_sha1'        => $sha1
02238                                 );
02239 
02240                                 // The live (current) version cannot be hidden!
02241                                 if ( !$this->unsuppress && $row->fa_deleted ) {
02242                                         $storeBatch[] = array( $deletedUrl, 'public', $destRel );
02243                                         $this->cleanupBatch[] = $row->fa_storage_key;
02244                                 }
02245                         } else {
02246                                 $archiveName = $row->fa_archive_name;
02247 
02248                                 if ( $archiveName == '' ) {
02249                                         // This was originally a current version; we
02250                                         // have to devise a new archive name for it.
02251                                         // Format is <timestamp of archiving>!<name>
02252                                         $timestamp = wfTimestamp( TS_UNIX, $row->fa_deleted_timestamp );
02253 
02254                                         do {
02255                                                 $archiveName = wfTimestamp( TS_MW, $timestamp ) . '!' . $row->fa_name;
02256                                                 $timestamp++;
02257                                         } while ( isset( $archiveNames[$archiveName] ) );
02258                                 }
02259 
02260                                 $archiveNames[$archiveName] = true;
02261                                 $destRel = $this->file->getArchiveRel( $archiveName );
02262                                 $insertBatch[] = array(
02263                                         'oi_name'         => $row->fa_name,
02264                                         'oi_archive_name' => $archiveName,
02265                                         'oi_size'         => $row->fa_size,
02266                                         'oi_width'        => $row->fa_width,
02267                                         'oi_height'       => $row->fa_height,
02268                                         'oi_bits'         => $row->fa_bits,
02269                                         'oi_description'  => $row->fa_description,
02270                                         'oi_user'         => $row->fa_user,
02271                                         'oi_user_text'    => $row->fa_user_text,
02272                                         'oi_timestamp'    => $row->fa_timestamp,
02273                                         'oi_metadata'     => $props['metadata'],
02274                                         'oi_media_type'   => $props['media_type'],
02275                                         'oi_major_mime'   => $props['major_mime'],
02276                                         'oi_minor_mime'   => $props['minor_mime'],
02277                                         'oi_deleted'      => $this->unsuppress ? 0 : $row->fa_deleted,
02278                                         'oi_sha1'         => $sha1 );
02279                         }
02280 
02281                         $deleteIds[] = $row->fa_id;
02282 
02283                         if ( !$this->unsuppress && $row->fa_deleted & File::DELETED_FILE ) {
02284                                 // private files can stay where they are
02285                                 $status->successCount++;
02286                         } else {
02287                                 $storeBatch[] = array( $deletedUrl, 'public', $destRel );
02288                                 $this->cleanupBatch[] = $row->fa_storage_key;
02289                         }
02290 
02291                         $first = false;
02292                 }
02293 
02294                 unset( $result );
02295 
02296                 // Add a warning to the status object for missing IDs
02297                 $missingIds = array_diff( $this->ids, $idsPresent );
02298 
02299                 foreach ( $missingIds as $id ) {
02300                         $status->error( 'undelete-missing-filearchive', $id );
02301                 }
02302 
02303                 // Remove missing files from batch, so we don't get errors when undeleting them
02304                 $storeBatch = $this->removeNonexistentFiles( $storeBatch );
02305 
02306                 // Run the store batch
02307                 // Use the OVERWRITE_SAME flag to smooth over a common error
02308                 $storeStatus = $this->file->repo->storeBatch( $storeBatch, FileRepo::OVERWRITE_SAME );
02309                 $status->merge( $storeStatus );
02310 
02311                 if ( !$status->isGood() ) {
02312                         // Even if some files could be copied, fail entirely as that is the
02313                         // easiest thing to do without data loss
02314                         $this->cleanupFailedBatch( $storeStatus, $storeBatch );
02315                         $status->ok = false;
02316                         $this->file->unlock();
02317 
02318                         return $status;
02319                 }
02320 
02321                 // Run the DB updates
02322                 // Because we have locked the image row, key conflicts should be rare.
02323                 // If they do occur, we can roll back the transaction at this time with
02324                 // no data loss, but leaving unregistered files scattered throughout the
02325                 // public zone.
02326                 // This is not ideal, which is why it's important to lock the image row.
02327                 if ( $insertCurrent ) {
02328                         $dbw->insert( 'image', $insertCurrent, __METHOD__ );
02329                 }
02330 
02331                 if ( $insertBatch ) {
02332                         $dbw->insert( 'oldimage', $insertBatch, __METHOD__ );
02333                 }
02334 
02335                 if ( $deleteIds ) {
02336                         $dbw->delete( 'filearchive',
02337                                 array( 'fa_id IN (' . $dbw->makeList( $deleteIds ) . ')' ),
02338                                 __METHOD__ );
02339                 }
02340 
02341                 // If store batch is empty (all files are missing), deletion is to be considered successful
02342                 if ( $status->successCount > 0 || !$storeBatch ) {
02343                         if ( !$exists ) {
02344                                 wfDebug( __METHOD__ . " restored {$status->successCount} items, creating a new current\n" );
02345 
02346                                 DeferredUpdates::addUpdate( SiteStatsUpdate::factory( array( 'images' => 1 ) ) );
02347 
02348                                 $this->file->purgeEverything();
02349                         } else {
02350                                 wfDebug( __METHOD__ . " restored {$status->successCount} as archived versions\n" );
02351                                 $this->file->purgeDescription();
02352                                 $this->file->purgeHistory();
02353                         }
02354                 }
02355 
02356                 $this->file->unlock();
02357 
02358                 return $status;
02359         }
02360 
02366         function removeNonexistentFiles( $triplets ) {
02367                 $files = $filteredTriplets = array();
02368                 foreach ( $triplets as $file ) {
02369                         $files[$file[0]] = $file[0];
02370                 }
02371 
02372                 $result = $this->file->repo->fileExistsBatch( $files );
02373 
02374                 foreach ( $triplets as $file ) {
02375                         if ( $result[$file[0]] ) {
02376                                 $filteredTriplets[] = $file;
02377                         }
02378                 }
02379 
02380                 return $filteredTriplets;
02381         }
02382 
02388         function removeNonexistentFromCleanup( $batch ) {
02389                 $files = $newBatch = array();
02390                 $repo = $this->file->repo;
02391 
02392                 foreach ( $batch as $file ) {
02393                         $files[$file] = $repo->getVirtualUrl( 'deleted' ) . '/' .
02394                                 rawurlencode( $repo->getDeletedHashPath( $file ) . $file );
02395                 }
02396 
02397                 $result = $repo->fileExistsBatch( $files );
02398 
02399                 foreach ( $batch as $file ) {
02400                         if ( $result[$file] ) {
02401                                 $newBatch[] = $file;
02402                         }
02403                 }
02404 
02405                 return $newBatch;
02406         }
02407 
02413         function cleanup() {
02414                 if ( !$this->cleanupBatch ) {
02415                         return $this->file->repo->newGood();
02416                 }
02417 
02418                 $this->cleanupBatch = $this->removeNonexistentFromCleanup( $this->cleanupBatch );
02419 
02420                 $status = $this->file->repo->cleanupDeletedBatch( $this->cleanupBatch );
02421 
02422                 return $status;
02423         }
02424 
02432         function cleanupFailedBatch( $storeStatus, $storeBatch ) {
02433                 $cleanupBatch = array();
02434 
02435                 foreach ( $storeStatus->success as $i => $success ) {
02436                         // Check if this item of the batch was successfully copied
02437                         if ( $success ) {
02438                                 // Item was successfully copied and needs to be removed again
02439                                 // Extract ($dstZone, $dstRel) from the batch
02440                                 $cleanupBatch[] = array( $storeBatch[$i][1], $storeBatch[$i][2] );
02441                         }
02442                 }
02443                 $this->file->repo->cleanupBatch( $cleanupBatch );
02444         }
02445 }
02446 
02447 # ------------------------------------------------------------------------------
02448 
02453 class LocalFileMoveBatch {
02454 
02458         var $file;
02459 
02463         var $target;
02464 
02465         var $cur, $olds, $oldCount, $archive;
02466 
02470         var $db;
02471 
02476         function __construct( File $file, Title $target ) {
02477                 $this->file = $file;
02478                 $this->target = $target;
02479                 $this->oldHash = $this->file->repo->getHashPath( $this->file->getName() );
02480                 $this->newHash = $this->file->repo->getHashPath( $this->target->getDBkey() );
02481                 $this->oldName = $this->file->getName();
02482                 $this->newName = $this->file->repo->getNameFromTitle( $this->target );
02483                 $this->oldRel = $this->oldHash . $this->oldName;
02484                 $this->newRel = $this->newHash . $this->newName;
02485                 $this->db = $file->getRepo()->getMasterDb();
02486         }
02487 
02491         function addCurrent() {
02492                 $this->cur = array( $this->oldRel, $this->newRel );
02493         }
02494 
02499         function addOlds() {
02500                 $archiveBase = 'archive';
02501                 $this->olds = array();
02502                 $this->oldCount = 0;
02503                 $archiveNames = array();
02504 
02505                 $result = $this->db->select( 'oldimage',
02506                         array( 'oi_archive_name', 'oi_deleted' ),
02507                         array( 'oi_name' => $this->oldName ),
02508                         __METHOD__
02509                 );
02510 
02511                 foreach ( $result as $row ) {
02512                         $archiveNames[] = $row->oi_archive_name;
02513                         $oldName = $row->oi_archive_name;
02514                         $bits = explode( '!', $oldName, 2 );
02515 
02516                         if ( count( $bits ) != 2 ) {
02517                                 wfDebug( "Old file name missing !: '$oldName' \n" );
02518                                 continue;
02519                         }
02520 
02521                         list( $timestamp, $filename ) = $bits;
02522 
02523                         if ( $this->oldName != $filename ) {
02524                                 wfDebug( "Old file name doesn't match: '$oldName' \n" );
02525                                 continue;
02526                         }
02527 
02528                         $this->oldCount++;
02529 
02530                         // Do we want to add those to oldCount?
02531                         if ( $row->oi_deleted & File::DELETED_FILE ) {
02532                                 continue;
02533                         }
02534 
02535                         $this->olds[] = array(
02536                                 "{$archiveBase}/{$this->oldHash}{$oldName}",
02537                                 "{$archiveBase}/{$this->newHash}{$timestamp}!{$this->newName}"
02538                         );
02539                 }
02540 
02541                 return $archiveNames;
02542         }
02543 
02548         function execute() {
02549                 $repo = $this->file->repo;
02550                 $status = $repo->newGood();
02551 
02552                 $triplets = $this->getMoveTriplets();
02553                 $triplets = $this->removeNonexistentFiles( $triplets );
02554 
02555                 $this->file->lock(); // begin
02556                 // Rename the file versions metadata in the DB.
02557                 // This implicitly locks the destination file, which avoids race conditions.
02558                 // If we moved the files from A -> C before DB updates, another process could
02559                 // move files from B -> C at this point, causing storeBatch() to fail and thus
02560                 // cleanupTarget() to trigger. It would delete the C files and cause data loss.
02561                 $statusDb = $this->doDBUpdates();
02562                 if ( !$statusDb->isGood() ) {
02563                         $this->file->unlockAndRollback();
02564                         $statusDb->ok = false;
02565                         return $statusDb;
02566                 }
02567                 wfDebugLog( 'imagemove', "Renamed {$this->file->getName()} in database: {$statusDb->successCount} successes, {$statusDb->failCount} failures" );
02568 
02569                 // Copy the files into their new location.
02570                 // If a prior process fataled copying or cleaning up files we tolerate any
02571                 // of the existing files if they are identical to the ones being stored.
02572                 $statusMove = $repo->storeBatch( $triplets, FileRepo::OVERWRITE_SAME );
02573                 wfDebugLog( 'imagemove', "Moved files for {$this->file->getName()}: {$statusMove->successCount} successes, {$statusMove->failCount} failures" );
02574                 if ( !$statusMove->isGood() ) {
02575                         // Delete any files copied over (while the destination is still locked)
02576                         $this->cleanupTarget( $triplets );
02577                         $this->file->unlockAndRollback(); // unlocks the destination
02578                         wfDebugLog( 'imagemove', "Error in moving files: " . $statusMove->getWikiText() );
02579                         $statusMove->ok = false;
02580                         return $statusMove;
02581                 }
02582                 $this->file->unlock(); // done
02583 
02584                 // Everything went ok, remove the source files
02585                 $this->cleanupSource( $triplets );
02586 
02587                 $status->merge( $statusDb );
02588                 $status->merge( $statusMove );
02589 
02590                 return $status;
02591         }
02592 
02599         function doDBUpdates() {
02600                 $repo = $this->file->repo;
02601                 $status = $repo->newGood();
02602                 $dbw = $this->db;
02603 
02604                 // Update current image
02605                 $dbw->update(
02606                         'image',
02607                         array( 'img_name' => $this->newName ),
02608                         array( 'img_name' => $this->oldName ),
02609                         __METHOD__
02610                 );
02611 
02612                 if ( $dbw->affectedRows() ) {
02613                         $status->successCount++;
02614                 } else {
02615                         $status->failCount++;
02616                         $status->fatal( 'imageinvalidfilename' );
02617                         return $status;
02618                 }
02619 
02620                 // Update old images
02621                 $dbw->update(
02622                         'oldimage',
02623                         array(
02624                                 'oi_name' => $this->newName,
02625                                 'oi_archive_name = ' . $dbw->strreplace( 'oi_archive_name',
02626                                         $dbw->addQuotes( $this->oldName ), $dbw->addQuotes( $this->newName ) ),
02627                         ),
02628                         array( 'oi_name' => $this->oldName ),
02629                         __METHOD__
02630                 );
02631 
02632                 $affected = $dbw->affectedRows();
02633                 $total = $this->oldCount;
02634                 $status->successCount += $affected;
02635                 // Bug 34934: $total is based on files that actually exist.
02636                 // There may be more DB rows than such files, in which case $affected
02637                 // can be greater than $total. We use max() to avoid negatives here.
02638                 $status->failCount += max( 0, $total - $affected );
02639                 if ( $status->failCount ) {
02640                         $status->error( 'imageinvalidfilename' );
02641                 }
02642 
02643                 return $status;
02644         }
02645 
02650         function getMoveTriplets() {
02651                 $moves = array_merge( array( $this->cur ), $this->olds );
02652                 $triplets = array(); // The format is: (srcUrl, destZone, destUrl)
02653 
02654                 foreach ( $moves as $move ) {
02655                         // $move: (oldRelativePath, newRelativePath)
02656                         $srcUrl = $this->file->repo->getVirtualUrl() . '/public/' . rawurlencode( $move[0] );
02657                         $triplets[] = array( $srcUrl, 'public', $move[1] );
02658                         wfDebugLog( 'imagemove', "Generated move triplet for {$this->file->getName()}: {$srcUrl} :: public :: {$move[1]}" );
02659                 }
02660 
02661                 return $triplets;
02662         }
02663 
02669         function removeNonexistentFiles( $triplets ) {
02670                 $files = array();
02671 
02672                 foreach ( $triplets as $file ) {
02673                         $files[$file[0]] = $file[0];
02674                 }
02675 
02676                 $result = $this->file->repo->fileExistsBatch( $files );
02677                 $filteredTriplets = array();
02678 
02679                 foreach ( $triplets as $file ) {
02680                         if ( $result[$file[0]] ) {
02681                                 $filteredTriplets[] = $file;
02682                         } else {
02683                                 wfDebugLog( 'imagemove', "File {$file[0]} does not exist" );
02684                         }
02685                 }
02686 
02687                 return $filteredTriplets;
02688         }
02689 
02694         function cleanupTarget( $triplets ) {
02695                 // Create dest pairs from the triplets
02696                 $pairs = array();
02697                 foreach ( $triplets as $triplet ) {
02698                         // $triplet: (old source virtual URL, dst zone, dest rel)
02699                         $pairs[] = array( $triplet[1], $triplet[2] );
02700                 }
02701 
02702                 $this->file->repo->cleanupBatch( $pairs );
02703         }
02704 
02709         function cleanupSource( $triplets ) {
02710                 // Create source file names from the triplets
02711                 $files = array();
02712                 foreach ( $triplets as $triplet ) {
02713                         $files[] = $triplet[0];
02714                 }
02715 
02716                 $this->file->repo->cleanupBatch( $files );
02717         }
02718 }