MediaWiki  REL1_20
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                                 $purgeList[] = "{$dir}/{$file}";
00819                         }
00820                 }
00821 
00822                 # Delete the thumbnails
00823                 $this->repo->quickPurgeBatch( $purgeList );
00824                 # Clear out the thumbnail directory if empty
00825                 $this->repo->quickCleanDir( $dir );
00826         }
00827 
00838         function getHistory( $limit = null, $start = null, $end = null, $inc = true ) {
00839                 $dbr = $this->repo->getSlaveDB();
00840                 $tables = array( 'oldimage' );
00841                 $fields = OldLocalFile::selectFields();
00842                 $conds = $opts = $join_conds = array();
00843                 $eq = $inc ? '=' : '';
00844                 $conds[] = "oi_name = " . $dbr->addQuotes( $this->title->getDBkey() );
00845 
00846                 if ( $start ) {
00847                         $conds[] = "oi_timestamp <$eq " . $dbr->addQuotes( $dbr->timestamp( $start ) );
00848                 }
00849 
00850                 if ( $end ) {
00851                         $conds[] = "oi_timestamp >$eq " . $dbr->addQuotes( $dbr->timestamp( $end ) );
00852                 }
00853 
00854                 if ( $limit ) {
00855                         $opts['LIMIT'] = $limit;
00856                 }
00857 
00858                 // Search backwards for time > x queries
00859                 $order = ( !$start && $end !== null ) ? 'ASC' : 'DESC';
00860                 $opts['ORDER BY'] = "oi_timestamp $order";
00861                 $opts['USE INDEX'] = array( 'oldimage' => 'oi_name_timestamp' );
00862 
00863                 wfRunHooks( 'LocalFile::getHistory', array( &$this, &$tables, &$fields,
00864                         &$conds, &$opts, &$join_conds ) );
00865 
00866                 $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $opts, $join_conds );
00867                 $r = array();
00868 
00869                 foreach ( $res as $row ) {
00870                         if ( $this->repo->oldFileFromRowFactory ) {
00871                                 $r[] = call_user_func( $this->repo->oldFileFromRowFactory, $row, $this->repo );
00872                         } else {
00873                                 $r[] = OldLocalFile::newFromRow( $row, $this->repo );
00874                         }
00875                 }
00876 
00877                 if ( $order == 'ASC' ) {
00878                         $r = array_reverse( $r ); // make sure it ends up descending
00879                 }
00880 
00881                 return $r;
00882         }
00883 
00893         public function nextHistoryLine() {
00894                 # Polymorphic function name to distinguish foreign and local fetches
00895                 $fname = get_class( $this ) . '::' . __FUNCTION__;
00896 
00897                 $dbr = $this->repo->getSlaveDB();
00898 
00899                 if ( $this->historyLine == 0 ) {// called for the first time, return line from cur
00900                         $this->historyRes = $dbr->select( 'image',
00901                                 array(
00902                                         '*',
00903                                         "'' AS oi_archive_name",
00904                                         '0 as oi_deleted',
00905                                         'img_sha1'
00906                                 ),
00907                                 array( 'img_name' => $this->title->getDBkey() ),
00908                                 $fname
00909                         );
00910 
00911                         if ( 0 == $dbr->numRows( $this->historyRes ) ) {
00912                                 $this->historyRes = null;
00913                                 return false;
00914                         }
00915                 } elseif ( $this->historyLine == 1 ) {
00916                         $this->historyRes = $dbr->select( 'oldimage', '*',
00917                                 array( 'oi_name' => $this->title->getDBkey() ),
00918                                 $fname,
00919                                 array( 'ORDER BY' => 'oi_timestamp DESC' )
00920                         );
00921                 }
00922                 $this->historyLine ++;
00923 
00924                 return $dbr->fetchObject( $this->historyRes );
00925         }
00926 
00930         public function resetHistory() {
00931                 $this->historyLine = 0;
00932 
00933                 if ( !is_null( $this->historyRes ) ) {
00934                         $this->historyRes = null;
00935                 }
00936         }
00937 
00966         function upload( $srcPath, $comment, $pageText, $flags = 0, $props = false, $timestamp = false, $user = null ) {
00967                 global $wgContLang;
00968 
00969                 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
00970                         return $this->readOnlyFatalStatus();
00971                 }
00972 
00973                 // truncate nicely or the DB will do it for us
00974                 // non-nicely (dangling multi-byte chars, non-truncated version in cache).
00975                 $comment = $wgContLang->truncate( $comment, 255 );
00976                 $this->lock(); // begin
00977                 $status = $this->publish( $srcPath, $flags );
00978 
00979                 if ( $status->successCount > 0 ) {
00980                         # Essentially we are displacing any existing current file and saving
00981                         # a new current file at the old location. If just the first succeeded,
00982                         # we still need to displace the current DB entry and put in a new one.
00983                         if ( !$this->recordUpload2( $status->value, $comment, $pageText, $props, $timestamp, $user ) ) {
00984                                 $status->fatal( 'filenotfound', $srcPath );
00985                         }
00986                 }
00987 
00988                 $this->unlock(); // done
00989 
00990                 return $status;
00991         }
00992 
01004         function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
01005                 $watch = false, $timestamp = false )
01006         {
01007                 $pageText = SpecialUpload::getInitialPageText( $desc, $license, $copyStatus, $source );
01008 
01009                 if ( !$this->recordUpload2( $oldver, $desc, $pageText ) ) {
01010                         return false;
01011                 }
01012 
01013                 if ( $watch ) {
01014                         global $wgUser;
01015                         $wgUser->addWatch( $this->getTitle() );
01016                 }
01017                 return true;
01018         }
01019 
01030         function recordUpload2(
01031                 $oldver, $comment, $pageText, $props = false, $timestamp = false, $user = null
01032         ) {
01033                 wfProfileIn( __METHOD__ );
01034 
01035                 if ( is_null( $user ) ) {
01036                         global $wgUser;
01037                         $user = $wgUser;
01038                 }
01039 
01040                 $dbw = $this->repo->getMasterDB();
01041                 $dbw->begin( __METHOD__ );
01042 
01043                 if ( !$props ) {
01044                         wfProfileIn( __METHOD__ . '-getProps' );
01045                         $props = $this->repo->getFileProps( $this->getVirtualUrl() );
01046                         wfProfileOut( __METHOD__ . '-getProps' );
01047                 }
01048 
01049                 if ( $timestamp === false ) {
01050                         $timestamp = $dbw->timestamp();
01051                 }
01052 
01053                 $props['description'] = $comment;
01054                 $props['user'] = $user->getId();
01055                 $props['user_text'] = $user->getName();
01056                 $props['timestamp'] = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
01057                 $this->setProps( $props );
01058 
01059                 # Fail now if the file isn't there
01060                 if ( !$this->fileExists ) {
01061                         wfDebug( __METHOD__ . ": File " . $this->getRel() . " went missing!\n" );
01062                         wfProfileOut( __METHOD__ );
01063                         return false;
01064                 }
01065 
01066                 $reupload = false;
01067 
01068                 # Test to see if the row exists using INSERT IGNORE
01069                 # This avoids race conditions by locking the row until the commit, and also
01070                 # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
01071                 $dbw->insert( 'image',
01072                         array(
01073                                 'img_name'        => $this->getName(),
01074                                 'img_size'        => $this->size,
01075                                 'img_width'       => intval( $this->width ),
01076                                 'img_height'      => intval( $this->height ),
01077                                 'img_bits'        => $this->bits,
01078                                 'img_media_type'  => $this->media_type,
01079                                 'img_major_mime'  => $this->major_mime,
01080                                 'img_minor_mime'  => $this->minor_mime,
01081                                 'img_timestamp'   => $timestamp,
01082                                 'img_description' => $comment,
01083                                 'img_user'        => $user->getId(),
01084                                 'img_user_text'   => $user->getName(),
01085                                 'img_metadata'    => $this->metadata,
01086                                 'img_sha1'        => $this->sha1
01087                         ),
01088                         __METHOD__,
01089                         'IGNORE'
01090                 );
01091                 if ( $dbw->affectedRows() == 0 ) {
01092                         # (bug 34993) Note: $oldver can be empty here, if the previous
01093                         # version of the file was broken. Allow registration of the new
01094                         # version to continue anyway, because that's better than having
01095                         # an image that's not fixable by user operations.
01096 
01097                         $reupload = true;
01098                         # Collision, this is an update of a file
01099                         # Insert previous contents into oldimage
01100                         $dbw->insertSelect( 'oldimage', 'image',
01101                                 array(
01102                                         'oi_name'         => 'img_name',
01103                                         'oi_archive_name' => $dbw->addQuotes( $oldver ),
01104                                         'oi_size'         => 'img_size',
01105                                         'oi_width'        => 'img_width',
01106                                         'oi_height'       => 'img_height',
01107                                         'oi_bits'         => 'img_bits',
01108                                         'oi_timestamp'    => 'img_timestamp',
01109                                         'oi_description'  => 'img_description',
01110                                         'oi_user'         => 'img_user',
01111                                         'oi_user_text'    => 'img_user_text',
01112                                         'oi_metadata'     => 'img_metadata',
01113                                         'oi_media_type'   => 'img_media_type',
01114                                         'oi_major_mime'   => 'img_major_mime',
01115                                         'oi_minor_mime'   => 'img_minor_mime',
01116                                         'oi_sha1'         => 'img_sha1'
01117                                 ),
01118                                 array( 'img_name' => $this->getName() ),
01119                                 __METHOD__
01120                         );
01121 
01122                         # Update the current image row
01123                         $dbw->update( 'image',
01124                                 array( /* SET */
01125                                         'img_size'        => $this->size,
01126                                         'img_width'       => intval( $this->width ),
01127                                         'img_height'      => intval( $this->height ),
01128                                         'img_bits'        => $this->bits,
01129                                         'img_media_type'  => $this->media_type,
01130                                         'img_major_mime'  => $this->major_mime,
01131                                         'img_minor_mime'  => $this->minor_mime,
01132                                         'img_timestamp'   => $timestamp,
01133                                         'img_description' => $comment,
01134                                         'img_user'        => $user->getId(),
01135                                         'img_user_text'   => $user->getName(),
01136                                         'img_metadata'    => $this->metadata,
01137                                         'img_sha1'        => $this->sha1
01138                                 ),
01139                                 array( 'img_name' => $this->getName() ),
01140                                 __METHOD__
01141                         );
01142                 } else {
01143                         # This is a new file, so update the image count
01144                         DeferredUpdates::addUpdate( SiteStatsUpdate::factory( array( 'images' => 1 ) ) );
01145                 }
01146 
01147                 $descTitle = $this->getTitle();
01148                 $wikiPage = new WikiFilePage( $descTitle );
01149                 $wikiPage->setFile( $this );
01150 
01151                 # Add the log entry
01152                 $log = new LogPage( 'upload' );
01153                 $action = $reupload ? 'overwrite' : 'upload';
01154                 $logId = $log->addEntry( $action, $descTitle, $comment, array(), $user );
01155 
01156                 wfProfileIn( __METHOD__ . '-edit' );
01157                 $exists = $descTitle->exists();
01158 
01159                 if ( $exists ) {
01160                         # Create a null revision
01161                         $latest = $descTitle->getLatestRevID();
01162                         $nullRevision = Revision::newNullRevision(
01163                                 $dbw,
01164                                 $descTitle->getArticleID(),
01165                                 $log->getRcComment(),
01166                                 false
01167                         );
01168                         if (!is_null($nullRevision)) {
01169                                 $nullRevision->insertOn( $dbw );
01170 
01171                                 wfRunHooks( 'NewRevisionFromEditComplete', array( $wikiPage, $nullRevision, $latest, $user ) );
01172                                 $wikiPage->updateRevisionOn( $dbw, $nullRevision );
01173                         }
01174                 }
01175 
01176                 # Commit the transaction now, in case something goes wrong later
01177                 # The most important thing is that files don't get lost, especially archives
01178                 # NOTE: once we have support for nested transactions, the commit may be moved
01179                 #       to after $wikiPage->doEdit has been called.
01180                 $dbw->commit( __METHOD__ );
01181 
01182                 if ( $exists ) {
01183                         # Invalidate the cache for the description page
01184                         $descTitle->invalidateCache();
01185                         $descTitle->purgeSquid();
01186                 } else {
01187                         # New file; create the description page.
01188                         # There's already a log entry, so don't make a second RC entry
01189                         # Squid and file cache for the description page are purged by doEdit.
01190                         $status = $wikiPage->doEdit( $pageText, $comment, EDIT_NEW | EDIT_SUPPRESS_RC, false, $user );
01191 
01192                         if ( isset( $status->value['revision'] ) ) { // XXX; doEdit() uses a transaction
01193                                 $dbw->begin();
01194                                 $dbw->update( 'logging',
01195                                         array( 'log_page' => $status->value['revision']->getPage() ),
01196                                         array( 'log_id' => $logId ),
01197                                         __METHOD__
01198                                 );
01199                                 $dbw->commit(); // commit before anything bad can happen
01200                         }
01201                 }
01202                 wfProfileOut( __METHOD__ . '-edit' );
01203 
01204                 # Save to cache and purge the squid
01205                 # We shall not saveToCache before the commit since otherwise
01206                 # in case of a rollback there is an usable file from memcached
01207                 # which in fact doesn't really exist (bug 24978)
01208                 $this->saveToCache();
01209 
01210                 if ( $reupload ) {
01211                         # Delete old thumbnails
01212                         wfProfileIn( __METHOD__ . '-purge' );
01213                         $this->purgeThumbnails();
01214                         wfProfileOut( __METHOD__ . '-purge' );
01215 
01216                         # Remove the old file from the squid cache
01217                         SquidUpdate::purge( array( $this->getURL() ) );
01218                 }
01219 
01220                 # Hooks, hooks, the magic of hooks...
01221                 wfProfileIn( __METHOD__ . '-hooks' );
01222                 wfRunHooks( 'FileUpload', array( $this, $reupload, $descTitle->exists() ) );
01223                 wfProfileOut( __METHOD__ . '-hooks' );
01224 
01225                 # Invalidate cache for all pages using this file
01226                 $update = new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' );
01227                 $update->doUpdate();
01228 
01229                 # Invalidate cache for all pages that redirects on this page
01230                 $redirs = $this->getTitle()->getRedirectsHere();
01231 
01232                 foreach ( $redirs as $redir ) {
01233                         $update = new HTMLCacheUpdate( $redir, 'imagelinks' );
01234                         $update->doUpdate();
01235                 }
01236 
01237                 wfProfileOut( __METHOD__ );
01238                 return true;
01239         }
01240 
01255         function publish( $srcPath, $flags = 0 ) {
01256                 return $this->publishTo( $srcPath, $this->getRel(), $flags );
01257         }
01258 
01273         function publishTo( $srcPath, $dstRel, $flags = 0 ) {
01274                 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
01275                         return $this->readOnlyFatalStatus();
01276                 }
01277 
01278                 $this->lock(); // begin
01279 
01280                 $archiveName = wfTimestamp( TS_MW ) . '!'. $this->getName();
01281                 $archiveRel = 'archive/' . $this->getHashPath() . $archiveName;
01282                 $flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0;
01283                 $status = $this->repo->publish( $srcPath, $dstRel, $archiveRel, $flags );
01284 
01285                 if ( $status->value == 'new' ) {
01286                         $status->value = '';
01287                 } else {
01288                         $status->value = $archiveName;
01289                 }
01290 
01291                 $this->unlock(); // done
01292 
01293                 return $status;
01294         }
01295 
01313         function move( $target ) {
01314                 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
01315                         return $this->readOnlyFatalStatus();
01316                 }
01317 
01318                 wfDebugLog( 'imagemove', "Got request to move {$this->name} to " . $target->getText() );
01319                 $batch = new LocalFileMoveBatch( $this, $target );
01320 
01321                 $this->lock(); // begin
01322                 $batch->addCurrent();
01323                 $archiveNames = $batch->addOlds();
01324                 $status = $batch->execute();
01325                 $this->unlock(); // done
01326 
01327                 wfDebugLog( 'imagemove', "Finished moving {$this->name}" );
01328 
01329                 $this->purgeEverything();
01330                 foreach ( $archiveNames as $archiveName ) {
01331                         $this->purgeOldThumbnails( $archiveName );
01332                 }
01333                 if ( $status->isOK() ) {
01334                         // Now switch the object
01335                         $this->title = $target;
01336                         // Force regeneration of the name and hashpath
01337                         unset( $this->name );
01338                         unset( $this->hashPath );
01339                         // Purge the new image
01340                         $this->purgeEverything();
01341                 }
01342 
01343                 return $status;
01344         }
01345 
01358         function delete( $reason, $suppress = false ) {
01359                 global $wgUseSquid;
01360                 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
01361                         return $this->readOnlyFatalStatus();
01362                 }
01363 
01364                 $batch = new LocalFileDeleteBatch( $this, $reason, $suppress );
01365 
01366                 $this->lock(); // begin
01367                 $batch->addCurrent();
01368                 # Get old version relative paths
01369                 $archiveNames = $batch->addOlds();
01370                 $status = $batch->execute();
01371                 $this->unlock(); // done
01372 
01373                 if ( $status->isOK() ) {
01374                         DeferredUpdates::addUpdate( SiteStatsUpdate::factory( array( 'images' => -1 ) ) );
01375                 }
01376 
01377                 $this->purgeEverything();
01378                 foreach ( $archiveNames as $archiveName ) {
01379                         $this->purgeOldThumbnails( $archiveName );
01380                 }
01381 
01382                 if ( $wgUseSquid ) {
01383                         // Purge the squid
01384                         $purgeUrls = array();
01385                         foreach ($archiveNames as $archiveName ) {
01386                                 $purgeUrls[] = $this->getArchiveUrl( $archiveName );
01387                         }
01388                         SquidUpdate::purge( $purgeUrls );
01389                 }
01390 
01391                 return $status;
01392         }
01393 
01408         function deleteOld( $archiveName, $reason, $suppress = false ) {
01409                 global $wgUseSquid;
01410                 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
01411                         return $this->readOnlyFatalStatus();
01412                 }
01413 
01414                 $batch = new LocalFileDeleteBatch( $this, $reason, $suppress );
01415 
01416                 $this->lock(); // begin
01417                 $batch->addOld( $archiveName );
01418                 $status = $batch->execute();
01419                 $this->unlock(); // done
01420 
01421                 $this->purgeOldThumbnails( $archiveName );
01422                 if ( $status->isOK() ) {
01423                         $this->purgeDescription();
01424                         $this->purgeHistory();
01425                 }
01426 
01427                 if ( $wgUseSquid ) {
01428                         // Purge the squid
01429                         SquidUpdate::purge( array( $this->getArchiveUrl( $archiveName ) ) );
01430                 }
01431 
01432                 return $status;
01433         }
01434 
01446         function restore( $versions = array(), $unsuppress = false ) {
01447                 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
01448                         return $this->readOnlyFatalStatus();
01449                 }
01450 
01451                 $batch = new LocalFileRestoreBatch( $this, $unsuppress );
01452 
01453                 $this->lock(); // begin
01454                 if ( !$versions ) {
01455                         $batch->addAll();
01456                 } else {
01457                         $batch->addIds( $versions );
01458                 }
01459                 $status = $batch->execute();
01460                 if ( $status->isGood() ) {
01461                         $cleanupStatus = $batch->cleanup();
01462                         $cleanupStatus->successCount = 0;
01463                         $cleanupStatus->failCount = 0;
01464                         $status->merge( $cleanupStatus );
01465                 }
01466                 $this->unlock(); // done
01467 
01468                 return $status;
01469         }
01470 
01480         function getDescriptionUrl() {
01481                 return $this->title->getLocalUrl();
01482         }
01483 
01490         function getDescriptionText() {
01491                 global $wgParser;
01492                 $revision = Revision::newFromTitle( $this->title, false, Revision::READ_NORMAL );
01493                 if ( !$revision ) return false;
01494                 $text = $revision->getText();
01495                 if ( !$text ) return false;
01496                 $pout = $wgParser->parse( $text, $this->title, new ParserOptions() );
01497                 return $pout->getText();
01498         }
01499 
01503         function getDescription( $audience = self::FOR_PUBLIC, User $user = null ) {
01504                 $this->load();
01505                 if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
01506                         return '';
01507                 } elseif ( $audience == self::FOR_THIS_USER
01508                         && !$this->userCan( self::DELETED_COMMENT, $user ) )
01509                 {
01510                         return '';
01511                 } else {
01512                         return $this->description;
01513                 }
01514         }
01515 
01519         function getTimestamp() {
01520                 $this->load();
01521                 return $this->timestamp;
01522         }
01523 
01527         function getSha1() {
01528                 $this->load();
01529                 // Initialise now if necessary
01530                 if ( $this->sha1 == '' && $this->fileExists ) {
01531                         $this->lock(); // begin
01532 
01533                         $this->sha1 = $this->repo->getFileSha1( $this->getPath() );
01534                         if ( !wfReadOnly() && strval( $this->sha1 ) != '' ) {
01535                                 $dbw = $this->repo->getMasterDB();
01536                                 $dbw->update( 'image',
01537                                         array( 'img_sha1' => $this->sha1 ),
01538                                         array( 'img_name' => $this->getName() ),
01539                                         __METHOD__ );
01540                                 $this->saveToCache();
01541                         }
01542 
01543                         $this->unlock(); // done
01544                 }
01545 
01546                 return $this->sha1;
01547         }
01548 
01552         function isCacheable() {
01553                 $this->load();
01554                 return strlen( $this->metadata ) <= self::CACHE_FIELD_MAX_LEN; // avoid OOMs
01555         }
01556 
01562         function lock() {
01563                 $dbw = $this->repo->getMasterDB();
01564 
01565                 if ( !$this->locked ) {
01566                         $dbw->begin( __METHOD__ );
01567                         $this->locked++;
01568                 }
01569 
01570                 return $dbw->selectField( 'image', '1',
01571                         array( 'img_name' => $this->getName() ), __METHOD__, array( 'FOR UPDATE' ) );
01572         }
01573 
01578         function unlock() {
01579                 if ( $this->locked ) {
01580                         --$this->locked;
01581                         if ( !$this->locked ) {
01582                                 $dbw = $this->repo->getMasterDB();
01583                                 $dbw->commit( __METHOD__ );
01584                         }
01585                 }
01586         }
01587 
01591         function unlockAndRollback() {
01592                 $this->locked = false;
01593                 $dbw = $this->repo->getMasterDB();
01594                 $dbw->rollback( __METHOD__ );
01595         }
01596 
01600         protected function readOnlyFatalStatus() {
01601                 return $this->getRepo()->newFatal( 'filereadonlyerror', $this->getName(),
01602                         $this->getRepo()->getName(), $this->getRepo()->getReadOnlyReason() );
01603         }
01604 } // LocalFile class
01605 
01606 # ------------------------------------------------------------------------------
01607 
01612 class LocalFileDeleteBatch {
01613 
01617         var $file;
01618 
01619         var $reason, $srcRels = array(), $archiveUrls = array(), $deletionBatch, $suppress;
01620         var $status;
01621 
01627         function __construct( File $file, $reason = '', $suppress = false ) {
01628                 $this->file = $file;
01629                 $this->reason = $reason;
01630                 $this->suppress = $suppress;
01631                 $this->status = $file->repo->newGood();
01632         }
01633 
01634         function addCurrent() {
01635                 $this->srcRels['.'] = $this->file->getRel();
01636         }
01637 
01641         function addOld( $oldName ) {
01642                 $this->srcRels[$oldName] = $this->file->getArchiveRel( $oldName );
01643                 $this->archiveUrls[] = $this->file->getArchiveUrl( $oldName );
01644         }
01645 
01650         function addOlds() {
01651                 $archiveNames = array();
01652 
01653                 $dbw = $this->file->repo->getMasterDB();
01654                 $result = $dbw->select( 'oldimage',
01655                         array( 'oi_archive_name' ),
01656                         array( 'oi_name' => $this->file->getName() ),
01657                         __METHOD__
01658                 );
01659 
01660                 foreach ( $result as $row ) {
01661                         $this->addOld( $row->oi_archive_name );
01662                         $archiveNames[] = $row->oi_archive_name;
01663                 }
01664 
01665                 return $archiveNames;
01666         }
01667 
01671         function getOldRels() {
01672                 if ( !isset( $this->srcRels['.'] ) ) {
01673                         $oldRels =& $this->srcRels;
01674                         $deleteCurrent = false;
01675                 } else {
01676                         $oldRels = $this->srcRels;
01677                         unset( $oldRels['.'] );
01678                         $deleteCurrent = true;
01679                 }
01680 
01681                 return array( $oldRels, $deleteCurrent );
01682         }
01683 
01687         protected function getHashes() {
01688                 $hashes = array();
01689                 list( $oldRels, $deleteCurrent ) = $this->getOldRels();
01690 
01691                 if ( $deleteCurrent ) {
01692                         $hashes['.'] = $this->file->getSha1();
01693                 }
01694 
01695                 if ( count( $oldRels ) ) {
01696                         $dbw = $this->file->repo->getMasterDB();
01697                         $res = $dbw->select(
01698                                 'oldimage',
01699                                 array( 'oi_archive_name', 'oi_sha1' ),
01700                                 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')',
01701                                 __METHOD__
01702                         );
01703 
01704                         foreach ( $res as $row ) {
01705                                 if ( rtrim( $row->oi_sha1, "\0" ) === '' ) {
01706                                         // Get the hash from the file
01707                                         $oldUrl = $this->file->getArchiveVirtualUrl( $row->oi_archive_name );
01708                                         $props = $this->file->repo->getFileProps( $oldUrl );
01709 
01710                                         if ( $props['fileExists'] ) {
01711                                                 // Upgrade the oldimage row
01712                                                 $dbw->update( 'oldimage',
01713                                                         array( 'oi_sha1' => $props['sha1'] ),
01714                                                         array( 'oi_name' => $this->file->getName(), 'oi_archive_name' => $row->oi_archive_name ),
01715                                                         __METHOD__ );
01716                                                 $hashes[$row->oi_archive_name] = $props['sha1'];
01717                                         } else {
01718                                                 $hashes[$row->oi_archive_name] = false;
01719                                         }
01720                                 } else {
01721                                         $hashes[$row->oi_archive_name] = $row->oi_sha1;
01722                                 }
01723                         }
01724                 }
01725 
01726                 $missing = array_diff_key( $this->srcRels, $hashes );
01727 
01728                 foreach ( $missing as $name => $rel ) {
01729                         $this->status->error( 'filedelete-old-unregistered', $name );
01730                 }
01731 
01732                 foreach ( $hashes as $name => $hash ) {
01733                         if ( !$hash ) {
01734                                 $this->status->error( 'filedelete-missing', $this->srcRels[$name] );
01735                                 unset( $hashes[$name] );
01736                         }
01737                 }
01738 
01739                 return $hashes;
01740         }
01741 
01742         function doDBInserts() {
01743                 global $wgUser;
01744 
01745                 $dbw = $this->file->repo->getMasterDB();
01746                 $encTimestamp = $dbw->addQuotes( $dbw->timestamp() );
01747                 $encUserId = $dbw->addQuotes( $wgUser->getId() );
01748                 $encReason = $dbw->addQuotes( $this->reason );
01749                 $encGroup = $dbw->addQuotes( 'deleted' );
01750                 $ext = $this->file->getExtension();
01751                 $dotExt = $ext === '' ? '' : ".$ext";
01752                 $encExt = $dbw->addQuotes( $dotExt );
01753                 list( $oldRels, $deleteCurrent ) = $this->getOldRels();
01754 
01755                 // Bitfields to further suppress the content
01756                 if ( $this->suppress ) {
01757                         $bitfield = 0;
01758                         // This should be 15...
01759                         $bitfield |= Revision::DELETED_TEXT;
01760                         $bitfield |= Revision::DELETED_COMMENT;
01761                         $bitfield |= Revision::DELETED_USER;
01762                         $bitfield |= Revision::DELETED_RESTRICTED;
01763                 } else {
01764                         $bitfield = 'oi_deleted';
01765                 }
01766 
01767                 if ( $deleteCurrent ) {
01768                         $concat = $dbw->buildConcat( array( "img_sha1", $encExt ) );
01769                         $where = array( 'img_name' => $this->file->getName() );
01770                         $dbw->insertSelect( 'filearchive', 'image',
01771                                 array(
01772                                         'fa_storage_group' => $encGroup,
01773                                         'fa_storage_key'   => "CASE WHEN img_sha1='' THEN '' ELSE $concat END",
01774                                         'fa_deleted_user'      => $encUserId,
01775                                         'fa_deleted_timestamp' => $encTimestamp,
01776                                         'fa_deleted_reason'    => $encReason,
01777                                         'fa_deleted'               => $this->suppress ? $bitfield : 0,
01778 
01779                                         'fa_name'         => 'img_name',
01780                                         'fa_archive_name' => 'NULL',
01781                                         'fa_size'         => 'img_size',
01782                                         'fa_width'        => 'img_width',
01783                                         'fa_height'       => 'img_height',
01784                                         'fa_metadata'     => 'img_metadata',
01785                                         'fa_bits'         => 'img_bits',
01786                                         'fa_media_type'   => 'img_media_type',
01787                                         'fa_major_mime'   => 'img_major_mime',
01788                                         'fa_minor_mime'   => 'img_minor_mime',
01789                                         'fa_description'  => 'img_description',
01790                                         'fa_user'         => 'img_user',
01791                                         'fa_user_text'    => 'img_user_text',
01792                                         'fa_timestamp'    => 'img_timestamp'
01793                                 ), $where, __METHOD__ );
01794                 }
01795 
01796                 if ( count( $oldRels ) ) {
01797                         $concat = $dbw->buildConcat( array( "oi_sha1", $encExt ) );
01798                         $where = array(
01799                                 'oi_name' => $this->file->getName(),
01800                                 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')' );
01801                         $dbw->insertSelect( 'filearchive', 'oldimage',
01802                                 array(
01803                                         'fa_storage_group' => $encGroup,
01804                                         'fa_storage_key'   => "CASE WHEN oi_sha1='' THEN '' ELSE $concat END",
01805                                         'fa_deleted_user'      => $encUserId,
01806                                         'fa_deleted_timestamp' => $encTimestamp,
01807                                         'fa_deleted_reason'    => $encReason,
01808                                         'fa_deleted'           => $this->suppress ? $bitfield : 'oi_deleted',
01809 
01810                                         'fa_name'         => 'oi_name',
01811                                         'fa_archive_name' => 'oi_archive_name',
01812                                         'fa_size'         => 'oi_size',
01813                                         'fa_width'        => 'oi_width',
01814                                         'fa_height'       => 'oi_height',
01815                                         'fa_metadata'     => 'oi_metadata',
01816                                         'fa_bits'         => 'oi_bits',
01817                                         'fa_media_type'   => 'oi_media_type',
01818                                         'fa_major_mime'   => 'oi_major_mime',
01819                                         'fa_minor_mime'   => 'oi_minor_mime',
01820                                         'fa_description'  => 'oi_description',
01821                                         'fa_user'         => 'oi_user',
01822                                         'fa_user_text'    => 'oi_user_text',
01823                                         'fa_timestamp'    => 'oi_timestamp',
01824                                 ), $where, __METHOD__ );
01825                 }
01826         }
01827 
01828         function doDBDeletes() {
01829                 $dbw = $this->file->repo->getMasterDB();
01830                 list( $oldRels, $deleteCurrent ) = $this->getOldRels();
01831 
01832                 if ( count( $oldRels ) ) {
01833                         $dbw->delete( 'oldimage',
01834                                 array(
01835                                         'oi_name' => $this->file->getName(),
01836                                         'oi_archive_name' => array_keys( $oldRels )
01837                                 ), __METHOD__ );
01838                 }
01839 
01840                 if ( $deleteCurrent ) {
01841                         $dbw->delete( 'image', array( 'img_name' => $this->file->getName() ), __METHOD__ );
01842                 }
01843         }
01844 
01849         function execute() {
01850                 wfProfileIn( __METHOD__ );
01851 
01852                 $this->file->lock();
01853                 // Leave private files alone
01854                 $privateFiles = array();
01855                 list( $oldRels, $deleteCurrent ) = $this->getOldRels();
01856                 $dbw = $this->file->repo->getMasterDB();
01857 
01858                 if ( !empty( $oldRels ) ) {
01859                         $res = $dbw->select( 'oldimage',
01860                                 array( 'oi_archive_name' ),
01861                                 array( 'oi_name' => $this->file->getName(),
01862                                         'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')',
01863                                         $dbw->bitAnd( 'oi_deleted', File::DELETED_FILE ) => File::DELETED_FILE ),
01864                                 __METHOD__ );
01865 
01866                         foreach ( $res as $row ) {
01867                                 $privateFiles[$row->oi_archive_name] = 1;
01868                         }
01869                 }
01870                 // Prepare deletion batch
01871                 $hashes = $this->getHashes();
01872                 $this->deletionBatch = array();
01873                 $ext = $this->file->getExtension();
01874                 $dotExt = $ext === '' ? '' : ".$ext";
01875 
01876                 foreach ( $this->srcRels as $name => $srcRel ) {
01877                         // Skip files that have no hash (missing source).
01878                         // Keep private files where they are.
01879                         if ( isset( $hashes[$name] ) && !array_key_exists( $name, $privateFiles ) ) {
01880                                 $hash = $hashes[$name];
01881                                 $key = $hash . $dotExt;
01882                                 $dstRel = $this->file->repo->getDeletedHashPath( $key ) . $key;
01883                                 $this->deletionBatch[$name] = array( $srcRel, $dstRel );
01884                         }
01885                 }
01886 
01887                 // Lock the filearchive rows so that the files don't get deleted by a cleanup operation
01888                 // We acquire this lock by running the inserts now, before the file operations.
01889                 //
01890                 // This potentially has poor lock contention characteristics -- an alternative
01891                 // scheme would be to insert stub filearchive entries with no fa_name and commit
01892                 // them in a separate transaction, then run the file ops, then update the fa_name fields.
01893                 $this->doDBInserts();
01894 
01895                 // Removes non-existent file from the batch, so we don't get errors.
01896                 $this->deletionBatch = $this->removeNonexistentFiles( $this->deletionBatch );
01897 
01898                 // Execute the file deletion batch
01899                 $status = $this->file->repo->deleteBatch( $this->deletionBatch );
01900 
01901                 if ( !$status->isGood() ) {
01902                         $this->status->merge( $status );
01903                 }
01904 
01905                 if ( !$this->status->isOK() ) {
01906                         // Critical file deletion error
01907                         // Roll back inserts, release lock and abort
01908                         // TODO: delete the defunct filearchive rows if we are using a non-transactional DB
01909                         $this->file->unlockAndRollback();
01910                         wfProfileOut( __METHOD__ );
01911                         return $this->status;
01912                 }
01913 
01914                 // Delete image/oldimage rows
01915                 $this->doDBDeletes();
01916 
01917                 // Commit and return
01918                 $this->file->unlock();
01919                 wfProfileOut( __METHOD__ );
01920 
01921                 return $this->status;
01922         }
01923 
01929         function removeNonexistentFiles( $batch ) {
01930                 $files = $newBatch = array();
01931 
01932                 foreach ( $batch as $batchItem ) {
01933                         list( $src, $dest ) = $batchItem;
01934                         $files[$src] = $this->file->repo->getVirtualUrl( 'public' ) . '/' . rawurlencode( $src );
01935                 }
01936 
01937                 $result = $this->file->repo->fileExistsBatch( $files );
01938 
01939                 foreach ( $batch as $batchItem ) {
01940                         if ( $result[$batchItem[0]] ) {
01941                                 $newBatch[] = $batchItem;
01942                         }
01943                 }
01944 
01945                 return $newBatch;
01946         }
01947 }
01948 
01949 # ------------------------------------------------------------------------------
01950 
01955 class LocalFileRestoreBatch {
01959         var $file;
01960 
01961         var $cleanupBatch, $ids, $all, $unsuppress = false;
01962 
01967         function __construct( File $file, $unsuppress = false ) {
01968                 $this->file = $file;
01969                 $this->cleanupBatch = $this->ids = array();
01970                 $this->ids = array();
01971                 $this->unsuppress = $unsuppress;
01972         }
01973 
01977         function addId( $fa_id ) {
01978                 $this->ids[] = $fa_id;
01979         }
01980 
01984         function addIds( $ids ) {
01985                 $this->ids = array_merge( $this->ids, $ids );
01986         }
01987 
01991         function addAll() {
01992                 $this->all = true;
01993         }
01994 
02003         function execute() {
02004                 global $wgLang;
02005 
02006                 if ( !$this->all && !$this->ids ) {
02007                         // Do nothing
02008                         return $this->file->repo->newGood();
02009                 }
02010 
02011                 $exists = $this->file->lock();
02012                 $dbw = $this->file->repo->getMasterDB();
02013                 $status = $this->file->repo->newGood();
02014 
02015                 // Fetch all or selected archived revisions for the file,
02016                 // sorted from the most recent to the oldest.
02017                 $conditions = array( 'fa_name' => $this->file->getName() );
02018 
02019                 if ( !$this->all ) {
02020                         $conditions[] = 'fa_id IN (' . $dbw->makeList( $this->ids ) . ')';
02021                 }
02022 
02023                 $result = $dbw->select( 'filearchive', '*',
02024                         $conditions,
02025                         __METHOD__,
02026                         array( 'ORDER BY' => 'fa_timestamp DESC' )
02027                 );
02028 
02029                 $idsPresent = array();
02030                 $storeBatch = array();
02031                 $insertBatch = array();
02032                 $insertCurrent = false;
02033                 $deleteIds = array();
02034                 $first = true;
02035                 $archiveNames = array();
02036 
02037                 foreach ( $result as $row ) {
02038                         $idsPresent[] = $row->fa_id;
02039 
02040                         if ( $row->fa_name != $this->file->getName() ) {
02041                                 $status->error( 'undelete-filename-mismatch', $wgLang->timeanddate( $row->fa_timestamp ) );
02042                                 $status->failCount++;
02043                                 continue;
02044                         }
02045 
02046                         if ( $row->fa_storage_key == '' ) {
02047                                 // Revision was missing pre-deletion
02048                                 $status->error( 'undelete-bad-store-key', $wgLang->timeanddate( $row->fa_timestamp ) );
02049                                 $status->failCount++;
02050                                 continue;
02051                         }
02052 
02053                         $deletedRel = $this->file->repo->getDeletedHashPath( $row->fa_storage_key ) . $row->fa_storage_key;
02054                         $deletedUrl = $this->file->repo->getVirtualUrl() . '/deleted/' . $deletedRel;
02055 
02056                         $sha1 = substr( $row->fa_storage_key, 0, strcspn( $row->fa_storage_key, '.' ) );
02057 
02058                         # Fix leading zero
02059                         if ( strlen( $sha1 ) == 32 && $sha1[0] == '0' ) {
02060                                 $sha1 = substr( $sha1, 1 );
02061                         }
02062 
02063                         if ( is_null( $row->fa_major_mime ) || $row->fa_major_mime == 'unknown'
02064                                 || is_null( $row->fa_minor_mime ) || $row->fa_minor_mime == 'unknown'
02065                                 || is_null( $row->fa_media_type ) || $row->fa_media_type == 'UNKNOWN'
02066                                 || is_null( $row->fa_metadata ) ) {
02067                                 // Refresh our metadata
02068                                 // Required for a new current revision; nice for older ones too. :)
02069                                 $props = RepoGroup::singleton()->getFileProps( $deletedUrl );
02070                         } else {
02071                                 $props = array(
02072                                         'minor_mime' => $row->fa_minor_mime,
02073                                         'major_mime' => $row->fa_major_mime,
02074                                         'media_type' => $row->fa_media_type,
02075                                         'metadata'   => $row->fa_metadata
02076                                 );
02077                         }
02078 
02079                         if ( $first && !$exists ) {
02080                                 // This revision will be published as the new current version
02081                                 $destRel = $this->file->getRel();
02082                                 $insertCurrent = array(
02083                                         'img_name'        => $row->fa_name,
02084                                         'img_size'        => $row->fa_size,
02085                                         'img_width'       => $row->fa_width,
02086                                         'img_height'      => $row->fa_height,
02087                                         'img_metadata'    => $props['metadata'],
02088                                         'img_bits'        => $row->fa_bits,
02089                                         'img_media_type'  => $props['media_type'],
02090                                         'img_major_mime'  => $props['major_mime'],
02091                                         'img_minor_mime'  => $props['minor_mime'],
02092                                         'img_description' => $row->fa_description,
02093                                         'img_user'        => $row->fa_user,
02094                                         'img_user_text'   => $row->fa_user_text,
02095                                         'img_timestamp'   => $row->fa_timestamp,
02096                                         'img_sha1'        => $sha1
02097                                 );
02098 
02099                                 // The live (current) version cannot be hidden!
02100                                 if ( !$this->unsuppress && $row->fa_deleted ) {
02101                                         $storeBatch[] = array( $deletedUrl, 'public', $destRel );
02102                                         $this->cleanupBatch[] = $row->fa_storage_key;
02103                                 }
02104                         } else {
02105                                 $archiveName = $row->fa_archive_name;
02106 
02107                                 if ( $archiveName == '' ) {
02108                                         // This was originally a current version; we
02109                                         // have to devise a new archive name for it.
02110                                         // Format is <timestamp of archiving>!<name>
02111                                         $timestamp = wfTimestamp( TS_UNIX, $row->fa_deleted_timestamp );
02112 
02113                                         do {
02114                                                 $archiveName = wfTimestamp( TS_MW, $timestamp ) . '!' . $row->fa_name;
02115                                                 $timestamp++;
02116                                         } while ( isset( $archiveNames[$archiveName] ) );
02117                                 }
02118 
02119                                 $archiveNames[$archiveName] = true;
02120                                 $destRel = $this->file->getArchiveRel( $archiveName );
02121                                 $insertBatch[] = array(
02122                                         'oi_name'         => $row->fa_name,
02123                                         'oi_archive_name' => $archiveName,
02124                                         'oi_size'         => $row->fa_size,
02125                                         'oi_width'        => $row->fa_width,
02126                                         'oi_height'       => $row->fa_height,
02127                                         'oi_bits'         => $row->fa_bits,
02128                                         'oi_description'  => $row->fa_description,
02129                                         'oi_user'         => $row->fa_user,
02130                                         'oi_user_text'    => $row->fa_user_text,
02131                                         'oi_timestamp'    => $row->fa_timestamp,
02132                                         'oi_metadata'     => $props['metadata'],
02133                                         'oi_media_type'   => $props['media_type'],
02134                                         'oi_major_mime'   => $props['major_mime'],
02135                                         'oi_minor_mime'   => $props['minor_mime'],
02136                                         'oi_deleted'      => $this->unsuppress ? 0 : $row->fa_deleted,
02137                                         'oi_sha1'         => $sha1 );
02138                         }
02139 
02140                         $deleteIds[] = $row->fa_id;
02141 
02142                         if ( !$this->unsuppress && $row->fa_deleted & File::DELETED_FILE ) {
02143                                 // private files can stay where they are
02144                                 $status->successCount++;
02145                         } else {
02146                                 $storeBatch[] = array( $deletedUrl, 'public', $destRel );
02147                                 $this->cleanupBatch[] = $row->fa_storage_key;
02148                         }
02149 
02150                         $first = false;
02151                 }
02152 
02153                 unset( $result );
02154 
02155                 // Add a warning to the status object for missing IDs
02156                 $missingIds = array_diff( $this->ids, $idsPresent );
02157 
02158                 foreach ( $missingIds as $id ) {
02159                         $status->error( 'undelete-missing-filearchive', $id );
02160                 }
02161 
02162                 // Remove missing files from batch, so we don't get errors when undeleting them
02163                 $storeBatch = $this->removeNonexistentFiles( $storeBatch );
02164 
02165                 // Run the store batch
02166                 // Use the OVERWRITE_SAME flag to smooth over a common error
02167                 $storeStatus = $this->file->repo->storeBatch( $storeBatch, FileRepo::OVERWRITE_SAME );
02168                 $status->merge( $storeStatus );
02169 
02170                 if ( !$status->isGood() ) {
02171                         // Even if some files could be copied, fail entirely as that is the
02172                         // easiest thing to do without data loss
02173                         $this->cleanupFailedBatch( $storeStatus, $storeBatch );
02174                         $status->ok = false;
02175                         $this->file->unlock();
02176 
02177                         return $status;
02178                 }
02179 
02180                 // Run the DB updates
02181                 // Because we have locked the image row, key conflicts should be rare.
02182                 // If they do occur, we can roll back the transaction at this time with
02183                 // no data loss, but leaving unregistered files scattered throughout the
02184                 // public zone.
02185                 // This is not ideal, which is why it's important to lock the image row.
02186                 if ( $insertCurrent ) {
02187                         $dbw->insert( 'image', $insertCurrent, __METHOD__ );
02188                 }
02189 
02190                 if ( $insertBatch ) {
02191                         $dbw->insert( 'oldimage', $insertBatch, __METHOD__ );
02192                 }
02193 
02194                 if ( $deleteIds ) {
02195                         $dbw->delete( 'filearchive',
02196                                 array( 'fa_id IN (' . $dbw->makeList( $deleteIds ) . ')' ),
02197                                 __METHOD__ );
02198                 }
02199 
02200                 // If store batch is empty (all files are missing), deletion is to be considered successful
02201                 if ( $status->successCount > 0 || !$storeBatch ) {
02202                         if ( !$exists ) {
02203                                 wfDebug( __METHOD__ . " restored {$status->successCount} items, creating a new current\n" );
02204 
02205                                 DeferredUpdates::addUpdate( SiteStatsUpdate::factory( array( 'images' => 1 ) ) );
02206 
02207                                 $this->file->purgeEverything();
02208                         } else {
02209                                 wfDebug( __METHOD__ . " restored {$status->successCount} as archived versions\n" );
02210                                 $this->file->purgeDescription();
02211                                 $this->file->purgeHistory();
02212                         }
02213                 }
02214 
02215                 $this->file->unlock();
02216 
02217                 return $status;
02218         }
02219 
02225         function removeNonexistentFiles( $triplets ) {
02226                 $files = $filteredTriplets = array();
02227                 foreach ( $triplets as $file ) {
02228                         $files[$file[0]] = $file[0];
02229                 }
02230 
02231                 $result = $this->file->repo->fileExistsBatch( $files );
02232 
02233                 foreach ( $triplets as $file ) {
02234                         if ( $result[$file[0]] ) {
02235                                 $filteredTriplets[] = $file;
02236                         }
02237                 }
02238 
02239                 return $filteredTriplets;
02240         }
02241 
02247         function removeNonexistentFromCleanup( $batch ) {
02248                 $files = $newBatch = array();
02249                 $repo = $this->file->repo;
02250 
02251                 foreach ( $batch as $file ) {
02252                         $files[$file] = $repo->getVirtualUrl( 'deleted' ) . '/' .
02253                                 rawurlencode( $repo->getDeletedHashPath( $file ) . $file );
02254                 }
02255 
02256                 $result = $repo->fileExistsBatch( $files );
02257 
02258                 foreach ( $batch as $file ) {
02259                         if ( $result[$file] ) {
02260                                 $newBatch[] = $file;
02261                         }
02262                 }
02263 
02264                 return $newBatch;
02265         }
02266 
02272         function cleanup() {
02273                 if ( !$this->cleanupBatch ) {
02274                         return $this->file->repo->newGood();
02275                 }
02276 
02277                 $this->cleanupBatch = $this->removeNonexistentFromCleanup( $this->cleanupBatch );
02278 
02279                 $status = $this->file->repo->cleanupDeletedBatch( $this->cleanupBatch );
02280 
02281                 return $status;
02282         }
02283 
02291         function cleanupFailedBatch( $storeStatus, $storeBatch ) {
02292                 $cleanupBatch = array();
02293 
02294                 foreach ( $storeStatus->success as $i => $success ) {
02295                         // Check if this item of the batch was successfully copied
02296                         if ( $success ) {
02297                                 // Item was successfully copied and needs to be removed again
02298                                 // Extract ($dstZone, $dstRel) from the batch
02299                                 $cleanupBatch[] = array( $storeBatch[$i][1], $storeBatch[$i][2] );
02300                         }
02301                 }
02302                 $this->file->repo->cleanupBatch( $cleanupBatch );
02303         }
02304 }
02305 
02306 # ------------------------------------------------------------------------------
02307 
02312 class LocalFileMoveBatch {
02313 
02317         var $file;
02318 
02322         var $target;
02323 
02324         var $cur, $olds, $oldCount, $archive;
02325 
02329         var $db;
02330 
02335         function __construct( File $file, Title $target ) {
02336                 $this->file = $file;
02337                 $this->target = $target;
02338                 $this->oldHash = $this->file->repo->getHashPath( $this->file->getName() );
02339                 $this->newHash = $this->file->repo->getHashPath( $this->target->getDBkey() );
02340                 $this->oldName = $this->file->getName();
02341                 $this->newName = $this->file->repo->getNameFromTitle( $this->target );
02342                 $this->oldRel = $this->oldHash . $this->oldName;
02343                 $this->newRel = $this->newHash . $this->newName;
02344                 $this->db = $file->getRepo()->getMasterDb();
02345         }
02346 
02350         function addCurrent() {
02351                 $this->cur = array( $this->oldRel, $this->newRel );
02352         }
02353 
02358         function addOlds() {
02359                 $archiveBase = 'archive';
02360                 $this->olds = array();
02361                 $this->oldCount = 0;
02362                 $archiveNames = array();
02363 
02364                 $result = $this->db->select( 'oldimage',
02365                         array( 'oi_archive_name', 'oi_deleted' ),
02366                         array( 'oi_name' => $this->oldName ),
02367                         __METHOD__
02368                 );
02369 
02370                 foreach ( $result as $row ) {
02371                         $archiveNames[] = $row->oi_archive_name;
02372                         $oldName = $row->oi_archive_name;
02373                         $bits = explode( '!', $oldName, 2 );
02374 
02375                         if ( count( $bits ) != 2 ) {
02376                                 wfDebug( "Old file name missing !: '$oldName' \n" );
02377                                 continue;
02378                         }
02379 
02380                         list( $timestamp, $filename ) = $bits;
02381 
02382                         if ( $this->oldName != $filename ) {
02383                                 wfDebug( "Old file name doesn't match: '$oldName' \n" );
02384                                 continue;
02385                         }
02386 
02387                         $this->oldCount++;
02388 
02389                         // Do we want to add those to oldCount?
02390                         if ( $row->oi_deleted & File::DELETED_FILE ) {
02391                                 continue;
02392                         }
02393 
02394                         $this->olds[] = array(
02395                                 "{$archiveBase}/{$this->oldHash}{$oldName}",
02396                                 "{$archiveBase}/{$this->newHash}{$timestamp}!{$this->newName}"
02397                         );
02398                 }
02399 
02400                 return $archiveNames;
02401         }
02402 
02407         function execute() {
02408                 $repo = $this->file->repo;
02409                 $status = $repo->newGood();
02410 
02411                 $triplets = $this->getMoveTriplets();
02412                 $triplets = $this->removeNonexistentFiles( $triplets );
02413 
02414                 $this->file->lock(); // begin
02415                 // Rename the file versions metadata in the DB.
02416                 // This implicitly locks the destination file, which avoids race conditions.
02417                 // If we moved the files from A -> C before DB updates, another process could
02418                 // move files from B -> C at this point, causing storeBatch() to fail and thus
02419                 // cleanupTarget() to trigger. It would delete the C files and cause data loss.
02420                 $statusDb = $this->doDBUpdates();
02421                 if ( !$statusDb->isGood() ) {
02422                         $this->file->unlockAndRollback();
02423                         $statusDb->ok = false;
02424                         return $statusDb;
02425                 }
02426                 wfDebugLog( 'imagemove', "Renamed {$this->file->getName()} in database: {$statusDb->successCount} successes, {$statusDb->failCount} failures" );
02427 
02428                 // Copy the files into their new location.
02429                 // If a prior process fataled copying or cleaning up files we tolerate any
02430                 // of the existing files if they are identical to the ones being stored.
02431                 $statusMove = $repo->storeBatch( $triplets, FileRepo::OVERWRITE_SAME );
02432                 wfDebugLog( 'imagemove', "Moved files for {$this->file->getName()}: {$statusMove->successCount} successes, {$statusMove->failCount} failures" );
02433                 if ( !$statusMove->isGood() ) {
02434                         // Delete any files copied over (while the destination is still locked)
02435                         $this->cleanupTarget( $triplets );
02436                         $this->file->unlockAndRollback(); // unlocks the destination
02437                         wfDebugLog( 'imagemove', "Error in moving files: " . $statusMove->getWikiText() );
02438                         $statusMove->ok = false;
02439                         return $statusMove;
02440                 }
02441                 $this->file->unlock(); // done
02442 
02443                 // Everything went ok, remove the source files
02444                 $this->cleanupSource( $triplets );
02445 
02446                 $status->merge( $statusDb );
02447                 $status->merge( $statusMove );
02448 
02449                 return $status;
02450         }
02451 
02458         function doDBUpdates() {
02459                 $repo = $this->file->repo;
02460                 $status = $repo->newGood();
02461                 $dbw = $this->db;
02462 
02463                 // Update current image
02464                 $dbw->update(
02465                         'image',
02466                         array( 'img_name' => $this->newName ),
02467                         array( 'img_name' => $this->oldName ),
02468                         __METHOD__
02469                 );
02470 
02471                 if ( $dbw->affectedRows() ) {
02472                         $status->successCount++;
02473                 } else {
02474                         $status->failCount++;
02475                         $status->fatal( 'imageinvalidfilename' );
02476                         return $status;
02477                 }
02478 
02479                 // Update old images
02480                 $dbw->update(
02481                         'oldimage',
02482                         array(
02483                                 'oi_name' => $this->newName,
02484                                 'oi_archive_name = ' . $dbw->strreplace( 'oi_archive_name',
02485                                         $dbw->addQuotes( $this->oldName ), $dbw->addQuotes( $this->newName ) ),
02486                         ),
02487                         array( 'oi_name' => $this->oldName ),
02488                         __METHOD__
02489                 );
02490 
02491                 $affected = $dbw->affectedRows();
02492                 $total = $this->oldCount;
02493                 $status->successCount += $affected;
02494                 // Bug 34934: $total is based on files that actually exist.
02495                 // There may be more DB rows than such files, in which case $affected
02496                 // can be greater than $total. We use max() to avoid negatives here.
02497                 $status->failCount += max( 0, $total - $affected );
02498                 if ( $status->failCount ) {
02499                         $status->error( 'imageinvalidfilename' );
02500                 }
02501 
02502                 return $status;
02503         }
02504 
02509         function getMoveTriplets() {
02510                 $moves = array_merge( array( $this->cur ), $this->olds );
02511                 $triplets = array();    // The format is: (srcUrl, destZone, destUrl)
02512 
02513                 foreach ( $moves as $move ) {
02514                         // $move: (oldRelativePath, newRelativePath)
02515                         $srcUrl = $this->file->repo->getVirtualUrl() . '/public/' . rawurlencode( $move[0] );
02516                         $triplets[] = array( $srcUrl, 'public', $move[1] );
02517                         wfDebugLog( 'imagemove', "Generated move triplet for {$this->file->getName()}: {$srcUrl} :: public :: {$move[1]}" );
02518                 }
02519 
02520                 return $triplets;
02521         }
02522 
02528         function removeNonexistentFiles( $triplets ) {
02529                 $files = array();
02530 
02531                 foreach ( $triplets as $file ) {
02532                         $files[$file[0]] = $file[0];
02533                 }
02534 
02535                 $result = $this->file->repo->fileExistsBatch( $files );
02536                 $filteredTriplets = array();
02537 
02538                 foreach ( $triplets as $file ) {
02539                         if ( $result[$file[0]] ) {
02540                                 $filteredTriplets[] = $file;
02541                         } else {
02542                                 wfDebugLog( 'imagemove', "File {$file[0]} does not exist" );
02543                         }
02544                 }
02545 
02546                 return $filteredTriplets;
02547         }
02548 
02553         function cleanupTarget( $triplets ) {
02554                 // Create dest pairs from the triplets
02555                 $pairs = array();
02556                 foreach ( $triplets as $triplet ) {
02557                         // $triplet: (old source virtual URL, dst zone, dest rel)
02558                         $pairs[] = array( $triplet[1], $triplet[2] );
02559                 }
02560 
02561                 $this->file->repo->cleanupBatch( $pairs );
02562         }
02563 
02568         function cleanupSource( $triplets ) {
02569                 // Create source file names from the triplets
02570                 $files = array();
02571                 foreach ( $triplets as $triplet ) {
02572                         $files[] = $triplet[0];
02573                 }
02574 
02575                 $this->file->repo->cleanupBatch( $files );
02576         }
02577 }