MediaWiki  master
LocalFile.php
Go to the documentation of this file.
00001 <?php
00027 define( 'MW_FILE_VERSION', 8 );
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 all this has been loaded from the database (loadFromXxx)
00071                 $upgraded,         # Whether the row was upgraded on load
00072                 $locked,           # True if the image row is locked
00073                 $missing,          # True if file is not present in file system. Not to be cached in memcached
00074                 $deleted;          # Bitfield akin to rev_deleted
00075 
00081         var $repo;
00082 
00083         protected $repoClass = 'LocalRepo';
00084 
00097         static function newFromTitle( $title, $repo, $unused = null ) {
00098                 return new self( $title, $repo );
00099         }
00100 
00110         static function newFromRow( $row, $repo ) {
00111                 $title = Title::makeTitle( NS_FILE, $row->img_name );
00112                 $file = new self( $title, $repo );
00113                 $file->loadFromRow( $row );
00114 
00115                 return $file;
00116         }
00117 
00128         static function newFromKey( $sha1, $repo, $timestamp = false ) {
00129                 $dbr = $repo->getSlaveDB();
00130 
00131                 $conds = array( 'img_sha1' => $sha1 );
00132                 if ( $timestamp ) {
00133                         $conds['img_timestamp'] = $dbr->timestamp( $timestamp );
00134                 }
00135 
00136                 $row = $dbr->selectRow( 'image', self::selectFields(), $conds, __METHOD__ );
00137                 if ( $row ) {
00138                         return self::newFromRow( $row, $repo );
00139                 } else {
00140                         return false;
00141                 }
00142         }
00143 
00148         static function selectFields() {
00149                 return array(
00150                         'img_name',
00151                         'img_size',
00152                         'img_width',
00153                         'img_height',
00154                         'img_metadata',
00155                         'img_bits',
00156                         'img_media_type',
00157                         'img_major_mime',
00158                         'img_minor_mime',
00159                         'img_description',
00160                         'img_user',
00161                         'img_user_text',
00162                         'img_timestamp',
00163                         'img_sha1',
00164                 );
00165         }
00166 
00171         function __construct( $title, $repo ) {
00172                 parent::__construct( $title, $repo );
00173 
00174                 $this->metadata = '';
00175                 $this->historyLine = 0;
00176                 $this->historyRes = null;
00177                 $this->dataLoaded = false;
00178 
00179                 $this->assertRepoDefined();
00180                 $this->assertTitleDefined();
00181         }
00182 
00188         function getCacheKey() {
00189                 $hashedName = md5( $this->getName() );
00190 
00191                 return $this->repo->getSharedCacheKey( 'file', $hashedName );
00192         }
00193 
00198         function loadFromCache() {
00199                 global $wgMemc;
00200 
00201                 wfProfileIn( __METHOD__ );
00202                 $this->dataLoaded = false;
00203                 $key = $this->getCacheKey();
00204 
00205                 if ( !$key ) {
00206                         wfProfileOut( __METHOD__ );
00207                         return false;
00208                 }
00209 
00210                 $cachedValues = $wgMemc->get( $key );
00211 
00212                 // Check if the key existed and belongs to this version of MediaWiki
00213                 if ( isset( $cachedValues['version'] ) && ( $cachedValues['version'] == MW_FILE_VERSION ) ) {
00214                         wfDebug( "Pulling file metadata from cache key $key\n" );
00215                         $this->fileExists = $cachedValues['fileExists'];
00216                         if ( $this->fileExists ) {
00217                                 $this->setProps( $cachedValues );
00218                         }
00219                         $this->dataLoaded = true;
00220                 }
00221 
00222                 if ( $this->dataLoaded ) {
00223                         wfIncrStats( 'image_cache_hit' );
00224                 } else {
00225                         wfIncrStats( 'image_cache_miss' );
00226                 }
00227 
00228                 wfProfileOut( __METHOD__ );
00229                 return $this->dataLoaded;
00230         }
00231 
00235         function saveToCache() {
00236                 global $wgMemc;
00237 
00238                 $this->load();
00239                 $key = $this->getCacheKey();
00240 
00241                 if ( !$key ) {
00242                         return;
00243                 }
00244 
00245                 $fields = $this->getCacheFields( '' );
00246                 $cache = array( 'version' => MW_FILE_VERSION );
00247                 $cache['fileExists'] = $this->fileExists;
00248 
00249                 if ( $this->fileExists ) {
00250                         foreach ( $fields as $field ) {
00251                                 $cache[$field] = $this->$field;
00252                         }
00253                 }
00254 
00255                 $wgMemc->set( $key, $cache, 60 * 60 * 24 * 7 ); // A week
00256         }
00257 
00261         function loadFromFile() {
00262                 $props = $this->repo->getFileProps( $this->getVirtualUrl() );
00263                 $this->setProps( $props );
00264         }
00265 
00270         function getCacheFields( $prefix = 'img_' ) {
00271                 static $fields = array( 'size', 'width', 'height', 'bits', 'media_type',
00272                         'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1', 'user', 'user_text', 'description' );
00273                 static $results = array();
00274 
00275                 if ( $prefix == '' ) {
00276                         return $fields;
00277                 }
00278 
00279                 if ( !isset( $results[$prefix] ) ) {
00280                         $prefixedFields = array();
00281                         foreach ( $fields as $field ) {
00282                                 $prefixedFields[] = $prefix . $field;
00283                         }
00284                         $results[$prefix] = $prefixedFields;
00285                 }
00286 
00287                 return $results[$prefix];
00288         }
00289 
00293         function loadFromDB() {
00294                 # Polymorphic function name to distinguish foreign and local fetches
00295                 $fname = get_class( $this ) . '::' . __FUNCTION__;
00296                 wfProfileIn( $fname );
00297 
00298                 # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
00299                 $this->dataLoaded = true;
00300 
00301                 $dbr = $this->repo->getMasterDB();
00302 
00303                 $row = $dbr->selectRow( 'image', $this->getCacheFields( 'img_' ),
00304                         array( 'img_name' => $this->getName() ), $fname );
00305 
00306                 if ( $row ) {
00307                         $this->loadFromRow( $row );
00308                 } else {
00309                         $this->fileExists = false;
00310                 }
00311 
00312                 wfProfileOut( $fname );
00313         }
00314 
00323         function decodeRow( $row, $prefix = 'img_' ) {
00324                 $array = (array)$row;
00325                 $prefixLength = strlen( $prefix );
00326 
00327                 // Sanity check prefix once
00328                 if ( substr( key( $array ), 0, $prefixLength ) !== $prefix ) {
00329                         throw new MWException( __METHOD__ .  ': incorrect $prefix parameter' );
00330                 }
00331 
00332                 $decoded = array();
00333 
00334                 foreach ( $array as $name => $value ) {
00335                         $decoded[substr( $name, $prefixLength )] = $value;
00336                 }
00337 
00338                 $decoded['timestamp'] = wfTimestamp( TS_MW, $decoded['timestamp'] );
00339 
00340                 if ( empty( $decoded['major_mime'] ) ) {
00341                         $decoded['mime'] = 'unknown/unknown';
00342                 } else {
00343                         if ( !$decoded['minor_mime'] ) {
00344                                 $decoded['minor_mime'] = 'unknown';
00345                         }
00346                         $decoded['mime'] = $decoded['major_mime'] . '/' . $decoded['minor_mime'];
00347                 }
00348 
00349                 # Trim zero padding from char/binary field
00350                 $decoded['sha1'] = rtrim( $decoded['sha1'], "\0" );
00351 
00352                 return $decoded;
00353         }
00354 
00358         function loadFromRow( $row, $prefix = 'img_' ) {
00359                 $this->dataLoaded = true;
00360                 $array = $this->decodeRow( $row, $prefix );
00361 
00362                 foreach ( $array as $name => $value ) {
00363                         $this->$name = $value;
00364                 }
00365 
00366                 $this->fileExists = true;
00367                 $this->maybeUpgradeRow();
00368         }
00369 
00373         function load() {
00374                 if ( !$this->dataLoaded ) {
00375                         if ( !$this->loadFromCache() ) {
00376                                 $this->loadFromDB();
00377                                 $this->saveToCache();
00378                         }
00379                         $this->dataLoaded = true;
00380                 }
00381         }
00382 
00386         function maybeUpgradeRow() {
00387                 global $wgUpdateCompatibleMetadata;
00388                 if ( wfReadOnly() ) {
00389                         return;
00390                 }
00391 
00392                 if ( is_null( $this->media_type ) ||
00393                         $this->mime == 'image/svg'
00394                 ) {
00395                         $this->upgradeRow();
00396                         $this->upgraded = true;
00397                 } else {
00398                         $handler = $this->getHandler();
00399                         if ( $handler ) {
00400                                 $validity = $handler->isMetadataValid( $this, $this->metadata );
00401                                 if ( $validity === MediaHandler::METADATA_BAD
00402                                         || ( $validity === MediaHandler::METADATA_COMPATIBLE && $wgUpdateCompatibleMetadata )
00403                                 ) {
00404                                         $this->upgradeRow();
00405                                         $this->upgraded = true;
00406                                 }
00407                         }
00408                 }
00409         }
00410 
00411         function getUpgraded() {
00412                 return $this->upgraded;
00413         }
00414 
00418         function upgradeRow() {
00419                 wfProfileIn( __METHOD__ );
00420 
00421                 $this->lock(); // begin
00422 
00423                 $this->loadFromFile();
00424 
00425                 # Don't destroy file info of missing files
00426                 if ( !$this->fileExists ) {
00427                         wfDebug( __METHOD__ . ": file does not exist, aborting\n" );
00428                         wfProfileOut( __METHOD__ );
00429                         return;
00430                 }
00431 
00432                 $dbw = $this->repo->getMasterDB();
00433                 list( $major, $minor ) = self::splitMime( $this->mime );
00434 
00435                 if ( wfReadOnly() ) {
00436                         wfProfileOut( __METHOD__ );
00437                         return;
00438                 }
00439                 wfDebug( __METHOD__ . ': upgrading ' . $this->getName() . " to the current schema\n" );
00440 
00441                 $dbw->update( 'image',
00442                         array(
00443                                 'img_size'       => $this->size, // sanity
00444                                 'img_width'      => $this->width,
00445                                 'img_height'     => $this->height,
00446                                 'img_bits'       => $this->bits,
00447                                 'img_media_type' => $this->media_type,
00448                                 'img_major_mime' => $major,
00449                                 'img_minor_mime' => $minor,
00450                                 'img_metadata'   => $this->metadata,
00451                                 'img_sha1'       => $this->sha1,
00452                         ),
00453                         array( 'img_name' => $this->getName() ),
00454                         __METHOD__
00455                 );
00456 
00457                 $this->saveToCache();
00458 
00459                 $this->unlock(); // done
00460 
00461                 wfProfileOut( __METHOD__ );
00462         }
00463 
00471         function setProps( $info ) {
00472                 $this->dataLoaded = true;
00473                 $fields = $this->getCacheFields( '' );
00474                 $fields[] = 'fileExists';
00475 
00476                 foreach ( $fields as $field ) {
00477                         if ( isset( $info[$field] ) ) {
00478                                 $this->$field = $info[$field];
00479                         }
00480                 }
00481 
00482                 // Fix up mime fields
00483                 if ( isset( $info['major_mime'] ) ) {
00484                         $this->mime = "{$info['major_mime']}/{$info['minor_mime']}";
00485                 } elseif ( isset( $info['mime'] ) ) {
00486                         $this->mime = $info['mime'];
00487                         list( $this->major_mime, $this->minor_mime ) = self::splitMime( $this->mime );
00488                 }
00489         }
00490 
00502         function isMissing() {
00503                 if ( $this->missing === null ) {
00504                         list( $fileExists ) = $this->repo->fileExists( $this->getVirtualUrl() );
00505                         $this->missing = !$fileExists;
00506                 }
00507                 return $this->missing;
00508         }
00509 
00516         public function getWidth( $page = 1 ) {
00517                 $this->load();
00518 
00519                 if ( $this->isMultipage() ) {
00520                         $dim = $this->getHandler()->getPageDimensions( $this, $page );
00521                         if ( $dim ) {
00522                                 return $dim['width'];
00523                         } else {
00524                                 return false;
00525                         }
00526                 } else {
00527                         return $this->width;
00528                 }
00529         }
00530 
00537         public function getHeight( $page = 1 ) {
00538                 $this->load();
00539 
00540                 if ( $this->isMultipage() ) {
00541                         $dim = $this->getHandler()->getPageDimensions( $this, $page );
00542                         if ( $dim ) {
00543                                 return $dim['height'];
00544                         } else {
00545                                 return false;
00546                         }
00547                 } else {
00548                         return $this->height;
00549                 }
00550         }
00551 
00558         function getUser( $type = 'text' ) {
00559                 $this->load();
00560 
00561                 if ( $type == 'text' ) {
00562                         return $this->user_text;
00563                 } elseif ( $type == 'id' ) {
00564                         return $this->user;
00565                 }
00566         }
00567 
00572         function getMetadata() {
00573                 $this->load();
00574                 return $this->metadata;
00575         }
00576 
00580         function getBitDepth() {
00581                 $this->load();
00582                 return $this->bits;
00583         }
00584 
00589         public function getSize() {
00590                 $this->load();
00591                 return $this->size;
00592         }
00593 
00598         function getMimeType() {
00599                 $this->load();
00600                 return $this->mime;
00601         }
00602 
00608         function getMediaType() {
00609                 $this->load();
00610                 return $this->media_type;
00611         }
00612 
00623         public function exists() {
00624                 $this->load();
00625                 return $this->fileExists;
00626         }
00627 
00640         function migrateThumbFile( $thumbName ) {
00641                 $thumbDir = $this->getThumbPath();
00642 
00643                 /* Old code for bug 2532
00644                 $thumbPath = "$thumbDir/$thumbName";
00645                 if ( is_dir( $thumbPath ) ) {
00646                         // Directory where file should be
00647                         // This happened occasionally due to broken migration code in 1.5
00648                         // Rename to broken-*
00649                         for ( $i = 0; $i < 100 ; $i++ ) {
00650                                 $broken = $this->repo->getZonePath( 'public' ) . "/broken-$i-$thumbName";
00651                                 if ( !file_exists( $broken ) ) {
00652                                         rename( $thumbPath, $broken );
00653                                         break;
00654                                 }
00655                         }
00656                         // Doesn't exist anymore
00657                         clearstatcache();
00658                 }
00659                 */
00660 
00661                 /*
00662                 if ( $this->repo->fileExists( $thumbDir ) ) {
00663                         // Delete file where directory should be
00664                         $this->repo->cleanupBatch( array( $thumbDir ) );
00665                 }
00666                 */
00667         }
00668 
00678         function getThumbnails( $archiveName = false ) {
00679                 if ( $archiveName ) {
00680                         $dir = $this->getArchiveThumbPath( $archiveName );
00681                 } else {
00682                         $dir = $this->getThumbPath();
00683                 }
00684 
00685                 $backend = $this->repo->getBackend();
00686                 $files = array( $dir );
00687                 $iterator = $backend->getFileList( array( 'dir' => $dir ) );
00688                 foreach ( $iterator as $file ) {
00689                         $files[] = $file;
00690                 }
00691 
00692                 return $files;
00693         }
00694 
00698         function purgeMetadataCache() {
00699                 $this->loadFromDB();
00700                 $this->saveToCache();
00701                 $this->purgeHistory();
00702         }
00703 
00707         function purgeHistory() {
00708                 global $wgMemc;
00709 
00710                 $hashedName = md5( $this->getName() );
00711                 $oldKey = $this->repo->getSharedCacheKey( 'oldfile', $hashedName );
00712 
00713                 // Must purge thumbnails for old versions too! bug 30192
00714                 foreach( $this->getHistory() as $oldFile ) {
00715                         $oldFile->purgeThumbnails();
00716                 }
00717 
00718                 if ( $oldKey ) {
00719                         $wgMemc->delete( $oldKey );
00720                 }
00721         }
00722 
00726         function purgeCache( $options = array() ) {
00727                 // Refresh metadata cache
00728                 $this->purgeMetadataCache();
00729 
00730                 // Delete thumbnails
00731                 $this->purgeThumbnails( $options );
00732 
00733                 // Purge squid cache for this file
00734                 SquidUpdate::purge( array( $this->getURL() ) );
00735         }
00736 
00741         function purgeOldThumbnails( $archiveName ) {
00742                 global $wgUseSquid;
00743                 wfProfileIn( __METHOD__ );
00744 
00745                 // Get a list of old thumbnails and URLs
00746                 $files = $this->getThumbnails( $archiveName );
00747                 $dir = array_shift( $files );
00748                 $this->purgeThumbList( $dir, $files );
00749 
00750                 // Purge any custom thumbnail caches
00751                 wfRunHooks( 'LocalFilePurgeThumbnails', array( $this, $archiveName ) );
00752 
00753                 // Purge the squid
00754                 if ( $wgUseSquid ) {
00755                         $urls = array();
00756                         foreach( $files as $file ) {
00757                                 $urls[] = $this->getArchiveThumbUrl( $archiveName, $file );
00758                         }
00759                         SquidUpdate::purge( $urls );
00760                 }
00761 
00762                 wfProfileOut( __METHOD__ );
00763         }
00764 
00768         function purgeThumbnails( $options = array() ) {
00769                 global $wgUseSquid;
00770                 wfProfileIn( __METHOD__ );
00771 
00772                 // Delete thumbnails
00773                 $files = $this->getThumbnails();
00774 
00775                 // Give media handler a chance to filter the purge list
00776                 if ( !empty( $options['forThumbRefresh'] ) ) {
00777                         $handler = $this->getHandler();
00778                         if ( $handler ) {
00779                                 $handler->filterThumbnailPurgeList( $files, $options );
00780                         }
00781                 }
00782 
00783                 $dir = array_shift( $files );
00784                 $this->purgeThumbList( $dir, $files );
00785 
00786                 // Purge any custom thumbnail caches
00787                 wfRunHooks( 'LocalFilePurgeThumbnails', array( $this, false ) );
00788 
00789                 // Purge the squid
00790                 if ( $wgUseSquid ) {
00791                         $urls = array();
00792                         foreach( $files as $file ) {
00793                                 $urls[] = $this->getThumbUrl( $file );
00794                         }
00795                         SquidUpdate::purge( $urls );
00796                 }
00797 
00798                 wfProfileOut( __METHOD__ );
00799         }
00800 
00806         protected function purgeThumbList( $dir, $files ) {
00807                 $fileListDebug = strtr(
00808                         var_export( $files, true ),
00809                         array("\n"=>'')
00810                 );
00811                 wfDebug( __METHOD__ . ": $fileListDebug\n" );
00812 
00813                 $purgeList = array();
00814                 foreach ( $files as $file ) {
00815                         # Check that the base file name is part of the thumb name
00816                         # This is a basic sanity check to avoid erasing unrelated directories
00817                         if ( strpos( $file, $this->getName() ) !== false
00818                                 || strpos( $file, "-thumbnail" ) !== false // "short" thumb name
00819                         ) {
00820                                 $purgeList[] = "{$dir}/{$file}";
00821                         }
00822                 }
00823 
00824                 # Delete the thumbnails
00825                 $this->repo->quickPurgeBatch( $purgeList );
00826                 # Clear out the thumbnail directory if empty
00827                 $this->repo->quickCleanDir( $dir );
00828         }
00829 
00840         function getHistory( $limit = null, $start = null, $end = null, $inc = true ) {
00841                 $dbr = $this->repo->getSlaveDB();
00842                 $tables = array( 'oldimage' );
00843                 $fields = OldLocalFile::selectFields();
00844                 $conds = $opts = $join_conds = array();
00845                 $eq = $inc ? '=' : '';
00846                 $conds[] = "oi_name = " . $dbr->addQuotes( $this->title->getDBkey() );
00847 
00848                 if ( $start ) {
00849                         $conds[] = "oi_timestamp <$eq " . $dbr->addQuotes( $dbr->timestamp( $start ) );
00850                 }
00851 
00852                 if ( $end ) {
00853                         $conds[] = "oi_timestamp >$eq " . $dbr->addQuotes( $dbr->timestamp( $end ) );
00854                 }
00855 
00856                 if ( $limit ) {
00857                         $opts['LIMIT'] = $limit;
00858                 }
00859 
00860                 // Search backwards for time > x queries
00861                 $order = ( !$start && $end !== null ) ? 'ASC' : 'DESC';
00862                 $opts['ORDER BY'] = "oi_timestamp $order";
00863                 $opts['USE INDEX'] = array( 'oldimage' => 'oi_name_timestamp' );
00864 
00865                 wfRunHooks( 'LocalFile::getHistory', array( &$this, &$tables, &$fields,
00866                         &$conds, &$opts, &$join_conds ) );
00867 
00868                 $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $opts, $join_conds );
00869                 $r = array();
00870 
00871                 foreach ( $res as $row ) {
00872                         if ( $this->repo->oldFileFromRowFactory ) {
00873                                 $r[] = call_user_func( $this->repo->oldFileFromRowFactory, $row, $this->repo );
00874                         } else {
00875                                 $r[] = OldLocalFile::newFromRow( $row, $this->repo );
00876                         }
00877                 }
00878 
00879                 if ( $order == 'ASC' ) {
00880                         $r = array_reverse( $r ); // make sure it ends up descending
00881                 }
00882 
00883                 return $r;
00884         }
00885 
00895         public function nextHistoryLine() {
00896                 # Polymorphic function name to distinguish foreign and local fetches
00897                 $fname = get_class( $this ) . '::' . __FUNCTION__;
00898 
00899                 $dbr = $this->repo->getSlaveDB();
00900 
00901                 if ( $this->historyLine == 0 ) {// called for the first time, return line from cur
00902                         $this->historyRes = $dbr->select( 'image',
00903                                 array(
00904                                         '*',
00905                                         "'' AS oi_archive_name",
00906                                         '0 as oi_deleted',
00907                                         'img_sha1'
00908                                 ),
00909                                 array( 'img_name' => $this->title->getDBkey() ),
00910                                 $fname
00911                         );
00912 
00913                         if ( 0 == $dbr->numRows( $this->historyRes ) ) {
00914                                 $this->historyRes = null;
00915                                 return false;
00916                         }
00917                 } elseif ( $this->historyLine == 1 ) {
00918                         $this->historyRes = $dbr->select( 'oldimage', '*',
00919                                 array( 'oi_name' => $this->title->getDBkey() ),
00920                                 $fname,
00921                                 array( 'ORDER BY' => 'oi_timestamp DESC' )
00922                         );
00923                 }
00924                 $this->historyLine ++;
00925 
00926                 return $dbr->fetchObject( $this->historyRes );
00927         }
00928 
00932         public function resetHistory() {
00933                 $this->historyLine = 0;
00934 
00935                 if ( !is_null( $this->historyRes ) ) {
00936                         $this->historyRes = null;
00937                 }
00938         }
00939 
00968         function upload( $srcPath, $comment, $pageText, $flags = 0, $props = false, $timestamp = false, $user = null ) {
00969                 global $wgContLang;
00970 
00971                 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
00972                         return $this->readOnlyFatalStatus();
00973                 }
00974 
00975                 // truncate nicely or the DB will do it for us
00976                 // non-nicely (dangling multi-byte chars, non-truncated version in cache).
00977                 $comment = $wgContLang->truncate( $comment, 255 );
00978                 $this->lock(); // begin
00979                 $status = $this->publish( $srcPath, $flags );
00980 
00981                 if ( $status->successCount > 0 ) {
00982                         # Essentially we are displacing any existing current file and saving
00983                         # a new current file at the old location. If just the first succeeded,
00984                         # we still need to displace the current DB entry and put in a new one.
00985                         if ( !$this->recordUpload2( $status->value, $comment, $pageText, $props, $timestamp, $user ) ) {
00986                                 $status->fatal( 'filenotfound', $srcPath );
00987                         }
00988                 }
00989 
00990                 $this->unlock(); // done
00991 
00992                 return $status;
00993         }
00994 
01006         function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
01007                 $watch = false, $timestamp = false )
01008         {
01009                 $pageText = SpecialUpload::getInitialPageText( $desc, $license, $copyStatus, $source );
01010 
01011                 if ( !$this->recordUpload2( $oldver, $desc, $pageText, false, $timestamp ) ) {
01012                         return false;
01013                 }
01014 
01015                 if ( $watch ) {
01016                         global $wgUser;
01017                         $wgUser->addWatch( $this->getTitle() );
01018                 }
01019                 return true;
01020         }
01021 
01032         function recordUpload2(
01033                 $oldver, $comment, $pageText, $props = false, $timestamp = false, $user = null
01034         ) {
01035                 wfProfileIn( __METHOD__ );
01036 
01037                 if ( is_null( $user ) ) {
01038                         global $wgUser;
01039                         $user = $wgUser;
01040                 }
01041 
01042                 $dbw = $this->repo->getMasterDB();
01043                 $dbw->begin( __METHOD__ );
01044 
01045                 if ( !$props ) {
01046                         wfProfileIn( __METHOD__ . '-getProps' );
01047                         $props = $this->repo->getFileProps( $this->getVirtualUrl() );
01048                         wfProfileOut( __METHOD__ . '-getProps' );
01049                 }
01050 
01051                 if ( $timestamp === false ) {
01052                         $timestamp = $dbw->timestamp();
01053                 }
01054 
01055                 $props['description'] = $comment;
01056                 $props['user'] = $user->getId();
01057                 $props['user_text'] = $user->getName();
01058                 $props['timestamp'] = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
01059                 $this->setProps( $props );
01060 
01061                 # Fail now if the file isn't there
01062                 if ( !$this->fileExists ) {
01063                         wfDebug( __METHOD__ . ": File " . $this->getRel() . " went missing!\n" );
01064                         wfProfileOut( __METHOD__ );
01065                         return false;
01066                 }
01067 
01068                 $reupload = false;
01069 
01070                 # Test to see if the row exists using INSERT IGNORE
01071                 # This avoids race conditions by locking the row until the commit, and also
01072                 # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
01073                 $dbw->insert( 'image',
01074                         array(
01075                                 'img_name'        => $this->getName(),
01076                                 'img_size'        => $this->size,
01077                                 'img_width'       => intval( $this->width ),
01078                                 'img_height'      => intval( $this->height ),
01079                                 'img_bits'        => $this->bits,
01080                                 'img_media_type'  => $this->media_type,
01081                                 'img_major_mime'  => $this->major_mime,
01082                                 'img_minor_mime'  => $this->minor_mime,
01083                                 'img_timestamp'   => $timestamp,
01084                                 'img_description' => $comment,
01085                                 'img_user'        => $user->getId(),
01086                                 'img_user_text'   => $user->getName(),
01087                                 'img_metadata'    => $this->metadata,
01088                                 'img_sha1'        => $this->sha1
01089                         ),
01090                         __METHOD__,
01091                         'IGNORE'
01092                 );
01093                 if ( $dbw->affectedRows() == 0 ) {
01094                         # (bug 34993) Note: $oldver can be empty here, if the previous
01095                         # version of the file was broken. Allow registration of the new
01096                         # version to continue anyway, because that's better than having
01097                         # an image that's not fixable by user operations.
01098 
01099                         $reupload = true;
01100                         # Collision, this is an update of a file
01101                         # Insert previous contents into oldimage
01102                         $dbw->insertSelect( 'oldimage', 'image',
01103                                 array(
01104                                         'oi_name'         => 'img_name',
01105                                         'oi_archive_name' => $dbw->addQuotes( $oldver ),
01106                                         'oi_size'         => 'img_size',
01107                                         'oi_width'        => 'img_width',
01108                                         'oi_height'       => 'img_height',
01109                                         'oi_bits'         => 'img_bits',
01110                                         'oi_timestamp'    => 'img_timestamp',
01111                                         'oi_description'  => 'img_description',
01112                                         'oi_user'         => 'img_user',
01113                                         'oi_user_text'    => 'img_user_text',
01114                                         'oi_metadata'     => 'img_metadata',
01115                                         'oi_media_type'   => 'img_media_type',
01116                                         'oi_major_mime'   => 'img_major_mime',
01117                                         'oi_minor_mime'   => 'img_minor_mime',
01118                                         'oi_sha1'         => 'img_sha1'
01119                                 ),
01120                                 array( 'img_name' => $this->getName() ),
01121                                 __METHOD__
01122                         );
01123 
01124                         # Update the current image row
01125                         $dbw->update( 'image',
01126                                 array( /* SET */
01127                                         'img_size'        => $this->size,
01128                                         'img_width'       => intval( $this->width ),
01129                                         'img_height'      => intval( $this->height ),
01130                                         'img_bits'        => $this->bits,
01131                                         'img_media_type'  => $this->media_type,
01132                                         'img_major_mime'  => $this->major_mime,
01133                                         'img_minor_mime'  => $this->minor_mime,
01134                                         'img_timestamp'   => $timestamp,
01135                                         'img_description' => $comment,
01136                                         'img_user'        => $user->getId(),
01137                                         'img_user_text'   => $user->getName(),
01138                                         'img_metadata'    => $this->metadata,
01139                                         'img_sha1'        => $this->sha1
01140                                 ),
01141                                 array( 'img_name' => $this->getName() ),
01142                                 __METHOD__
01143                         );
01144                 } else {
01145                         # This is a new file, so update the image count
01146                         DeferredUpdates::addUpdate( SiteStatsUpdate::factory( array( 'images' => 1 ) ) );
01147                 }
01148 
01149                 $descTitle = $this->getTitle();
01150                 $wikiPage = new WikiFilePage( $descTitle );
01151                 $wikiPage->setFile( $this );
01152 
01153                 # Add the log entry
01154                 $log = new LogPage( 'upload' );
01155                 $action = $reupload ? 'overwrite' : 'upload';
01156                 $logId = $log->addEntry( $action, $descTitle, $comment, array(), $user );
01157 
01158                 wfProfileIn( __METHOD__ . '-edit' );
01159                 $exists = $descTitle->exists();
01160 
01161                 if ( $exists ) {
01162                         # Create a null revision
01163                         $latest = $descTitle->getLatestRevID();
01164                         $nullRevision = Revision::newNullRevision(
01165                                 $dbw,
01166                                 $descTitle->getArticleID(),
01167                                 $log->getRcComment(),
01168                                 false
01169                         );
01170                         if (!is_null($nullRevision)) {
01171                                 $nullRevision->insertOn( $dbw );
01172 
01173                                 wfRunHooks( 'NewRevisionFromEditComplete', array( $wikiPage, $nullRevision, $latest, $user ) );
01174                                 $wikiPage->updateRevisionOn( $dbw, $nullRevision );
01175                         }
01176                 }
01177 
01178                 # Commit the transaction now, in case something goes wrong later
01179                 # The most important thing is that files don't get lost, especially archives
01180                 # NOTE: once we have support for nested transactions, the commit may be moved
01181                 #       to after $wikiPage->doEdit has been called.
01182                 $dbw->commit( __METHOD__ );
01183 
01184                 if ( $exists ) {
01185                         # Invalidate the cache for the description page
01186                         $descTitle->invalidateCache();
01187                         $descTitle->purgeSquid();
01188                 } else {
01189                         # New file; create the description page.
01190                         # There's already a log entry, so don't make a second RC entry
01191                         # Squid and file cache for the description page are purged by doEditContent.
01192                         $content = ContentHandler::makeContent( $pageText, $descTitle );
01193                         $status = $wikiPage->doEditContent( $content, $comment, EDIT_NEW | EDIT_SUPPRESS_RC, false, $user );
01194 
01195                         if ( isset( $status->value['revision'] ) ) { // XXX; doEdit() uses a transaction
01196                                 $dbw->begin();
01197                                 $dbw->update( 'logging',
01198                                         array( 'log_page' => $status->value['revision']->getPage() ),
01199                                         array( 'log_id' => $logId ),
01200                                         __METHOD__
01201                                 );
01202                                 $dbw->commit(); // commit before anything bad can happen
01203                         }
01204                 }
01205                 wfProfileOut( __METHOD__ . '-edit' );
01206 
01207                 # Save to cache and purge the squid
01208                 # We shall not saveToCache before the commit since otherwise
01209                 # in case of a rollback there is an usable file from memcached
01210                 # which in fact doesn't really exist (bug 24978)
01211                 $this->saveToCache();
01212 
01213                 if ( $reupload ) {
01214                         # Delete old thumbnails
01215                         wfProfileIn( __METHOD__ . '-purge' );
01216                         $this->purgeThumbnails();
01217                         wfProfileOut( __METHOD__ . '-purge' );
01218 
01219                         # Remove the old file from the squid cache
01220                         SquidUpdate::purge( array( $this->getURL() ) );
01221                 }
01222 
01223                 # Hooks, hooks, the magic of hooks...
01224                 wfProfileIn( __METHOD__ . '-hooks' );
01225                 wfRunHooks( 'FileUpload', array( $this, $reupload, $descTitle->exists() ) );
01226                 wfProfileOut( __METHOD__ . '-hooks' );
01227 
01228                 # Invalidate cache for all pages using this file
01229                 $update = new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' );
01230                 $update->doUpdate();
01231 
01232                 # Invalidate cache for all pages that redirects on this page
01233                 $redirs = $this->getTitle()->getRedirectsHere();
01234 
01235                 foreach ( $redirs as $redir ) {
01236                         $update = new HTMLCacheUpdate( $redir, 'imagelinks' );
01237                         $update->doUpdate();
01238                 }
01239 
01240                 wfProfileOut( __METHOD__ );
01241                 return true;
01242         }
01243 
01258         function publish( $srcPath, $flags = 0 ) {
01259                 return $this->publishTo( $srcPath, $this->getRel(), $flags );
01260         }
01261 
01276         function publishTo( $srcPath, $dstRel, $flags = 0 ) {
01277                 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
01278                         return $this->readOnlyFatalStatus();
01279                 }
01280 
01281                 $this->lock(); // begin
01282 
01283                 $archiveName = wfTimestamp( TS_MW ) . '!'. $this->getName();
01284                 $archiveRel = 'archive/' . $this->getHashPath() . $archiveName;
01285                 $flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0;
01286                 $status = $this->repo->publish( $srcPath, $dstRel, $archiveRel, $flags );
01287 
01288                 if ( $status->value == 'new' ) {
01289                         $status->value = '';
01290                 } else {
01291                         $status->value = $archiveName;
01292                 }
01293 
01294                 $this->unlock(); // done
01295 
01296                 return $status;
01297         }
01298 
01316         function move( $target ) {
01317                 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
01318                         return $this->readOnlyFatalStatus();
01319                 }
01320 
01321                 wfDebugLog( 'imagemove', "Got request to move {$this->name} to " . $target->getText() );
01322                 $batch = new LocalFileMoveBatch( $this, $target );
01323 
01324                 $this->lock(); // begin
01325                 $batch->addCurrent();
01326                 $archiveNames = $batch->addOlds();
01327                 $status = $batch->execute();
01328                 $this->unlock(); // done
01329 
01330                 wfDebugLog( 'imagemove', "Finished moving {$this->name}" );
01331 
01332                 $this->purgeEverything();
01333                 foreach ( $archiveNames as $archiveName ) {
01334                         $this->purgeOldThumbnails( $archiveName );
01335                 }
01336                 if ( $status->isOK() ) {
01337                         // Now switch the object
01338                         $this->title = $target;
01339                         // Force regeneration of the name and hashpath
01340                         unset( $this->name );
01341                         unset( $this->hashPath );
01342                         // Purge the new image
01343                         $this->purgeEverything();
01344                 }
01345 
01346                 return $status;
01347         }
01348 
01361         function delete( $reason, $suppress = false ) {
01362                 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
01363                         return $this->readOnlyFatalStatus();
01364                 }
01365 
01366                 $batch = new LocalFileDeleteBatch( $this, $reason, $suppress );
01367 
01368                 $this->lock(); // begin
01369                 $batch->addCurrent();
01370                 # Get old version relative paths
01371                 $archiveNames = $batch->addOlds();
01372                 $status = $batch->execute();
01373                 $this->unlock(); // done
01374 
01375                 if ( $status->isOK() ) {
01376                         DeferredUpdates::addUpdate( SiteStatsUpdate::factory( array( 'images' => -1 ) ) );
01377                 }
01378 
01379                 $this->purgeEverything();
01380                 foreach ( $archiveNames as $archiveName ) {
01381                         $this->purgeOldThumbnails( $archiveName );
01382                 }
01383 
01384                 return $status;
01385         }
01386 
01401         function deleteOld( $archiveName, $reason, $suppress = false ) {
01402                 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
01403                         return $this->readOnlyFatalStatus();
01404                 }
01405 
01406                 $batch = new LocalFileDeleteBatch( $this, $reason, $suppress );
01407 
01408                 $this->lock(); // begin
01409                 $batch->addOld( $archiveName );
01410                 $status = $batch->execute();
01411                 $this->unlock(); // done
01412 
01413                 $this->purgeOldThumbnails( $archiveName );
01414                 if ( $status->isOK() ) {
01415                         $this->purgeDescription();
01416                         $this->purgeHistory();
01417                 }
01418 
01419                 return $status;
01420         }
01421 
01433         function restore( $versions = array(), $unsuppress = false ) {
01434                 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
01435                         return $this->readOnlyFatalStatus();
01436                 }
01437 
01438                 $batch = new LocalFileRestoreBatch( $this, $unsuppress );
01439 
01440                 $this->lock(); // begin
01441                 if ( !$versions ) {
01442                         $batch->addAll();
01443                 } else {
01444                         $batch->addIds( $versions );
01445                 }
01446                 $status = $batch->execute();
01447                 if ( $status->isGood() ) {
01448                         $cleanupStatus = $batch->cleanup();
01449                         $cleanupStatus->successCount = 0;
01450                         $cleanupStatus->failCount = 0;
01451                         $status->merge( $cleanupStatus );
01452                 }
01453                 $this->unlock(); // done
01454 
01455                 return $status;
01456         }
01457 
01467         function getDescriptionUrl() {
01468                 return $this->title->getLocalUrl();
01469         }
01470 
01477         function getDescriptionText() {
01478                 global $wgParser;
01479                 $revision = Revision::newFromTitle( $this->title, false, Revision::READ_NORMAL );
01480                 if ( !$revision ) return false;
01481                 $content = $revision->getContent();
01482                 if ( !$content ) return false;
01483                 $pout = $content->getParserOutput( $this->title, null, new ParserOptions() );
01484                 return $pout->getText();
01485         }
01486 
01490         function getDescription( $audience = self::FOR_PUBLIC, User $user = null ) {
01491                 $this->load();
01492                 if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
01493                         return '';
01494                 } elseif ( $audience == self::FOR_THIS_USER
01495                         && !$this->userCan( self::DELETED_COMMENT, $user ) )
01496                 {
01497                         return '';
01498                 } else {
01499                         return $this->description;
01500                 }
01501         }
01502 
01506         function getTimestamp() {
01507                 $this->load();
01508                 return $this->timestamp;
01509         }
01510 
01514         function getSha1() {
01515                 $this->load();
01516                 // Initialise now if necessary
01517                 if ( $this->sha1 == '' && $this->fileExists ) {
01518                         $this->lock(); // begin
01519 
01520                         $this->sha1 = $this->repo->getFileSha1( $this->getPath() );
01521                         if ( !wfReadOnly() && strval( $this->sha1 ) != '' ) {
01522                                 $dbw = $this->repo->getMasterDB();
01523                                 $dbw->update( 'image',
01524                                         array( 'img_sha1' => $this->sha1 ),
01525                                         array( 'img_name' => $this->getName() ),
01526                                         __METHOD__ );
01527                                 $this->saveToCache();
01528                         }
01529 
01530                         $this->unlock(); // done
01531                 }
01532 
01533                 return $this->sha1;
01534         }
01535 
01539         function isCacheable() {
01540                 $this->load();
01541                 return strlen( $this->metadata ) <= self::CACHE_FIELD_MAX_LEN; // avoid OOMs
01542         }
01543 
01549         function lock() {
01550                 $dbw = $this->repo->getMasterDB();
01551 
01552                 if ( !$this->locked ) {
01553                         $dbw->begin( __METHOD__ );
01554                         $this->locked++;
01555                 }
01556 
01557                 return $dbw->selectField( 'image', '1',
01558                         array( 'img_name' => $this->getName() ), __METHOD__, array( 'FOR UPDATE' ) );
01559         }
01560 
01565         function unlock() {
01566                 if ( $this->locked ) {
01567                         --$this->locked;
01568                         if ( !$this->locked ) {
01569                                 $dbw = $this->repo->getMasterDB();
01570                                 $dbw->commit( __METHOD__ );
01571                         }
01572                 }
01573         }
01574 
01578         function unlockAndRollback() {
01579                 $this->locked = false;
01580                 $dbw = $this->repo->getMasterDB();
01581                 $dbw->rollback( __METHOD__ );
01582         }
01583 
01587         protected function readOnlyFatalStatus() {
01588                 return $this->getRepo()->newFatal( 'filereadonlyerror', $this->getName(),
01589                         $this->getRepo()->getName(), $this->getRepo()->getReadOnlyReason() );
01590         }
01591 } // LocalFile class
01592 
01593 # ------------------------------------------------------------------------------
01594 
01599 class LocalFileDeleteBatch {
01600 
01604         var $file;
01605 
01606         var $reason, $srcRels = array(), $archiveUrls = array(), $deletionBatch, $suppress;
01607         var $status;
01608 
01614         function __construct( File $file, $reason = '', $suppress = false ) {
01615                 $this->file = $file;
01616                 $this->reason = $reason;
01617                 $this->suppress = $suppress;
01618                 $this->status = $file->repo->newGood();
01619         }
01620 
01621         function addCurrent() {
01622                 $this->srcRels['.'] = $this->file->getRel();
01623         }
01624 
01628         function addOld( $oldName ) {
01629                 $this->srcRels[$oldName] = $this->file->getArchiveRel( $oldName );
01630                 $this->archiveUrls[] = $this->file->getArchiveUrl( $oldName );
01631         }
01632 
01637         function addOlds() {
01638                 $archiveNames = array();
01639 
01640                 $dbw = $this->file->repo->getMasterDB();
01641                 $result = $dbw->select( 'oldimage',
01642                         array( 'oi_archive_name' ),
01643                         array( 'oi_name' => $this->file->getName() ),
01644                         __METHOD__
01645                 );
01646 
01647                 foreach ( $result as $row ) {
01648                         $this->addOld( $row->oi_archive_name );
01649                         $archiveNames[] = $row->oi_archive_name;
01650                 }
01651 
01652                 return $archiveNames;
01653         }
01654 
01658         function getOldRels() {
01659                 if ( !isset( $this->srcRels['.'] ) ) {
01660                         $oldRels =& $this->srcRels;
01661                         $deleteCurrent = false;
01662                 } else {
01663                         $oldRels = $this->srcRels;
01664                         unset( $oldRels['.'] );
01665                         $deleteCurrent = true;
01666                 }
01667 
01668                 return array( $oldRels, $deleteCurrent );
01669         }
01670 
01674         protected function getHashes() {
01675                 $hashes = array();
01676                 list( $oldRels, $deleteCurrent ) = $this->getOldRels();
01677 
01678                 if ( $deleteCurrent ) {
01679                         $hashes['.'] = $this->file->getSha1();
01680                 }
01681 
01682                 if ( count( $oldRels ) ) {
01683                         $dbw = $this->file->repo->getMasterDB();
01684                         $res = $dbw->select(
01685                                 'oldimage',
01686                                 array( 'oi_archive_name', 'oi_sha1' ),
01687                                 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')',
01688                                 __METHOD__
01689                         );
01690 
01691                         foreach ( $res as $row ) {
01692                                 if ( rtrim( $row->oi_sha1, "\0" ) === '' ) {
01693                                         // Get the hash from the file
01694                                         $oldUrl = $this->file->getArchiveVirtualUrl( $row->oi_archive_name );
01695                                         $props = $this->file->repo->getFileProps( $oldUrl );
01696 
01697                                         if ( $props['fileExists'] ) {
01698                                                 // Upgrade the oldimage row
01699                                                 $dbw->update( 'oldimage',
01700                                                         array( 'oi_sha1' => $props['sha1'] ),
01701                                                         array( 'oi_name' => $this->file->getName(), 'oi_archive_name' => $row->oi_archive_name ),
01702                                                         __METHOD__ );
01703                                                 $hashes[$row->oi_archive_name] = $props['sha1'];
01704                                         } else {
01705                                                 $hashes[$row->oi_archive_name] = false;
01706                                         }
01707                                 } else {
01708                                         $hashes[$row->oi_archive_name] = $row->oi_sha1;
01709                                 }
01710                         }
01711                 }
01712 
01713                 $missing = array_diff_key( $this->srcRels, $hashes );
01714 
01715                 foreach ( $missing as $name => $rel ) {
01716                         $this->status->error( 'filedelete-old-unregistered', $name );
01717                 }
01718 
01719                 foreach ( $hashes as $name => $hash ) {
01720                         if ( !$hash ) {
01721                                 $this->status->error( 'filedelete-missing', $this->srcRels[$name] );
01722                                 unset( $hashes[$name] );
01723                         }
01724                 }
01725 
01726                 return $hashes;
01727         }
01728 
01729         function doDBInserts() {
01730                 global $wgUser;
01731 
01732                 $dbw = $this->file->repo->getMasterDB();
01733                 $encTimestamp = $dbw->addQuotes( $dbw->timestamp() );
01734                 $encUserId = $dbw->addQuotes( $wgUser->getId() );
01735                 $encReason = $dbw->addQuotes( $this->reason );
01736                 $encGroup = $dbw->addQuotes( 'deleted' );
01737                 $ext = $this->file->getExtension();
01738                 $dotExt = $ext === '' ? '' : ".$ext";
01739                 $encExt = $dbw->addQuotes( $dotExt );
01740                 list( $oldRels, $deleteCurrent ) = $this->getOldRels();
01741 
01742                 // Bitfields to further suppress the content
01743                 if ( $this->suppress ) {
01744                         $bitfield = 0;
01745                         // This should be 15...
01746                         $bitfield |= Revision::DELETED_TEXT;
01747                         $bitfield |= Revision::DELETED_COMMENT;
01748                         $bitfield |= Revision::DELETED_USER;
01749                         $bitfield |= Revision::DELETED_RESTRICTED;
01750                 } else {
01751                         $bitfield = 'oi_deleted';
01752                 }
01753 
01754                 if ( $deleteCurrent ) {
01755                         $concat = $dbw->buildConcat( array( "img_sha1", $encExt ) );
01756                         $where = array( 'img_name' => $this->file->getName() );
01757                         $dbw->insertSelect( 'filearchive', 'image',
01758                                 array(
01759                                         'fa_storage_group' => $encGroup,
01760                                         'fa_storage_key'   => "CASE WHEN img_sha1='' THEN '' ELSE $concat END",
01761                                         'fa_deleted_user'      => $encUserId,
01762                                         'fa_deleted_timestamp' => $encTimestamp,
01763                                         'fa_deleted_reason'    => $encReason,
01764                                         'fa_deleted'           => $this->suppress ? $bitfield : 0,
01765 
01766                                         'fa_name'         => 'img_name',
01767                                         'fa_archive_name' => 'NULL',
01768                                         'fa_size'         => 'img_size',
01769                                         'fa_width'        => 'img_width',
01770                                         'fa_height'       => 'img_height',
01771                                         'fa_metadata'     => 'img_metadata',
01772                                         'fa_bits'         => 'img_bits',
01773                                         'fa_media_type'   => 'img_media_type',
01774                                         'fa_major_mime'   => 'img_major_mime',
01775                                         'fa_minor_mime'   => 'img_minor_mime',
01776                                         'fa_description'  => 'img_description',
01777                                         'fa_user'         => 'img_user',
01778                                         'fa_user_text'    => 'img_user_text',
01779                                         'fa_timestamp'    => 'img_timestamp',
01780                                         'fa_sha1'         => 'img_sha1',
01781                                 ), $where, __METHOD__ );
01782                 }
01783 
01784                 if ( count( $oldRels ) ) {
01785                         $concat = $dbw->buildConcat( array( "oi_sha1", $encExt ) );
01786                         $where = array(
01787                                 'oi_name' => $this->file->getName(),
01788                                 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')' );
01789                         $dbw->insertSelect( 'filearchive', 'oldimage',
01790                                 array(
01791                                         'fa_storage_group' => $encGroup,
01792                                         'fa_storage_key'   => "CASE WHEN oi_sha1='' THEN '' ELSE $concat END",
01793                                         'fa_deleted_user'      => $encUserId,
01794                                         'fa_deleted_timestamp' => $encTimestamp,
01795                                         'fa_deleted_reason'    => $encReason,
01796                                         'fa_deleted'           => $this->suppress ? $bitfield : 'oi_deleted',
01797 
01798                                         'fa_name'         => 'oi_name',
01799                                         'fa_archive_name' => 'oi_archive_name',
01800                                         'fa_size'         => 'oi_size',
01801                                         'fa_width'        => 'oi_width',
01802                                         'fa_height'       => 'oi_height',
01803                                         'fa_metadata'     => 'oi_metadata',
01804                                         'fa_bits'         => 'oi_bits',
01805                                         'fa_media_type'   => 'oi_media_type',
01806                                         'fa_major_mime'   => 'oi_major_mime',
01807                                         'fa_minor_mime'   => 'oi_minor_mime',
01808                                         'fa_description'  => 'oi_description',
01809                                         'fa_user'         => 'oi_user',
01810                                         'fa_user_text'    => 'oi_user_text',
01811                                         'fa_timestamp'    => 'oi_timestamp',
01812                                         'fa_sha1'         => 'oi_sha1',
01813                                 ), $where, __METHOD__ );
01814                 }
01815         }
01816 
01817         function doDBDeletes() {
01818                 $dbw = $this->file->repo->getMasterDB();
01819                 list( $oldRels, $deleteCurrent ) = $this->getOldRels();
01820 
01821                 if ( count( $oldRels ) ) {
01822                         $dbw->delete( 'oldimage',
01823                                 array(
01824                                         'oi_name' => $this->file->getName(),
01825                                         'oi_archive_name' => array_keys( $oldRels )
01826                                 ), __METHOD__ );
01827                 }
01828 
01829                 if ( $deleteCurrent ) {
01830                         $dbw->delete( 'image', array( 'img_name' => $this->file->getName() ), __METHOD__ );
01831                 }
01832         }
01833 
01838         function execute() {
01839                 wfProfileIn( __METHOD__ );
01840 
01841                 $this->file->lock();
01842                 // Leave private files alone
01843                 $privateFiles = array();
01844                 list( $oldRels, $deleteCurrent ) = $this->getOldRels();
01845                 $dbw = $this->file->repo->getMasterDB();
01846 
01847                 if ( !empty( $oldRels ) ) {
01848                         $res = $dbw->select( 'oldimage',
01849                                 array( 'oi_archive_name' ),
01850                                 array( 'oi_name' => $this->file->getName(),
01851                                         'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')',
01852                                         $dbw->bitAnd( 'oi_deleted', File::DELETED_FILE ) => File::DELETED_FILE ),
01853                                 __METHOD__ );
01854 
01855                         foreach ( $res as $row ) {
01856                                 $privateFiles[$row->oi_archive_name] = 1;
01857                         }
01858                 }
01859                 // Prepare deletion batch
01860                 $hashes = $this->getHashes();
01861                 $this->deletionBatch = array();
01862                 $ext = $this->file->getExtension();
01863                 $dotExt = $ext === '' ? '' : ".$ext";
01864 
01865                 foreach ( $this->srcRels as $name => $srcRel ) {
01866                         // Skip files that have no hash (missing source).
01867                         // Keep private files where they are.
01868                         if ( isset( $hashes[$name] ) && !array_key_exists( $name, $privateFiles ) ) {
01869                                 $hash = $hashes[$name];
01870                                 $key = $hash . $dotExt;
01871                                 $dstRel = $this->file->repo->getDeletedHashPath( $key ) . $key;
01872                                 $this->deletionBatch[$name] = array( $srcRel, $dstRel );
01873                         }
01874                 }
01875 
01876                 // Lock the filearchive rows so that the files don't get deleted by a cleanup operation
01877                 // We acquire this lock by running the inserts now, before the file operations.
01878                 //
01879                 // This potentially has poor lock contention characteristics -- an alternative
01880                 // scheme would be to insert stub filearchive entries with no fa_name and commit
01881                 // them in a separate transaction, then run the file ops, then update the fa_name fields.
01882                 $this->doDBInserts();
01883 
01884                 // Removes non-existent file from the batch, so we don't get errors.
01885                 $this->deletionBatch = $this->removeNonexistentFiles( $this->deletionBatch );
01886 
01887                 // Execute the file deletion batch
01888                 $status = $this->file->repo->deleteBatch( $this->deletionBatch );
01889 
01890                 if ( !$status->isGood() ) {
01891                         $this->status->merge( $status );
01892                 }
01893 
01894                 if ( !$this->status->isOK() ) {
01895                         // Critical file deletion error
01896                         // Roll back inserts, release lock and abort
01897                         // TODO: delete the defunct filearchive rows if we are using a non-transactional DB
01898                         $this->file->unlockAndRollback();
01899                         wfProfileOut( __METHOD__ );
01900                         return $this->status;
01901                 }
01902 
01903                 // Delete image/oldimage rows
01904                 $this->doDBDeletes();
01905 
01906                 // Commit and return
01907                 $this->file->unlock();
01908                 wfProfileOut( __METHOD__ );
01909 
01910                 return $this->status;
01911         }
01912 
01918         function removeNonexistentFiles( $batch ) {
01919                 $files = $newBatch = array();
01920 
01921                 foreach ( $batch as $batchItem ) {
01922                         list( $src, $dest ) = $batchItem;
01923                         $files[$src] = $this->file->repo->getVirtualUrl( 'public' ) . '/' . rawurlencode( $src );
01924                 }
01925 
01926                 $result = $this->file->repo->fileExistsBatch( $files );
01927 
01928                 foreach ( $batch as $batchItem ) {
01929                         if ( $result[$batchItem[0]] ) {
01930                                 $newBatch[] = $batchItem;
01931                         }
01932                 }
01933 
01934                 return $newBatch;
01935         }
01936 }
01937 
01938 # ------------------------------------------------------------------------------
01939 
01944 class LocalFileRestoreBatch {
01948         var $file;
01949 
01950         var $cleanupBatch, $ids, $all, $unsuppress = false;
01951 
01956         function __construct( File $file, $unsuppress = false ) {
01957                 $this->file = $file;
01958                 $this->cleanupBatch = $this->ids = array();
01959                 $this->ids = array();
01960                 $this->unsuppress = $unsuppress;
01961         }
01962 
01966         function addId( $fa_id ) {
01967                 $this->ids[] = $fa_id;
01968         }
01969 
01973         function addIds( $ids ) {
01974                 $this->ids = array_merge( $this->ids, $ids );
01975         }
01976 
01980         function addAll() {
01981                 $this->all = true;
01982         }
01983 
01992         function execute() {
01993                 global $wgLang;
01994 
01995                 if ( !$this->all && !$this->ids ) {
01996                         // Do nothing
01997                         return $this->file->repo->newGood();
01998                 }
01999 
02000                 $exists = $this->file->lock();
02001                 $dbw = $this->file->repo->getMasterDB();
02002                 $status = $this->file->repo->newGood();
02003 
02004                 // Fetch all or selected archived revisions for the file,
02005                 // sorted from the most recent to the oldest.
02006                 $conditions = array( 'fa_name' => $this->file->getName() );
02007 
02008                 if ( !$this->all ) {
02009                         $conditions[] = 'fa_id IN (' . $dbw->makeList( $this->ids ) . ')';
02010                 }
02011 
02012                 $result = $dbw->select( 'filearchive', '*',
02013                         $conditions,
02014                         __METHOD__,
02015                         array( 'ORDER BY' => 'fa_timestamp DESC' )
02016                 );
02017 
02018                 $idsPresent = array();
02019                 $storeBatch = array();
02020                 $insertBatch = array();
02021                 $insertCurrent = false;
02022                 $deleteIds = array();
02023                 $first = true;
02024                 $archiveNames = array();
02025 
02026                 foreach ( $result as $row ) {
02027                         $idsPresent[] = $row->fa_id;
02028 
02029                         if ( $row->fa_name != $this->file->getName() ) {
02030                                 $status->error( 'undelete-filename-mismatch', $wgLang->timeanddate( $row->fa_timestamp ) );
02031                                 $status->failCount++;
02032                                 continue;
02033                         }
02034 
02035                         if ( $row->fa_storage_key == '' ) {
02036                                 // Revision was missing pre-deletion
02037                                 $status->error( 'undelete-bad-store-key', $wgLang->timeanddate( $row->fa_timestamp ) );
02038                                 $status->failCount++;
02039                                 continue;
02040                         }
02041 
02042                         $deletedRel = $this->file->repo->getDeletedHashPath( $row->fa_storage_key ) . $row->fa_storage_key;
02043                         $deletedUrl = $this->file->repo->getVirtualUrl() . '/deleted/' . $deletedRel;
02044 
02045                         if( isset( $row->fa_sha1 ) ) {
02046                                 $sha1 = $row->fa_sha1;
02047                         } else {
02048                                 // old row, populate from key
02049                                 $sha1 = LocalRepo::getHashFromKey( $row->fa_storage_key );
02050                         }
02051 
02052                         # Fix leading zero
02053                         if ( strlen( $sha1 ) == 32 && $sha1[0] == '0' ) {
02054                                 $sha1 = substr( $sha1, 1 );
02055                         }
02056 
02057                         if ( is_null( $row->fa_major_mime ) || $row->fa_major_mime == 'unknown'
02058                                 || is_null( $row->fa_minor_mime ) || $row->fa_minor_mime == 'unknown'
02059                                 || is_null( $row->fa_media_type ) || $row->fa_media_type == 'UNKNOWN'
02060                                 || is_null( $row->fa_metadata ) ) {
02061                                 // Refresh our metadata
02062                                 // Required for a new current revision; nice for older ones too. :)
02063                                 $props = RepoGroup::singleton()->getFileProps( $deletedUrl );
02064                         } else {
02065                                 $props = array(
02066                                         'minor_mime' => $row->fa_minor_mime,
02067                                         'major_mime' => $row->fa_major_mime,
02068                                         'media_type' => $row->fa_media_type,
02069                                         'metadata'   => $row->fa_metadata
02070                                 );
02071                         }
02072 
02073                         if ( $first && !$exists ) {
02074                                 // This revision will be published as the new current version
02075                                 $destRel = $this->file->getRel();
02076                                 $insertCurrent = array(
02077                                         'img_name'        => $row->fa_name,
02078                                         'img_size'        => $row->fa_size,
02079                                         'img_width'       => $row->fa_width,
02080                                         'img_height'      => $row->fa_height,
02081                                         'img_metadata'    => $props['metadata'],
02082                                         'img_bits'        => $row->fa_bits,
02083                                         'img_media_type'  => $props['media_type'],
02084                                         'img_major_mime'  => $props['major_mime'],
02085                                         'img_minor_mime'  => $props['minor_mime'],
02086                                         'img_description' => $row->fa_description,
02087                                         'img_user'        => $row->fa_user,
02088                                         'img_user_text'   => $row->fa_user_text,
02089                                         'img_timestamp'   => $row->fa_timestamp,
02090                                         'img_sha1'        => $sha1
02091                                 );
02092 
02093                                 // The live (current) version cannot be hidden!
02094                                 if ( !$this->unsuppress && $row->fa_deleted ) {
02095                                         $storeBatch[] = array( $deletedUrl, 'public', $destRel );
02096                                         $this->cleanupBatch[] = $row->fa_storage_key;
02097                                 }
02098                         } else {
02099                                 $archiveName = $row->fa_archive_name;
02100 
02101                                 if ( $archiveName == '' ) {
02102                                         // This was originally a current version; we
02103                                         // have to devise a new archive name for it.
02104                                         // Format is <timestamp of archiving>!<name>
02105                                         $timestamp = wfTimestamp( TS_UNIX, $row->fa_deleted_timestamp );
02106 
02107                                         do {
02108                                                 $archiveName = wfTimestamp( TS_MW, $timestamp ) . '!' . $row->fa_name;
02109                                                 $timestamp++;
02110                                         } while ( isset( $archiveNames[$archiveName] ) );
02111                                 }
02112 
02113                                 $archiveNames[$archiveName] = true;
02114                                 $destRel = $this->file->getArchiveRel( $archiveName );
02115                                 $insertBatch[] = array(
02116                                         'oi_name'         => $row->fa_name,
02117                                         'oi_archive_name' => $archiveName,
02118                                         'oi_size'         => $row->fa_size,
02119                                         'oi_width'        => $row->fa_width,
02120                                         'oi_height'       => $row->fa_height,
02121                                         'oi_bits'         => $row->fa_bits,
02122                                         'oi_description'  => $row->fa_description,
02123                                         'oi_user'         => $row->fa_user,
02124                                         'oi_user_text'    => $row->fa_user_text,
02125                                         'oi_timestamp'    => $row->fa_timestamp,
02126                                         'oi_metadata'     => $props['metadata'],
02127                                         'oi_media_type'   => $props['media_type'],
02128                                         'oi_major_mime'   => $props['major_mime'],
02129                                         'oi_minor_mime'   => $props['minor_mime'],
02130                                         'oi_deleted'      => $this->unsuppress ? 0 : $row->fa_deleted,
02131                                         'oi_sha1'         => $sha1 );
02132                         }
02133 
02134                         $deleteIds[] = $row->fa_id;
02135 
02136                         if ( !$this->unsuppress && $row->fa_deleted & File::DELETED_FILE ) {
02137                                 // private files can stay where they are
02138                                 $status->successCount++;
02139                         } else {
02140                                 $storeBatch[] = array( $deletedUrl, 'public', $destRel );
02141                                 $this->cleanupBatch[] = $row->fa_storage_key;
02142                         }
02143 
02144                         $first = false;
02145                 }
02146 
02147                 unset( $result );
02148 
02149                 // Add a warning to the status object for missing IDs
02150                 $missingIds = array_diff( $this->ids, $idsPresent );
02151 
02152                 foreach ( $missingIds as $id ) {
02153                         $status->error( 'undelete-missing-filearchive', $id );
02154                 }
02155 
02156                 // Remove missing files from batch, so we don't get errors when undeleting them
02157                 $storeBatch = $this->removeNonexistentFiles( $storeBatch );
02158 
02159                 // Run the store batch
02160                 // Use the OVERWRITE_SAME flag to smooth over a common error
02161                 $storeStatus = $this->file->repo->storeBatch( $storeBatch, FileRepo::OVERWRITE_SAME );
02162                 $status->merge( $storeStatus );
02163 
02164                 if ( !$status->isGood() ) {
02165                         // Even if some files could be copied, fail entirely as that is the
02166                         // easiest thing to do without data loss
02167                         $this->cleanupFailedBatch( $storeStatus, $storeBatch );
02168                         $status->ok = false;
02169                         $this->file->unlock();
02170 
02171                         return $status;
02172                 }
02173 
02174                 // Run the DB updates
02175                 // Because we have locked the image row, key conflicts should be rare.
02176                 // If they do occur, we can roll back the transaction at this time with
02177                 // no data loss, but leaving unregistered files scattered throughout the
02178                 // public zone.
02179                 // This is not ideal, which is why it's important to lock the image row.
02180                 if ( $insertCurrent ) {
02181                         $dbw->insert( 'image', $insertCurrent, __METHOD__ );
02182                 }
02183 
02184                 if ( $insertBatch ) {
02185                         $dbw->insert( 'oldimage', $insertBatch, __METHOD__ );
02186                 }
02187 
02188                 if ( $deleteIds ) {
02189                         $dbw->delete( 'filearchive',
02190                                 array( 'fa_id IN (' . $dbw->makeList( $deleteIds ) . ')' ),
02191                                 __METHOD__ );
02192                 }
02193 
02194                 // If store batch is empty (all files are missing), deletion is to be considered successful
02195                 if ( $status->successCount > 0 || !$storeBatch ) {
02196                         if ( !$exists ) {
02197                                 wfDebug( __METHOD__ . " restored {$status->successCount} items, creating a new current\n" );
02198 
02199                                 DeferredUpdates::addUpdate( SiteStatsUpdate::factory( array( 'images' => 1 ) ) );
02200 
02201                                 $this->file->purgeEverything();
02202                         } else {
02203                                 wfDebug( __METHOD__ . " restored {$status->successCount} as archived versions\n" );
02204                                 $this->file->purgeDescription();
02205                                 $this->file->purgeHistory();
02206                         }
02207                 }
02208 
02209                 $this->file->unlock();
02210 
02211                 return $status;
02212         }
02213 
02219         function removeNonexistentFiles( $triplets ) {
02220                 $files = $filteredTriplets = array();
02221                 foreach ( $triplets as $file ) {
02222                         $files[$file[0]] = $file[0];
02223                 }
02224 
02225                 $result = $this->file->repo->fileExistsBatch( $files );
02226 
02227                 foreach ( $triplets as $file ) {
02228                         if ( $result[$file[0]] ) {
02229                                 $filteredTriplets[] = $file;
02230                         }
02231                 }
02232 
02233                 return $filteredTriplets;
02234         }
02235 
02241         function removeNonexistentFromCleanup( $batch ) {
02242                 $files = $newBatch = array();
02243                 $repo = $this->file->repo;
02244 
02245                 foreach ( $batch as $file ) {
02246                         $files[$file] = $repo->getVirtualUrl( 'deleted' ) . '/' .
02247                                 rawurlencode( $repo->getDeletedHashPath( $file ) . $file );
02248                 }
02249 
02250                 $result = $repo->fileExistsBatch( $files );
02251 
02252                 foreach ( $batch as $file ) {
02253                         if ( $result[$file] ) {
02254                                 $newBatch[] = $file;
02255                         }
02256                 }
02257 
02258                 return $newBatch;
02259         }
02260 
02266         function cleanup() {
02267                 if ( !$this->cleanupBatch ) {
02268                         return $this->file->repo->newGood();
02269                 }
02270 
02271                 $this->cleanupBatch = $this->removeNonexistentFromCleanup( $this->cleanupBatch );
02272 
02273                 $status = $this->file->repo->cleanupDeletedBatch( $this->cleanupBatch );
02274 
02275                 return $status;
02276         }
02277 
02285         function cleanupFailedBatch( $storeStatus, $storeBatch ) {
02286                 $cleanupBatch = array();
02287 
02288                 foreach ( $storeStatus->success as $i => $success ) {
02289                         // Check if this item of the batch was successfully copied
02290                         if ( $success ) {
02291                                 // Item was successfully copied and needs to be removed again
02292                                 // Extract ($dstZone, $dstRel) from the batch
02293                                 $cleanupBatch[] = array( $storeBatch[$i][1], $storeBatch[$i][2] );
02294                         }
02295                 }
02296                 $this->file->repo->cleanupBatch( $cleanupBatch );
02297         }
02298 }
02299 
02300 # ------------------------------------------------------------------------------
02301 
02306 class LocalFileMoveBatch {
02307 
02311         var $file;
02312 
02316         var $target;
02317 
02318         var $cur, $olds, $oldCount, $archive;
02319 
02323         var $db;
02324 
02329         function __construct( File $file, Title $target ) {
02330                 $this->file = $file;
02331                 $this->target = $target;
02332                 $this->oldHash = $this->file->repo->getHashPath( $this->file->getName() );
02333                 $this->newHash = $this->file->repo->getHashPath( $this->target->getDBkey() );
02334                 $this->oldName = $this->file->getName();
02335                 $this->newName = $this->file->repo->getNameFromTitle( $this->target );
02336                 $this->oldRel = $this->oldHash . $this->oldName;
02337                 $this->newRel = $this->newHash . $this->newName;
02338                 $this->db = $file->getRepo()->getMasterDb();
02339         }
02340 
02344         function addCurrent() {
02345                 $this->cur = array( $this->oldRel, $this->newRel );
02346         }
02347 
02352         function addOlds() {
02353                 $archiveBase = 'archive';
02354                 $this->olds = array();
02355                 $this->oldCount = 0;
02356                 $archiveNames = array();
02357 
02358                 $result = $this->db->select( 'oldimage',
02359                         array( 'oi_archive_name', 'oi_deleted' ),
02360                         array( 'oi_name' => $this->oldName ),
02361                         __METHOD__
02362                 );
02363 
02364                 foreach ( $result as $row ) {
02365                         $archiveNames[] = $row->oi_archive_name;
02366                         $oldName = $row->oi_archive_name;
02367                         $bits = explode( '!', $oldName, 2 );
02368 
02369                         if ( count( $bits ) != 2 ) {
02370                                 wfDebug( "Old file name missing !: '$oldName' \n" );
02371                                 continue;
02372                         }
02373 
02374                         list( $timestamp, $filename ) = $bits;
02375 
02376                         if ( $this->oldName != $filename ) {
02377                                 wfDebug( "Old file name doesn't match: '$oldName' \n" );
02378                                 continue;
02379                         }
02380 
02381                         $this->oldCount++;
02382 
02383                         // Do we want to add those to oldCount?
02384                         if ( $row->oi_deleted & File::DELETED_FILE ) {
02385                                 continue;
02386                         }
02387 
02388                         $this->olds[] = array(
02389                                 "{$archiveBase}/{$this->oldHash}{$oldName}",
02390                                 "{$archiveBase}/{$this->newHash}{$timestamp}!{$this->newName}"
02391                         );
02392                 }
02393 
02394                 return $archiveNames;
02395         }
02396 
02401         function execute() {
02402                 $repo = $this->file->repo;
02403                 $status = $repo->newGood();
02404 
02405                 $triplets = $this->getMoveTriplets();
02406                 $triplets = $this->removeNonexistentFiles( $triplets );
02407 
02408                 $this->file->lock(); // begin
02409                 // Rename the file versions metadata in the DB.
02410                 // This implicitly locks the destination file, which avoids race conditions.
02411                 // If we moved the files from A -> C before DB updates, another process could
02412                 // move files from B -> C at this point, causing storeBatch() to fail and thus
02413                 // cleanupTarget() to trigger. It would delete the C files and cause data loss.
02414                 $statusDb = $this->doDBUpdates();
02415                 if ( !$statusDb->isGood() ) {
02416                         $this->file->unlockAndRollback();
02417                         $statusDb->ok = false;
02418                         return $statusDb;
02419                 }
02420                 wfDebugLog( 'imagemove', "Renamed {$this->file->getName()} in database: {$statusDb->successCount} successes, {$statusDb->failCount} failures" );
02421 
02422                 // Copy the files into their new location.
02423                 // If a prior process fataled copying or cleaning up files we tolerate any
02424                 // of the existing files if they are identical to the ones being stored.
02425                 $statusMove = $repo->storeBatch( $triplets, FileRepo::OVERWRITE_SAME );
02426                 wfDebugLog( 'imagemove', "Moved files for {$this->file->getName()}: {$statusMove->successCount} successes, {$statusMove->failCount} failures" );
02427                 if ( !$statusMove->isGood() ) {
02428                         // Delete any files copied over (while the destination is still locked)
02429                         $this->cleanupTarget( $triplets );
02430                         $this->file->unlockAndRollback(); // unlocks the destination
02431                         wfDebugLog( 'imagemove', "Error in moving files: " . $statusMove->getWikiText() );
02432                         $statusMove->ok = false;
02433                         return $statusMove;
02434                 }
02435                 $this->file->unlock(); // done
02436 
02437                 // Everything went ok, remove the source files
02438                 $this->cleanupSource( $triplets );
02439 
02440                 $status->merge( $statusDb );
02441                 $status->merge( $statusMove );
02442 
02443                 return $status;
02444         }
02445 
02452         function doDBUpdates() {
02453                 $repo = $this->file->repo;
02454                 $status = $repo->newGood();
02455                 $dbw = $this->db;
02456 
02457                 // Update current image
02458                 $dbw->update(
02459                         'image',
02460                         array( 'img_name' => $this->newName ),
02461                         array( 'img_name' => $this->oldName ),
02462                         __METHOD__
02463                 );
02464 
02465                 if ( $dbw->affectedRows() ) {
02466                         $status->successCount++;
02467                 } else {
02468                         $status->failCount++;
02469                         $status->fatal( 'imageinvalidfilename' );
02470                         return $status;
02471                 }
02472 
02473                 // Update old images
02474                 $dbw->update(
02475                         'oldimage',
02476                         array(
02477                                 'oi_name' => $this->newName,
02478                                 'oi_archive_name = ' . $dbw->strreplace( 'oi_archive_name',
02479                                         $dbw->addQuotes( $this->oldName ), $dbw->addQuotes( $this->newName ) ),
02480                         ),
02481                         array( 'oi_name' => $this->oldName ),
02482                         __METHOD__
02483                 );
02484 
02485                 $affected = $dbw->affectedRows();
02486                 $total = $this->oldCount;
02487                 $status->successCount += $affected;
02488                 // Bug 34934: $total is based on files that actually exist.
02489                 // There may be more DB rows than such files, in which case $affected
02490                 // can be greater than $total. We use max() to avoid negatives here.
02491                 $status->failCount += max( 0, $total - $affected );
02492                 if ( $status->failCount ) {
02493                         $status->error( 'imageinvalidfilename' );
02494                 }
02495 
02496                 return $status;
02497         }
02498 
02503         function getMoveTriplets() {
02504                 $moves = array_merge( array( $this->cur ), $this->olds );
02505                 $triplets = array();    // The format is: (srcUrl, destZone, destUrl)
02506 
02507                 foreach ( $moves as $move ) {
02508                         // $move: (oldRelativePath, newRelativePath)
02509                         $srcUrl = $this->file->repo->getVirtualUrl() . '/public/' . rawurlencode( $move[0] );
02510                         $triplets[] = array( $srcUrl, 'public', $move[1] );
02511                         wfDebugLog( 'imagemove', "Generated move triplet for {$this->file->getName()}: {$srcUrl} :: public :: {$move[1]}" );
02512                 }
02513 
02514                 return $triplets;
02515         }
02516 
02522         function removeNonexistentFiles( $triplets ) {
02523                 $files = array();
02524 
02525                 foreach ( $triplets as $file ) {
02526                         $files[$file[0]] = $file[0];
02527                 }
02528 
02529                 $result = $this->file->repo->fileExistsBatch( $files );
02530                 $filteredTriplets = array();
02531 
02532                 foreach ( $triplets as $file ) {
02533                         if ( $result[$file[0]] ) {
02534                                 $filteredTriplets[] = $file;
02535                         } else {
02536                                 wfDebugLog( 'imagemove', "File {$file[0]} does not exist" );
02537                         }
02538                 }
02539 
02540                 return $filteredTriplets;
02541         }
02542 
02547         function cleanupTarget( $triplets ) {
02548                 // Create dest pairs from the triplets
02549                 $pairs = array();
02550                 foreach ( $triplets as $triplet ) {
02551                         // $triplet: (old source virtual URL, dst zone, dest rel)
02552                         $pairs[] = array( $triplet[1], $triplet[2] );
02553                 }
02554 
02555                 $this->file->repo->cleanupBatch( $pairs );
02556         }
02557 
02562         function cleanupSource( $triplets ) {
02563                 // Create source file names from the triplets
02564                 $files = array();
02565                 foreach ( $triplets as $triplet ) {
02566                         $files[] = $triplet[0];
02567                 }
02568 
02569                 $this->file->repo->cleanupBatch( $files );
02570         }
02571 }