MediaWiki  REL1_19
LocalFile.php
Go to the documentation of this file.
00001 <?php
00012 define( 'MW_FILE_VERSION', 8 );
00013 
00031 class LocalFile extends File {
00035         var
00036                 $fileExists,       # does the file exist on disk? (loadFromXxx)
00037                 $historyLine,      # Number of line to return by nextHistoryLine() (constructor)
00038                 $historyRes,       # result of the query for the file's history (nextHistoryLine)
00039                 $width,            # \
00040                 $height,           #  |
00041                 $bits,             #   --- returned by getimagesize (loadFromXxx)
00042                 $attr,             # /
00043                 $media_type,       # MEDIATYPE_xxx (bitmap, drawing, audio...)
00044                 $mime,             # MIME type, determined by MimeMagic::guessMimeType
00045                 $major_mime,       # Major mime type
00046                 $minor_mime,       # Minor mime type
00047                 $size,             # Size in bytes (loadFromXxx)
00048                 $metadata,         # Handler-specific metadata
00049                 $timestamp,        # Upload timestamp
00050                 $sha1,             # SHA-1 base 36 content hash
00051                 $user, $user_text, # User, who uploaded the file
00052                 $description,      # Description of current revision of the file
00053                 $dataLoaded,       # Whether or not all this has been loaded from the database (loadFromXxx)
00054                 $upgraded,         # Whether the row was upgraded on load
00055                 $locked,           # True if the image row is locked
00056                 $missing,          # True if file is not present in file system. Not to be cached in memcached
00057                 $deleted;          # Bitfield akin to rev_deleted
00058 
00061         protected $repoClass = 'LocalRepo';
00062 
00075         static function newFromTitle( $title, $repo, $unused = null ) {
00076                 return new self( $title, $repo );
00077         }
00078 
00088         static function newFromRow( $row, $repo ) {
00089                 $title = Title::makeTitle( NS_FILE, $row->img_name );
00090                 $file = new self( $title, $repo );
00091                 $file->loadFromRow( $row );
00092 
00093                 return $file;
00094         }
00095 
00106         static function newFromKey( $sha1, $repo, $timestamp = false ) {
00107                 $dbr = $repo->getSlaveDB();
00108 
00109                 $conds = array( 'img_sha1' => $sha1 );
00110                 if ( $timestamp ) {
00111                         $conds['img_timestamp'] = $dbr->timestamp( $timestamp );
00112                 }
00113 
00114                 $row = $dbr->selectRow( 'image', self::selectFields(), $conds, __METHOD__ );
00115                 if ( $row ) {
00116                         return self::newFromRow( $row, $repo );
00117                 } else {
00118                         return false;
00119                 }
00120         }
00121 
00125         static function selectFields() {
00126                 return array(
00127                         'img_name',
00128                         'img_size',
00129                         'img_width',
00130                         'img_height',
00131                         'img_metadata',
00132                         'img_bits',
00133                         'img_media_type',
00134                         'img_major_mime',
00135                         'img_minor_mime',
00136                         'img_description',
00137                         'img_user',
00138                         'img_user_text',
00139                         'img_timestamp',
00140                         'img_sha1',
00141                 );
00142         }
00143 
00148         function __construct( $title, $repo ) {
00149                 parent::__construct( $title, $repo );
00150 
00151                 $this->metadata = '';
00152                 $this->historyLine = 0;
00153                 $this->historyRes = null;
00154                 $this->dataLoaded = false;
00155 
00156                 $this->assertRepoDefined();
00157                 $this->assertTitleDefined();
00158         }
00159 
00164         function getCacheKey() {
00165                 $hashedName = md5( $this->getName() );
00166 
00167                 return $this->repo->getSharedCacheKey( 'file', $hashedName );
00168         }
00169 
00173         function loadFromCache() {
00174                 global $wgMemc;
00175 
00176                 wfProfileIn( __METHOD__ );
00177                 $this->dataLoaded = false;
00178                 $key = $this->getCacheKey();
00179 
00180                 if ( !$key ) {
00181                         wfProfileOut( __METHOD__ );
00182                         return false;
00183                 }
00184 
00185                 $cachedValues = $wgMemc->get( $key );
00186 
00187                 // Check if the key existed and belongs to this version of MediaWiki
00188                 if ( isset( $cachedValues['version'] ) && ( $cachedValues['version'] == MW_FILE_VERSION ) ) {
00189                         wfDebug( "Pulling file metadata from cache key $key\n" );
00190                         $this->fileExists = $cachedValues['fileExists'];
00191                         if ( $this->fileExists ) {
00192                                 $this->setProps( $cachedValues );
00193                         }
00194                         $this->dataLoaded = true;
00195                 }
00196 
00197                 if ( $this->dataLoaded ) {
00198                         wfIncrStats( 'image_cache_hit' );
00199                 } else {
00200                         wfIncrStats( 'image_cache_miss' );
00201                 }
00202 
00203                 wfProfileOut( __METHOD__ );
00204                 return $this->dataLoaded;
00205         }
00206 
00210         function saveToCache() {
00211                 global $wgMemc;
00212 
00213                 $this->load();
00214                 $key = $this->getCacheKey();
00215 
00216                 if ( !$key ) {
00217                         return;
00218                 }
00219 
00220                 $fields = $this->getCacheFields( '' );
00221                 $cache = array( 'version' => MW_FILE_VERSION );
00222                 $cache['fileExists'] = $this->fileExists;
00223 
00224                 if ( $this->fileExists ) {
00225                         foreach ( $fields as $field ) {
00226                                 $cache[$field] = $this->$field;
00227                         }
00228                 }
00229 
00230                 $wgMemc->set( $key, $cache, 60 * 60 * 24 * 7 ); // A week
00231         }
00232 
00236         function loadFromFile() {
00237                 $props = $this->repo->getFileProps( $this->getVirtualUrl() );
00238                 $this->setProps( $props );
00239         }
00240 
00241         function getCacheFields( $prefix = 'img_' ) {
00242                 static $fields = array( 'size', 'width', 'height', 'bits', 'media_type',
00243                         'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1', 'user', 'user_text', 'description' );
00244                 static $results = array();
00245 
00246                 if ( $prefix == '' ) {
00247                         return $fields;
00248                 }
00249 
00250                 if ( !isset( $results[$prefix] ) ) {
00251                         $prefixedFields = array();
00252                         foreach ( $fields as $field ) {
00253                                 $prefixedFields[] = $prefix . $field;
00254                         }
00255                         $results[$prefix] = $prefixedFields;
00256                 }
00257 
00258                 return $results[$prefix];
00259         }
00260 
00264         function loadFromDB() {
00265                 # Polymorphic function name to distinguish foreign and local fetches
00266                 $fname = get_class( $this ) . '::' . __FUNCTION__;
00267                 wfProfileIn( $fname );
00268 
00269                 # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
00270                 $this->dataLoaded = true;
00271 
00272                 $dbr = $this->repo->getMasterDB();
00273 
00274                 $row = $dbr->selectRow( 'image', $this->getCacheFields( 'img_' ),
00275                         array( 'img_name' => $this->getName() ), $fname );
00276 
00277                 if ( $row ) {
00278                         $this->loadFromRow( $row );
00279                 } else {
00280                         $this->fileExists = false;
00281                 }
00282 
00283                 wfProfileOut( $fname );
00284         }
00285 
00290         function decodeRow( $row, $prefix = 'img_' ) {
00291                 $array = (array)$row;
00292                 $prefixLength = strlen( $prefix );
00293 
00294                 // Sanity check prefix once
00295                 if ( substr( key( $array ), 0, $prefixLength ) !== $prefix ) {
00296                         throw new MWException( __METHOD__ .  ': incorrect $prefix parameter' );
00297                 }
00298 
00299                 $decoded = array();
00300 
00301                 foreach ( $array as $name => $value ) {
00302                         $decoded[substr( $name, $prefixLength )] = $value;
00303                 }
00304 
00305                 $decoded['timestamp'] = wfTimestamp( TS_MW, $decoded['timestamp'] );
00306 
00307                 if ( empty( $decoded['major_mime'] ) ) {
00308                         $decoded['mime'] = 'unknown/unknown';
00309                 } else {
00310                         if ( !$decoded['minor_mime'] ) {
00311                                 $decoded['minor_mime'] = 'unknown';
00312                         }
00313                         $decoded['mime'] = $decoded['major_mime'] . '/' . $decoded['minor_mime'];
00314                 }
00315 
00316                 # Trim zero padding from char/binary field
00317                 $decoded['sha1'] = rtrim( $decoded['sha1'], "\0" );
00318 
00319                 return $decoded;
00320         }
00321 
00325         function loadFromRow( $row, $prefix = 'img_' ) {
00326                 $this->dataLoaded = true;
00327                 $array = $this->decodeRow( $row, $prefix );
00328 
00329                 foreach ( $array as $name => $value ) {
00330                         $this->$name = $value;
00331                 }
00332 
00333                 $this->fileExists = true;
00334                 $this->maybeUpgradeRow();
00335         }
00336 
00340         function load() {
00341                 if ( !$this->dataLoaded ) {
00342                         if ( !$this->loadFromCache() ) {
00343                                 $this->loadFromDB();
00344                                 $this->saveToCache();
00345                         }
00346                         $this->dataLoaded = true;
00347                 }
00348         }
00349 
00353         function maybeUpgradeRow() {
00354                 global $wgUpdateCompatibleMetadata;
00355                 if ( wfReadOnly() ) {
00356                         return;
00357                 }
00358 
00359                 if ( is_null( $this->media_type ) ||
00360                         $this->mime == 'image/svg'
00361                 ) {
00362                         $this->upgradeRow();
00363                         $this->upgraded = true;
00364                 } else {
00365                         $handler = $this->getHandler();
00366                         if ( $handler ) {
00367                                 $validity = $handler->isMetadataValid( $this, $this->metadata );
00368                                 if ( $validity === MediaHandler::METADATA_BAD
00369                                         || ( $validity === MediaHandler::METADATA_COMPATIBLE && $wgUpdateCompatibleMetadata )
00370                                 ) {
00371                                         $this->upgradeRow();
00372                                         $this->upgraded = true;
00373                                 }
00374                         }
00375                 }
00376         }
00377 
00378         function getUpgraded() {
00379                 return $this->upgraded;
00380         }
00381 
00385         function upgradeRow() {
00386                 wfProfileIn( __METHOD__ );
00387 
00388                 $this->lock(); // begin
00389 
00390                 $this->loadFromFile();
00391 
00392                 # Don't destroy file info of missing files
00393                 if ( !$this->fileExists ) {
00394                         wfDebug( __METHOD__ . ": file does not exist, aborting\n" );
00395                         wfProfileOut( __METHOD__ );
00396                         return;
00397                 }
00398 
00399                 $dbw = $this->repo->getMasterDB();
00400                 list( $major, $minor ) = self::splitMime( $this->mime );
00401 
00402                 if ( wfReadOnly() ) {
00403                         wfProfileOut( __METHOD__ );
00404                         return;
00405                 }
00406                 wfDebug( __METHOD__ . ': upgrading ' . $this->getName() . " to the current schema\n" );
00407 
00408                 $dbw->update( 'image',
00409                         array(
00410                                 'img_width'      => $this->width,
00411                                 'img_height'     => $this->height,
00412                                 'img_bits'       => $this->bits,
00413                                 'img_media_type' => $this->media_type,
00414                                 'img_major_mime' => $major,
00415                                 'img_minor_mime' => $minor,
00416                                 'img_metadata'   => $this->metadata,
00417                                 'img_sha1'       => $this->sha1,
00418                         ),
00419                         array( 'img_name' => $this->getName() ),
00420                         __METHOD__
00421                 );
00422 
00423                 $this->saveToCache();
00424 
00425                 $this->unlock(); // done
00426 
00427                 wfProfileOut( __METHOD__ );
00428         }
00429 
00437         function setProps( $info ) {
00438                 $this->dataLoaded = true;
00439                 $fields = $this->getCacheFields( '' );
00440                 $fields[] = 'fileExists';
00441 
00442                 foreach ( $fields as $field ) {
00443                         if ( isset( $info[$field] ) ) {
00444                                 $this->$field = $info[$field];
00445                         }
00446                 }
00447 
00448                 // Fix up mime fields
00449                 if ( isset( $info['major_mime'] ) ) {
00450                         $this->mime = "{$info['major_mime']}/{$info['minor_mime']}";
00451                 } elseif ( isset( $info['mime'] ) ) {
00452                         $this->mime = $info['mime'];
00453                         list( $this->major_mime, $this->minor_mime ) = self::splitMime( $this->mime );
00454                 }
00455         }
00456 
00465         function isMissing() {
00466                 if ( $this->missing === null ) {
00467                         list( $fileExists ) = $this->repo->fileExists( $this->getVirtualUrl(), FileRepo::FILES_ONLY );
00468                         $this->missing = !$fileExists;
00469                 }
00470                 return $this->missing;
00471         }
00472 
00478         public function getWidth( $page = 1 ) {
00479                 $this->load();
00480 
00481                 if ( $this->isMultipage() ) {
00482                         $dim = $this->getHandler()->getPageDimensions( $this, $page );
00483                         if ( $dim ) {
00484                                 return $dim['width'];
00485                         } else {
00486                                 return false;
00487                         }
00488                 } else {
00489                         return $this->width;
00490                 }
00491         }
00492 
00498         public function getHeight( $page = 1 ) {
00499                 $this->load();
00500 
00501                 if ( $this->isMultipage() ) {
00502                         $dim = $this->getHandler()->getPageDimensions( $this, $page );
00503                         if ( $dim ) {
00504                                 return $dim['height'];
00505                         } else {
00506                                 return false;
00507                         }
00508                 } else {
00509                         return $this->height;
00510                 }
00511         }
00512 
00518         function getUser( $type = 'text' ) {
00519                 $this->load();
00520 
00521                 if ( $type == 'text' ) {
00522                         return $this->user_text;
00523                 } elseif ( $type == 'id' ) {
00524                         return $this->user;
00525                 }
00526         }
00527 
00531         function getMetadata() {
00532                 $this->load();
00533                 return $this->metadata;
00534         }
00535 
00536         function getBitDepth() {
00537                 $this->load();
00538                 return $this->bits;
00539         }
00540 
00544         public function getSize() {
00545                 $this->load();
00546                 return $this->size;
00547         }
00548 
00552         function getMimeType() {
00553                 $this->load();
00554                 return $this->mime;
00555         }
00556 
00561         function getMediaType() {
00562                 $this->load();
00563                 return $this->media_type;
00564         }
00565 
00576         public function exists() {
00577                 $this->load();
00578                 return $this->fileExists;
00579         }
00580 
00590         function migrateThumbFile( $thumbName ) {
00591                 $thumbDir = $this->getThumbPath();
00592 
00593                 /* Old code for bug 2532
00594                 $thumbPath = "$thumbDir/$thumbName";
00595                 if ( is_dir( $thumbPath ) ) {
00596                         // Directory where file should be
00597                         // This happened occasionally due to broken migration code in 1.5
00598                         // Rename to broken-*
00599                         for ( $i = 0; $i < 100 ; $i++ ) {
00600                                 $broken = $this->repo->getZonePath( 'public' ) . "/broken-$i-$thumbName";
00601                                 if ( !file_exists( $broken ) ) {
00602                                         rename( $thumbPath, $broken );
00603                                         break;
00604                                 }
00605                         }
00606                         // Doesn't exist anymore
00607                         clearstatcache();
00608                 }
00609                 */
00610 
00611                 if ( $this->repo->fileExists( $thumbDir, FileRepo::FILES_ONLY ) ) {
00612                         // Delete file where directory should be
00613                         $this->repo->cleanupBatch( array( $thumbDir ) );
00614                 }
00615         }
00616 
00626         function getThumbnails( $archiveName = false ) {
00627                 $this->load();
00628 
00629                 if ( $archiveName ) {
00630                         $dir = $this->getArchiveThumbPath( $archiveName );
00631                 } else {
00632                         $dir = $this->getThumbPath();
00633                 }
00634 
00635                 $backend = $this->repo->getBackend();
00636                 $files = array( $dir );
00637                 $iterator = $backend->getFileList( array( 'dir' => $dir ) );
00638                 foreach ( $iterator as $file ) {
00639                         $files[] = $file;
00640                 }
00641 
00642                 return $files;
00643         }
00644 
00648         function purgeMetadataCache() {
00649                 $this->loadFromDB();
00650                 $this->saveToCache();
00651                 $this->purgeHistory();
00652         }
00653 
00657         function purgeHistory() {
00658                 global $wgMemc;
00659 
00660                 $hashedName = md5( $this->getName() );
00661                 $oldKey = $this->repo->getSharedCacheKey( 'oldfile', $hashedName );
00662 
00663                 // Must purge thumbnails for old versions too! bug 30192
00664                 foreach( $this->getHistory() as $oldFile ) {
00665                         $oldFile->purgeThumbnails();
00666                 }
00667 
00668                 if ( $oldKey ) {
00669                         $wgMemc->delete( $oldKey );
00670                 }
00671         }
00672 
00676         function purgeCache( $options = array() ) {
00677                 // Refresh metadata cache
00678                 $this->purgeMetadataCache();
00679 
00680                 // Delete thumbnails
00681                 $this->purgeThumbnails( $options );
00682 
00683                 // Purge squid cache for this file
00684                 SquidUpdate::purge( array( $this->getURL() ) );
00685         }
00686 
00691         function purgeOldThumbnails( $archiveName ) {
00692                 global $wgUseSquid;
00693                 // Get a list of old thumbnails and URLs
00694                 $files = $this->getThumbnails( $archiveName );
00695                 $dir = array_shift( $files );
00696                 $this->purgeThumbList( $dir, $files );
00697 
00698                 // Purge any custom thumbnail caches
00699                 wfRunHooks( 'LocalFilePurgeThumbnails', array( $this, $archiveName ) );
00700 
00701                 // Purge the squid
00702                 if ( $wgUseSquid ) {
00703                         $urls = array();
00704                         foreach( $files as $file ) {
00705                                 $urls[] = $this->getArchiveThumbUrl( $archiveName, $file );
00706                         }
00707                         SquidUpdate::purge( $urls );
00708                 }
00709         }
00710 
00714         function purgeThumbnails( $options = array() ) {
00715                 global $wgUseSquid;
00716 
00717                 // Delete thumbnails
00718                 $files = $this->getThumbnails();
00719 
00720                 // Give media handler a chance to filter the purge list
00721                 if ( !empty( $options['forThumbRefresh'] ) ) {
00722                         $handler = $this->getHandler();
00723                         if ( $handler ) {
00724                                 $handler->filterThumbnailPurgeList( $files, $options );
00725                         }
00726                 }
00727 
00728                 $dir = array_shift( $files );
00729                 $this->purgeThumbList( $dir, $files );
00730 
00731                 // Purge any custom thumbnail caches
00732                 wfRunHooks( 'LocalFilePurgeThumbnails', array( $this, false ) );
00733 
00734                 // Purge the squid
00735                 if ( $wgUseSquid ) {
00736                         $urls = array();
00737                         foreach( $files as $file ) {
00738                                 $urls[] = $this->getThumbUrl( $file );
00739                         }
00740                         SquidUpdate::purge( $urls );
00741                 }
00742         }
00743 
00749         protected function purgeThumbList( $dir, $files ) {
00750                 $fileListDebug = strtr(
00751                         var_export( $files, true ),
00752                         array("\n"=>'')
00753                 );
00754                 wfDebug( __METHOD__ . ": $fileListDebug\n" );
00755 
00756                 $purgeList = array();
00757                 foreach ( $files as $file ) {
00758                         # Check that the base file name is part of the thumb name
00759                         # This is a basic sanity check to avoid erasing unrelated directories
00760                         if ( strpos( $file, $this->getName() ) !== false ) {
00761                                 $purgeList[] = "{$dir}/{$file}";
00762                         }
00763                 }
00764 
00765                 # Delete the thumbnails
00766                 $this->repo->cleanupBatch( $purgeList, FileRepo::SKIP_LOCKING );
00767                 # Clear out the thumbnail directory if empty
00768                 $this->repo->getBackend()->clean( array( 'dir' => $dir ) );
00769         }
00770 
00774         function getHistory( $limit = null, $start = null, $end = null, $inc = true ) {
00775                 $dbr = $this->repo->getSlaveDB();
00776                 $tables = array( 'oldimage' );
00777                 $fields = OldLocalFile::selectFields();
00778                 $conds = $opts = $join_conds = array();
00779                 $eq = $inc ? '=' : '';
00780                 $conds[] = "oi_name = " . $dbr->addQuotes( $this->title->getDBkey() );
00781 
00782                 if ( $start ) {
00783                         $conds[] = "oi_timestamp <$eq " . $dbr->addQuotes( $dbr->timestamp( $start ) );
00784                 }
00785 
00786                 if ( $end ) {
00787                         $conds[] = "oi_timestamp >$eq " . $dbr->addQuotes( $dbr->timestamp( $end ) );
00788                 }
00789 
00790                 if ( $limit ) {
00791                         $opts['LIMIT'] = $limit;
00792                 }
00793 
00794                 // Search backwards for time > x queries
00795                 $order = ( !$start && $end !== null ) ? 'ASC' : 'DESC';
00796                 $opts['ORDER BY'] = "oi_timestamp $order";
00797                 $opts['USE INDEX'] = array( 'oldimage' => 'oi_name_timestamp' );
00798 
00799                 wfRunHooks( 'LocalFile::getHistory', array( &$this, &$tables, &$fields,
00800                         &$conds, &$opts, &$join_conds ) );
00801 
00802                 $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $opts, $join_conds );
00803                 $r = array();
00804 
00805                 foreach ( $res as $row ) {
00806                         if ( $this->repo->oldFileFromRowFactory ) {
00807                                 $r[] = call_user_func( $this->repo->oldFileFromRowFactory, $row, $this->repo );
00808                         } else {
00809                                 $r[] = OldLocalFile::newFromRow( $row, $this->repo );
00810                         }
00811                 }
00812 
00813                 if ( $order == 'ASC' ) {
00814                         $r = array_reverse( $r ); // make sure it ends up descending
00815                 }
00816 
00817                 return $r;
00818         }
00819 
00828         public function nextHistoryLine() {
00829                 # Polymorphic function name to distinguish foreign and local fetches
00830                 $fname = get_class( $this ) . '::' . __FUNCTION__;
00831 
00832                 $dbr = $this->repo->getSlaveDB();
00833 
00834                 if ( $this->historyLine == 0 ) {// called for the first time, return line from cur
00835                         $this->historyRes = $dbr->select( 'image',
00836                                 array(
00837                                         '*',
00838                                         "'' AS oi_archive_name",
00839                                         '0 as oi_deleted',
00840                                         'img_sha1'
00841                                 ),
00842                                 array( 'img_name' => $this->title->getDBkey() ),
00843                                 $fname
00844                         );
00845 
00846                         if ( 0 == $dbr->numRows( $this->historyRes ) ) {
00847                                 $this->historyRes = null;
00848                                 return false;
00849                         }
00850                 } elseif ( $this->historyLine == 1 ) {
00851                         $this->historyRes = $dbr->select( 'oldimage', '*',
00852                                 array( 'oi_name' => $this->title->getDBkey() ),
00853                                 $fname,
00854                                 array( 'ORDER BY' => 'oi_timestamp DESC' )
00855                         );
00856                 }
00857                 $this->historyLine ++;
00858 
00859                 return $dbr->fetchObject( $this->historyRes );
00860         }
00861 
00865         public function resetHistory() {
00866                 $this->historyLine = 0;
00867 
00868                 if ( !is_null( $this->historyRes ) ) {
00869                         $this->historyRes = null;
00870                 }
00871         }
00872 
00901         function upload( $srcPath, $comment, $pageText, $flags = 0, $props = false, $timestamp = false, $user = null ) {
00902                 global $wgContLang;
00903                 // truncate nicely or the DB will do it for us
00904                 // non-nicely (dangling multi-byte chars, non-truncated
00905                 // version in cache).
00906                 $comment = $wgContLang->truncate( $comment, 255 );
00907                 $this->lock(); // begin
00908                 $status = $this->publish( $srcPath, $flags );
00909 
00910                 if ( $status->successCount > 0 ) {
00911                         # Essentially we are displacing any existing current file and saving
00912                         # a new current file at the old location. If just the first succeeded,
00913                         # we still need to displace the current DB entry and put in a new one.
00914                         if ( !$this->recordUpload2( $status->value, $comment, $pageText, $props, $timestamp, $user ) ) {
00915                                 $status->fatal( 'filenotfound', $srcPath );
00916                         }
00917                 }
00918 
00919                 $this->unlock(); // done
00920 
00921                 return $status;
00922         }
00923 
00927         function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
00928                 $watch = false, $timestamp = false )
00929         {
00930                 $pageText = SpecialUpload::getInitialPageText( $desc, $license, $copyStatus, $source );
00931 
00932                 if ( !$this->recordUpload2( $oldver, $desc, $pageText ) ) {
00933                         return false;
00934                 }
00935 
00936                 if ( $watch ) {
00937                         global $wgUser;
00938                         $wgUser->addWatch( $this->getTitle() );
00939                 }
00940                 return true;
00941         }
00942 
00946         function recordUpload2(
00947                 $oldver, $comment, $pageText, $props = false, $timestamp = false, $user = null
00948         ) {
00949                 if ( is_null( $user ) ) {
00950                         global $wgUser;
00951                         $user = $wgUser;
00952                 }
00953 
00954                 $dbw = $this->repo->getMasterDB();
00955                 $dbw->begin();
00956 
00957                 if ( !$props ) {
00958                         $props = $this->repo->getFileProps( $this->getVirtualUrl() );
00959                 }
00960 
00961                 if ( $timestamp === false ) {
00962                         $timestamp = $dbw->timestamp();
00963                 }
00964 
00965                 $props['description'] = $comment;
00966                 $props['user'] = $user->getId();
00967                 $props['user_text'] = $user->getName();
00968                 $props['timestamp'] = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
00969                 $this->setProps( $props );
00970 
00971                 # Delete thumbnails
00972                 $this->purgeThumbnails();
00973 
00974                 # The file is already on its final location, remove it from the squid cache
00975                 SquidUpdate::purge( array( $this->getURL() ) );
00976 
00977                 # Fail now if the file isn't there
00978                 if ( !$this->fileExists ) {
00979                         wfDebug( __METHOD__ . ": File " . $this->getRel() . " went missing!\n" );
00980                         return false;
00981                 }
00982 
00983                 $reupload = false;
00984 
00985                 # Test to see if the row exists using INSERT IGNORE
00986                 # This avoids race conditions by locking the row until the commit, and also
00987                 # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
00988                 $dbw->insert( 'image',
00989                         array(
00990                                 'img_name'        => $this->getName(),
00991                                 'img_size'        => $this->size,
00992                                 'img_width'       => intval( $this->width ),
00993                                 'img_height'      => intval( $this->height ),
00994                                 'img_bits'        => $this->bits,
00995                                 'img_media_type'  => $this->media_type,
00996                                 'img_major_mime'  => $this->major_mime,
00997                                 'img_minor_mime'  => $this->minor_mime,
00998                                 'img_timestamp'   => $timestamp,
00999                                 'img_description' => $comment,
01000                                 'img_user'        => $user->getId(),
01001                                 'img_user_text'   => $user->getName(),
01002                                 'img_metadata'    => $this->metadata,
01003                                 'img_sha1'        => $this->sha1
01004                         ),
01005                         __METHOD__,
01006                         'IGNORE'
01007                 );
01008 
01009                 if ( $dbw->affectedRows() == 0 ) {
01010                         if ( $oldver == '' ) { // XXX
01011                                 # (bug 34993) publish() can displace the current file and yet fail to save 
01012                                 # a new one. The next publish attempt will treat the file as a brand new file 
01013                                 # and pass an empty $oldver. Allow this bogus value so we can displace the 
01014                                 # `image` row to `oldimage`, leaving room for the new current file `image` row.
01015                                 #throw new MWException( "Empty oi_archive_name. Database and storage out of sync?" );
01016                         }
01017                         $reupload = true;
01018                         # Collision, this is an update of a file
01019                         # Insert previous contents into oldimage
01020                         $dbw->insertSelect( 'oldimage', 'image',
01021                                 array(
01022                                         'oi_name'         => 'img_name',
01023                                         'oi_archive_name' => $dbw->addQuotes( $oldver ),
01024                                         'oi_size'         => 'img_size',
01025                                         'oi_width'        => 'img_width',
01026                                         'oi_height'       => 'img_height',
01027                                         'oi_bits'         => 'img_bits',
01028                                         'oi_timestamp'    => 'img_timestamp',
01029                                         'oi_description'  => 'img_description',
01030                                         'oi_user'         => 'img_user',
01031                                         'oi_user_text'    => 'img_user_text',
01032                                         'oi_metadata'     => 'img_metadata',
01033                                         'oi_media_type'   => 'img_media_type',
01034                                         'oi_major_mime'   => 'img_major_mime',
01035                                         'oi_minor_mime'   => 'img_minor_mime',
01036                                         'oi_sha1'         => 'img_sha1'
01037                                 ),
01038                                 array( 'img_name' => $this->getName() ),
01039                                 __METHOD__
01040                         );
01041 
01042                         # Update the current image row
01043                         $dbw->update( 'image',
01044                                 array( /* SET */
01045                                         'img_size'        => $this->size,
01046                                         'img_width'       => intval( $this->width ),
01047                                         'img_height'      => intval( $this->height ),
01048                                         'img_bits'        => $this->bits,
01049                                         'img_media_type'  => $this->media_type,
01050                                         'img_major_mime'  => $this->major_mime,
01051                                         'img_minor_mime'  => $this->minor_mime,
01052                                         'img_timestamp'   => $timestamp,
01053                                         'img_description' => $comment,
01054                                         'img_user'        => $user->getId(),
01055                                         'img_user_text'   => $user->getName(),
01056                                         'img_metadata'    => $this->metadata,
01057                                         'img_sha1'        => $this->sha1
01058                                 ),
01059                                 array( 'img_name' => $this->getName() ),
01060                                 __METHOD__
01061                         );
01062                 } else {
01063                         # This is a new file
01064                         # Update the image count
01065                         $dbw->begin( __METHOD__ );
01066                         $dbw->update(
01067                                 'site_stats',
01068                                 array( 'ss_images = ss_images+1' ),
01069                                 '*',
01070                                 __METHOD__
01071                         );
01072                         $dbw->commit( __METHOD__ );
01073                 }
01074 
01075                 $descTitle = $this->getTitle();
01076                 $wikiPage = new WikiFilePage( $descTitle );
01077                 $wikiPage->setFile( $this );
01078 
01079                 # Add the log entry
01080                 $log = new LogPage( 'upload' );
01081                 $action = $reupload ? 'overwrite' : 'upload';
01082                 $log->addEntry( $action, $descTitle, $comment, array(), $user );
01083 
01084                 if ( $descTitle->exists() ) {
01085                         # Create a null revision
01086                         $latest = $descTitle->getLatestRevID();
01087                         $nullRevision = Revision::newNullRevision(
01088                                 $dbw,
01089                                 $descTitle->getArticleId(),
01090                                 $log->getRcComment(),
01091                                 false
01092                         );
01093                         if (!is_null($nullRevision)) {
01094                                 $nullRevision->insertOn( $dbw );
01095 
01096                                 wfRunHooks( 'NewRevisionFromEditComplete', array( $wikiPage, $nullRevision, $latest, $user ) );
01097                                 $wikiPage->updateRevisionOn( $dbw, $nullRevision );
01098                         }
01099                         # Invalidate the cache for the description page
01100                         $descTitle->invalidateCache();
01101                         $descTitle->purgeSquid();
01102                 } else {
01103                         # New file; create the description page.
01104                         # There's already a log entry, so don't make a second RC entry
01105                         # Squid and file cache for the description page are purged by doEdit.
01106                         $wikiPage->doEdit( $pageText, $comment, EDIT_NEW | EDIT_SUPPRESS_RC, false, $user );
01107                 }
01108 
01109                 # Commit the transaction now, in case something goes wrong later
01110                 # The most important thing is that files don't get lost, especially archives
01111                 $dbw->commit();
01112 
01113                 # Save to cache and purge the squid
01114                 # We shall not saveToCache before the commit since otherwise
01115                 # in case of a rollback there is an usable file from memcached
01116                 # which in fact doesn't really exist (bug 24978)
01117                 $this->saveToCache();
01118 
01119                 # Hooks, hooks, the magic of hooks...
01120                 wfRunHooks( 'FileUpload', array( $this, $reupload, $descTitle->exists() ) );
01121 
01122                 # Invalidate cache for all pages using this file
01123                 $update = new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' );
01124                 $update->doUpdate();
01125 
01126                 # Invalidate cache for all pages that redirects on this page
01127                 $redirs = $this->getTitle()->getRedirectsHere();
01128 
01129                 foreach ( $redirs as $redir ) {
01130                         $update = new HTMLCacheUpdate( $redir, 'imagelinks' );
01131                         $update->doUpdate();
01132                 }
01133 
01134                 return true;
01135         }
01136 
01151         function publish( $srcPath, $flags = 0 ) {
01152                 return $this->publishTo( $srcPath, $this->getRel(), $flags );
01153         }
01154 
01169         function publishTo( $srcPath, $dstRel, $flags = 0 ) {
01170                 $this->lock(); // begin
01171 
01172                 $archiveName = wfTimestamp( TS_MW ) . '!'. $this->getName();
01173                 $archiveRel = 'archive/' . $this->getHashPath() . $archiveName;
01174                 $flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0;
01175                 $status = $this->repo->publish( $srcPath, $dstRel, $archiveRel, $flags );
01176 
01177                 if ( $status->value == 'new' ) {
01178                         $status->value = '';
01179                 } else {
01180                         $status->value = $archiveName;
01181                 }
01182 
01183                 $this->unlock(); // done
01184 
01185                 return $status;
01186         }
01187 
01205         function move( $target ) {
01206                 wfDebugLog( 'imagemove', "Got request to move {$this->name} to " . $target->getText() );
01207                 $this->lock(); // begin
01208 
01209                 $batch = new LocalFileMoveBatch( $this, $target );
01210                 $batch->addCurrent();
01211                 $batch->addOlds();
01212 
01213                 $status = $batch->execute();
01214                 wfDebugLog( 'imagemove', "Finished moving {$this->name}" );
01215 
01216                 $this->purgeEverything();
01217                 $this->unlock(); // done
01218 
01219                 if ( $status->isOk() ) {
01220                         // Now switch the object
01221                         $this->title = $target;
01222                         // Force regeneration of the name and hashpath
01223                         unset( $this->name );
01224                         unset( $this->hashPath );
01225                         // Purge the new image
01226                         $this->purgeEverything();
01227                 }
01228 
01229                 return $status;
01230         }
01231 
01244         function delete( $reason, $suppress = false ) {
01245                 global $wgUseSquid;
01246                 $this->lock(); // begin
01247 
01248                 $batch = new LocalFileDeleteBatch( $this, $reason, $suppress );
01249                 $batch->addCurrent();
01250 
01251                 # Get old version relative paths
01252                 $dbw = $this->repo->getMasterDB();
01253                 $result = $dbw->select( 'oldimage',
01254                         array( 'oi_archive_name' ),
01255                         array( 'oi_name' => $this->getName() ) );
01256                 foreach ( $result as $row ) {
01257                         $batch->addOld( $row->oi_archive_name );
01258                         $this->purgeOldThumbnails( $row->oi_archive_name );
01259                 }
01260                 $status = $batch->execute();
01261 
01262                 if ( $status->ok ) {
01263                         // Update site_stats
01264                         $site_stats = $dbw->tableName( 'site_stats' );
01265                         $dbw->query( "UPDATE $site_stats SET ss_images=ss_images-1", __METHOD__ );
01266                         $this->purgeEverything();
01267                 }
01268 
01269                 $this->unlock(); // done
01270 
01271                 if ( $wgUseSquid ) {
01272                         // Purge the squid
01273                         $purgeUrls = array();
01274                         foreach ($archiveNames as $archiveName ) {
01275                                 $purgeUrls[] = $this->getArchiveUrl( $archiveName );
01276                         }
01277                         SquidUpdate::purge( $purgeUrls );
01278                 }
01279 
01280                 return $status;
01281         }
01282 
01297         function deleteOld( $archiveName, $reason, $suppress = false ) {
01298                 global $wgUseSquid;
01299                 $this->lock(); // begin
01300 
01301                 $batch = new LocalFileDeleteBatch( $this, $reason, $suppress );
01302                 $batch->addOld( $archiveName );
01303                 $this->purgeOldThumbnails( $archiveName );
01304                 $status = $batch->execute();
01305 
01306                 $this->unlock(); // done
01307 
01308                 if ( $status->ok ) {
01309                         $this->purgeDescription();
01310                         $this->purgeHistory();
01311                 }
01312 
01313                 if ( $wgUseSquid ) {
01314                         // Purge the squid
01315                         SquidUpdate::purge( array( $this->getArchiveUrl( $archiveName ) ) );
01316                 }
01317 
01318                 return $status;
01319         }
01320 
01332         function restore( $versions = array(), $unsuppress = false ) {
01333                 $batch = new LocalFileRestoreBatch( $this, $unsuppress );
01334 
01335                 if ( !$versions ) {
01336                         $batch->addAll();
01337                 } else {
01338                         $batch->addIds( $versions );
01339                 }
01340 
01341                 $status = $batch->execute();
01342 
01343                 if ( !$status->isGood() ) {
01344                         return $status;
01345                 }
01346 
01347                 $cleanupStatus = $batch->cleanup();
01348                 $cleanupStatus->successCount = 0;
01349                 $cleanupStatus->failCount = 0;
01350                 $status->merge( $cleanupStatus );
01351 
01352                 return $status;
01353         }
01354 
01363         function getDescriptionUrl() {
01364                 return $this->title->getLocalUrl();
01365         }
01366 
01372         function getDescriptionText() {
01373                 global $wgParser;
01374                 $revision = Revision::newFromTitle( $this->title );
01375                 if ( !$revision ) return false;
01376                 $text = $revision->getText();
01377                 if ( !$text ) return false;
01378                 $pout = $wgParser->parse( $text, $this->title, new ParserOptions() );
01379                 return $pout->getText();
01380         }
01381 
01382         function getDescription() {
01383                 $this->load();
01384                 return $this->description;
01385         }
01386 
01387         function getTimestamp() {
01388                 $this->load();
01389                 return $this->timestamp;
01390         }
01391 
01392         function getSha1() {
01393                 $this->load();
01394                 // Initialise now if necessary
01395                 if ( $this->sha1 == '' && $this->fileExists ) {
01396                         $this->lock(); // begin
01397 
01398                         $this->sha1 = $this->repo->getFileSha1( $this->getPath() );
01399                         if ( !wfReadOnly() && strval( $this->sha1 ) != '' ) {
01400                                 $dbw = $this->repo->getMasterDB();
01401                                 $dbw->update( 'image',
01402                                         array( 'img_sha1' => $this->sha1 ),
01403                                         array( 'img_name' => $this->getName() ),
01404                                         __METHOD__ );
01405                                 $this->saveToCache();
01406                         }
01407 
01408                         $this->unlock(); // done
01409                 }
01410 
01411                 return $this->sha1;
01412         }
01413 
01419         function lock() {
01420                 $dbw = $this->repo->getMasterDB();
01421 
01422                 if ( !$this->locked ) {
01423                         $dbw->begin();
01424                         $this->locked++;
01425                 }
01426 
01427                 return $dbw->selectField( 'image', '1', array( 'img_name' => $this->getName() ), __METHOD__ );
01428         }
01429 
01434         function unlock() {
01435                 if ( $this->locked ) {
01436                         --$this->locked;
01437                         if ( !$this->locked ) {
01438                                 $dbw = $this->repo->getMasterDB();
01439                                 $dbw->commit();
01440                         }
01441                 }
01442         }
01443 
01447         function unlockAndRollback() {
01448                 $this->locked = false;
01449                 $dbw = $this->repo->getMasterDB();
01450                 $dbw->rollback();
01451         }
01452 } // LocalFile class
01453 
01454 # ------------------------------------------------------------------------------
01455 
01460 class LocalFileDeleteBatch {
01461 
01465         var $file;
01466 
01467         var $reason, $srcRels = array(), $archiveUrls = array(), $deletionBatch, $suppress;
01468         var $status;
01469 
01470         function __construct( File $file, $reason = '', $suppress = false ) {
01471                 $this->file = $file;
01472                 $this->reason = $reason;
01473                 $this->suppress = $suppress;
01474                 $this->status = $file->repo->newGood();
01475         }
01476 
01477         function addCurrent() {
01478                 $this->srcRels['.'] = $this->file->getRel();
01479         }
01480 
01481         function addOld( $oldName ) {
01482                 $this->srcRels[$oldName] = $this->file->getArchiveRel( $oldName );
01483                 $this->archiveUrls[] = $this->file->getArchiveUrl( $oldName );
01484         }
01485 
01486         function getOldRels() {
01487                 if ( !isset( $this->srcRels['.'] ) ) {
01488                         $oldRels =& $this->srcRels;
01489                         $deleteCurrent = false;
01490                 } else {
01491                         $oldRels = $this->srcRels;
01492                         unset( $oldRels['.'] );
01493                         $deleteCurrent = true;
01494                 }
01495 
01496                 return array( $oldRels, $deleteCurrent );
01497         }
01498 
01499         protected function getHashes() {
01500                 $hashes = array();
01501                 list( $oldRels, $deleteCurrent ) = $this->getOldRels();
01502 
01503                 if ( $deleteCurrent ) {
01504                         $hashes['.'] = $this->file->getSha1();
01505                 }
01506 
01507                 if ( count( $oldRels ) ) {
01508                         $dbw = $this->file->repo->getMasterDB();
01509                         $res = $dbw->select(
01510                                 'oldimage',
01511                                 array( 'oi_archive_name', 'oi_sha1' ),
01512                                 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')',
01513                                 __METHOD__
01514                         );
01515 
01516                         foreach ( $res as $row ) {
01517                                 if ( rtrim( $row->oi_sha1, "\0" ) === '' ) {
01518                                         // Get the hash from the file
01519                                         $oldUrl = $this->file->getArchiveVirtualUrl( $row->oi_archive_name );
01520                                         $props = $this->file->repo->getFileProps( $oldUrl );
01521 
01522                                         if ( $props['fileExists'] ) {
01523                                                 // Upgrade the oldimage row
01524                                                 $dbw->update( 'oldimage',
01525                                                         array( 'oi_sha1' => $props['sha1'] ),
01526                                                         array( 'oi_name' => $this->file->getName(), 'oi_archive_name' => $row->oi_archive_name ),
01527                                                         __METHOD__ );
01528                                                 $hashes[$row->oi_archive_name] = $props['sha1'];
01529                                         } else {
01530                                                 $hashes[$row->oi_archive_name] = false;
01531                                         }
01532                                 } else {
01533                                         $hashes[$row->oi_archive_name] = $row->oi_sha1;
01534                                 }
01535                         }
01536                 }
01537 
01538                 $missing = array_diff_key( $this->srcRels, $hashes );
01539 
01540                 foreach ( $missing as $name => $rel ) {
01541                         $this->status->error( 'filedelete-old-unregistered', $name );
01542                 }
01543 
01544                 foreach ( $hashes as $name => $hash ) {
01545                         if ( !$hash ) {
01546                                 $this->status->error( 'filedelete-missing', $this->srcRels[$name] );
01547                                 unset( $hashes[$name] );
01548                         }
01549                 }
01550 
01551                 return $hashes;
01552         }
01553 
01554         function doDBInserts() {
01555                 global $wgUser;
01556 
01557                 $dbw = $this->file->repo->getMasterDB();
01558                 $encTimestamp = $dbw->addQuotes( $dbw->timestamp() );
01559                 $encUserId = $dbw->addQuotes( $wgUser->getId() );
01560                 $encReason = $dbw->addQuotes( $this->reason );
01561                 $encGroup = $dbw->addQuotes( 'deleted' );
01562                 $ext = $this->file->getExtension();
01563                 $dotExt = $ext === '' ? '' : ".$ext";
01564                 $encExt = $dbw->addQuotes( $dotExt );
01565                 list( $oldRels, $deleteCurrent ) = $this->getOldRels();
01566 
01567                 // Bitfields to further suppress the content
01568                 if ( $this->suppress ) {
01569                         $bitfield = 0;
01570                         // This should be 15...
01571                         $bitfield |= Revision::DELETED_TEXT;
01572                         $bitfield |= Revision::DELETED_COMMENT;
01573                         $bitfield |= Revision::DELETED_USER;
01574                         $bitfield |= Revision::DELETED_RESTRICTED;
01575                 } else {
01576                         $bitfield = 'oi_deleted';
01577                 }
01578 
01579                 if ( $deleteCurrent ) {
01580                         $concat = $dbw->buildConcat( array( "img_sha1", $encExt ) );
01581                         $where = array( 'img_name' => $this->file->getName() );
01582                         $dbw->insertSelect( 'filearchive', 'image',
01583                                 array(
01584                                         'fa_storage_group' => $encGroup,
01585                                         'fa_storage_key'   => "CASE WHEN img_sha1='' THEN '' ELSE $concat END",
01586                                         'fa_deleted_user'      => $encUserId,
01587                                         'fa_deleted_timestamp' => $encTimestamp,
01588                                         'fa_deleted_reason'    => $encReason,
01589                                         'fa_deleted'               => $this->suppress ? $bitfield : 0,
01590 
01591                                         'fa_name'         => 'img_name',
01592                                         'fa_archive_name' => 'NULL',
01593                                         'fa_size'         => 'img_size',
01594                                         'fa_width'        => 'img_width',
01595                                         'fa_height'       => 'img_height',
01596                                         'fa_metadata'     => 'img_metadata',
01597                                         'fa_bits'         => 'img_bits',
01598                                         'fa_media_type'   => 'img_media_type',
01599                                         'fa_major_mime'   => 'img_major_mime',
01600                                         'fa_minor_mime'   => 'img_minor_mime',
01601                                         'fa_description'  => 'img_description',
01602                                         'fa_user'         => 'img_user',
01603                                         'fa_user_text'    => 'img_user_text',
01604                                         'fa_timestamp'    => 'img_timestamp'
01605                                 ), $where, __METHOD__ );
01606                 }
01607 
01608                 if ( count( $oldRels ) ) {
01609                         $concat = $dbw->buildConcat( array( "oi_sha1", $encExt ) );
01610                         $where = array(
01611                                 'oi_name' => $this->file->getName(),
01612                                 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')' );
01613                         $dbw->insertSelect( 'filearchive', 'oldimage',
01614                                 array(
01615                                         'fa_storage_group' => $encGroup,
01616                                         'fa_storage_key'   => "CASE WHEN oi_sha1='' THEN '' ELSE $concat END",
01617                                         'fa_deleted_user'      => $encUserId,
01618                                         'fa_deleted_timestamp' => $encTimestamp,
01619                                         'fa_deleted_reason'    => $encReason,
01620                                         'fa_deleted'               => $this->suppress ? $bitfield : 'oi_deleted',
01621 
01622                                         'fa_name'         => 'oi_name',
01623                                         'fa_archive_name' => 'oi_archive_name',
01624                                         'fa_size'         => 'oi_size',
01625                                         'fa_width'        => 'oi_width',
01626                                         'fa_height'       => 'oi_height',
01627                                         'fa_metadata'     => 'oi_metadata',
01628                                         'fa_bits'         => 'oi_bits',
01629                                         'fa_media_type'   => 'oi_media_type',
01630                                         'fa_major_mime'   => 'oi_major_mime',
01631                                         'fa_minor_mime'   => 'oi_minor_mime',
01632                                         'fa_description'  => 'oi_description',
01633                                         'fa_user'         => 'oi_user',
01634                                         'fa_user_text'    => 'oi_user_text',
01635                                         'fa_timestamp'    => 'oi_timestamp',
01636                                         'fa_deleted'      => $bitfield
01637                                 ), $where, __METHOD__ );
01638                 }
01639         }
01640 
01641         function doDBDeletes() {
01642                 $dbw = $this->file->repo->getMasterDB();
01643                 list( $oldRels, $deleteCurrent ) = $this->getOldRels();
01644 
01645                 if ( count( $oldRels ) ) {
01646                         $dbw->delete( 'oldimage',
01647                                 array(
01648                                         'oi_name' => $this->file->getName(),
01649                                         'oi_archive_name' => array_keys( $oldRels )
01650                                 ), __METHOD__ );
01651                 }
01652 
01653                 if ( $deleteCurrent ) {
01654                         $dbw->delete( 'image', array( 'img_name' => $this->file->getName() ), __METHOD__ );
01655                 }
01656         }
01657 
01661         function execute() {
01662                 global $wgUseSquid;
01663                 wfProfileIn( __METHOD__ );
01664 
01665                 $this->file->lock();
01666                 // Leave private files alone
01667                 $privateFiles = array();
01668                 list( $oldRels, $deleteCurrent ) = $this->getOldRels();
01669                 $dbw = $this->file->repo->getMasterDB();
01670 
01671                 if ( !empty( $oldRels ) ) {
01672                         $res = $dbw->select( 'oldimage',
01673                                 array( 'oi_archive_name' ),
01674                                 array( 'oi_name' => $this->file->getName(),
01675                                         'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')',
01676                                         $dbw->bitAnd( 'oi_deleted', File::DELETED_FILE ) => File::DELETED_FILE ),
01677                                 __METHOD__ );
01678 
01679                         foreach ( $res as $row ) {
01680                                 $privateFiles[$row->oi_archive_name] = 1;
01681                         }
01682                 }
01683                 // Prepare deletion batch
01684                 $hashes = $this->getHashes();
01685                 $this->deletionBatch = array();
01686                 $ext = $this->file->getExtension();
01687                 $dotExt = $ext === '' ? '' : ".$ext";
01688 
01689                 foreach ( $this->srcRels as $name => $srcRel ) {
01690                         // Skip files that have no hash (missing source).
01691                         // Keep private files where they are.
01692                         if ( isset( $hashes[$name] ) && !array_key_exists( $name, $privateFiles ) ) {
01693                                 $hash = $hashes[$name];
01694                                 $key = $hash . $dotExt;
01695                                 $dstRel = $this->file->repo->getDeletedHashPath( $key ) . $key;
01696                                 $this->deletionBatch[$name] = array( $srcRel, $dstRel );
01697                         }
01698                 }
01699 
01700                 // Lock the filearchive rows so that the files don't get deleted by a cleanup operation
01701                 // We acquire this lock by running the inserts now, before the file operations.
01702                 //
01703                 // This potentially has poor lock contention characteristics -- an alternative
01704                 // scheme would be to insert stub filearchive entries with no fa_name and commit
01705                 // them in a separate transaction, then run the file ops, then update the fa_name fields.
01706                 $this->doDBInserts();
01707 
01708                 // Removes non-existent file from the batch, so we don't get errors.
01709                 $this->deletionBatch = $this->removeNonexistentFiles( $this->deletionBatch );
01710 
01711                 // Execute the file deletion batch
01712                 $status = $this->file->repo->deleteBatch( $this->deletionBatch );
01713 
01714                 if ( !$status->isGood() ) {
01715                         $this->status->merge( $status );
01716                 }
01717 
01718                 if ( !$this->status->ok ) {
01719                         // Critical file deletion error
01720                         // Roll back inserts, release lock and abort
01721                         // TODO: delete the defunct filearchive rows if we are using a non-transactional DB
01722                         $this->file->unlockAndRollback();
01723                         wfProfileOut( __METHOD__ );
01724                         return $this->status;
01725                 }
01726 
01727                 // Purge squid
01728                 if ( $wgUseSquid ) {
01729                         $urls = array();
01730 
01731                         foreach ( $this->srcRels as $srcRel ) {
01732                                 $urlRel = str_replace( '%2F', '/', rawurlencode( $srcRel ) );
01733                                 $urls[] = $this->file->repo->getZoneUrl( 'public' ) . '/' . $urlRel;
01734                         }
01735                         SquidUpdate::purge( $urls );
01736                 }
01737 
01738                 // Delete image/oldimage rows
01739                 $this->doDBDeletes();
01740 
01741                 // Commit and return
01742                 $this->file->unlock();
01743                 wfProfileOut( __METHOD__ );
01744 
01745                 return $this->status;
01746         }
01747 
01751         function removeNonexistentFiles( $batch ) {
01752                 $files = $newBatch = array();
01753 
01754                 foreach ( $batch as $batchItem ) {
01755                         list( $src, $dest ) = $batchItem;
01756                         $files[$src] = $this->file->repo->getVirtualUrl( 'public' ) . '/' . rawurlencode( $src );
01757                 }
01758 
01759                 $result = $this->file->repo->fileExistsBatch( $files, FileRepo::FILES_ONLY );
01760 
01761                 foreach ( $batch as $batchItem ) {
01762                         if ( $result[$batchItem[0]] ) {
01763                                 $newBatch[] = $batchItem;
01764                         }
01765                 }
01766 
01767                 return $newBatch;
01768         }
01769 }
01770 
01771 # ------------------------------------------------------------------------------
01772 
01777 class LocalFileRestoreBatch {
01781         var $file;
01782 
01783         var $cleanupBatch, $ids, $all, $unsuppress = false;
01784 
01785         function __construct( File $file, $unsuppress = false ) {
01786                 $this->file = $file;
01787                 $this->cleanupBatch = $this->ids = array();
01788                 $this->ids = array();
01789                 $this->unsuppress = $unsuppress;
01790         }
01791 
01795         function addId( $fa_id ) {
01796                 $this->ids[] = $fa_id;
01797         }
01798 
01802         function addIds( $ids ) {
01803                 $this->ids = array_merge( $this->ids, $ids );
01804         }
01805 
01809         function addAll() {
01810                 $this->all = true;
01811         }
01812 
01820         function execute() {
01821                 global $wgLang;
01822 
01823                 if ( !$this->all && !$this->ids ) {
01824                         // Do nothing
01825                         return $this->file->repo->newGood();
01826                 }
01827 
01828                 $exists = $this->file->lock();
01829                 $dbw = $this->file->repo->getMasterDB();
01830                 $status = $this->file->repo->newGood();
01831 
01832                 // Fetch all or selected archived revisions for the file,
01833                 // sorted from the most recent to the oldest.
01834                 $conditions = array( 'fa_name' => $this->file->getName() );
01835 
01836                 if ( !$this->all ) {
01837                         $conditions[] = 'fa_id IN (' . $dbw->makeList( $this->ids ) . ')';
01838                 }
01839 
01840                 $result = $dbw->select( 'filearchive', '*',
01841                         $conditions,
01842                         __METHOD__,
01843                         array( 'ORDER BY' => 'fa_timestamp DESC' )
01844                 );
01845 
01846                 $idsPresent = array();
01847                 $storeBatch = array();
01848                 $insertBatch = array();
01849                 $insertCurrent = false;
01850                 $deleteIds = array();
01851                 $first = true;
01852                 $archiveNames = array();
01853 
01854                 foreach ( $result as $row ) {
01855                         $idsPresent[] = $row->fa_id;
01856 
01857                         if ( $row->fa_name != $this->file->getName() ) {
01858                                 $status->error( 'undelete-filename-mismatch', $wgLang->timeanddate( $row->fa_timestamp ) );
01859                                 $status->failCount++;
01860                                 continue;
01861                         }
01862 
01863                         if ( $row->fa_storage_key == '' ) {
01864                                 // Revision was missing pre-deletion
01865                                 $status->error( 'undelete-bad-store-key', $wgLang->timeanddate( $row->fa_timestamp ) );
01866                                 $status->failCount++;
01867                                 continue;
01868                         }
01869 
01870                         $deletedRel = $this->file->repo->getDeletedHashPath( $row->fa_storage_key ) . $row->fa_storage_key;
01871                         $deletedUrl = $this->file->repo->getVirtualUrl() . '/deleted/' . $deletedRel;
01872 
01873                         $sha1 = substr( $row->fa_storage_key, 0, strcspn( $row->fa_storage_key, '.' ) );
01874 
01875                         # Fix leading zero
01876                         if ( strlen( $sha1 ) == 32 && $sha1[0] == '0' ) {
01877                                 $sha1 = substr( $sha1, 1 );
01878                         }
01879 
01880                         if ( is_null( $row->fa_major_mime ) || $row->fa_major_mime == 'unknown'
01881                                 || is_null( $row->fa_minor_mime ) || $row->fa_minor_mime == 'unknown'
01882                                 || is_null( $row->fa_media_type ) || $row->fa_media_type == 'UNKNOWN'
01883                                 || is_null( $row->fa_metadata ) ) {
01884                                 // Refresh our metadata
01885                                 // Required for a new current revision; nice for older ones too. :)
01886                                 $props = RepoGroup::singleton()->getFileProps( $deletedUrl );
01887                         } else {
01888                                 $props = array(
01889                                         'minor_mime' => $row->fa_minor_mime,
01890                                         'major_mime' => $row->fa_major_mime,
01891                                         'media_type' => $row->fa_media_type,
01892                                         'metadata'   => $row->fa_metadata
01893                                 );
01894                         }
01895 
01896                         if ( $first && !$exists ) {
01897                                 // This revision will be published as the new current version
01898                                 $destRel = $this->file->getRel();
01899                                 $insertCurrent = array(
01900                                         'img_name'        => $row->fa_name,
01901                                         'img_size'        => $row->fa_size,
01902                                         'img_width'       => $row->fa_width,
01903                                         'img_height'      => $row->fa_height,
01904                                         'img_metadata'    => $props['metadata'],
01905                                         'img_bits'        => $row->fa_bits,
01906                                         'img_media_type'  => $props['media_type'],
01907                                         'img_major_mime'  => $props['major_mime'],
01908                                         'img_minor_mime'  => $props['minor_mime'],
01909                                         'img_description' => $row->fa_description,
01910                                         'img_user'        => $row->fa_user,
01911                                         'img_user_text'   => $row->fa_user_text,
01912                                         'img_timestamp'   => $row->fa_timestamp,
01913                                         'img_sha1'        => $sha1
01914                                 );
01915 
01916                                 // The live (current) version cannot be hidden!
01917                                 if ( !$this->unsuppress && $row->fa_deleted ) {
01918                                         $storeBatch[] = array( $deletedUrl, 'public', $destRel );
01919                                         $this->cleanupBatch[] = $row->fa_storage_key;
01920                                 }
01921                         } else {
01922                                 $archiveName = $row->fa_archive_name;
01923 
01924                                 if ( $archiveName == '' ) {
01925                                         // This was originally a current version; we
01926                                         // have to devise a new archive name for it.
01927                                         // Format is <timestamp of archiving>!<name>
01928                                         $timestamp = wfTimestamp( TS_UNIX, $row->fa_deleted_timestamp );
01929 
01930                                         do {
01931                                                 $archiveName = wfTimestamp( TS_MW, $timestamp ) . '!' . $row->fa_name;
01932                                                 $timestamp++;
01933                                         } while ( isset( $archiveNames[$archiveName] ) );
01934                                 }
01935 
01936                                 $archiveNames[$archiveName] = true;
01937                                 $destRel = $this->file->getArchiveRel( $archiveName );
01938                                 $insertBatch[] = array(
01939                                         'oi_name'         => $row->fa_name,
01940                                         'oi_archive_name' => $archiveName,
01941                                         'oi_size'         => $row->fa_size,
01942                                         'oi_width'        => $row->fa_width,
01943                                         'oi_height'       => $row->fa_height,
01944                                         'oi_bits'         => $row->fa_bits,
01945                                         'oi_description'  => $row->fa_description,
01946                                         'oi_user'         => $row->fa_user,
01947                                         'oi_user_text'    => $row->fa_user_text,
01948                                         'oi_timestamp'    => $row->fa_timestamp,
01949                                         'oi_metadata'     => $props['metadata'],
01950                                         'oi_media_type'   => $props['media_type'],
01951                                         'oi_major_mime'   => $props['major_mime'],
01952                                         'oi_minor_mime'   => $props['minor_mime'],
01953                                         'oi_deleted'      => $this->unsuppress ? 0 : $row->fa_deleted,
01954                                         'oi_sha1'         => $sha1 );
01955                         }
01956 
01957                         $deleteIds[] = $row->fa_id;
01958 
01959                         if ( !$this->unsuppress && $row->fa_deleted & File::DELETED_FILE ) {
01960                                 // private files can stay where they are
01961                                 $status->successCount++;
01962                         } else {
01963                                 $storeBatch[] = array( $deletedUrl, 'public', $destRel );
01964                                 $this->cleanupBatch[] = $row->fa_storage_key;
01965                         }
01966 
01967                         $first = false;
01968                 }
01969 
01970                 unset( $result );
01971 
01972                 // Add a warning to the status object for missing IDs
01973                 $missingIds = array_diff( $this->ids, $idsPresent );
01974 
01975                 foreach ( $missingIds as $id ) {
01976                         $status->error( 'undelete-missing-filearchive', $id );
01977                 }
01978 
01979                 // Remove missing files from batch, so we don't get errors when undeleting them
01980                 $storeBatch = $this->removeNonexistentFiles( $storeBatch );
01981 
01982                 // Run the store batch
01983                 // Use the OVERWRITE_SAME flag to smooth over a common error
01984                 $storeStatus = $this->file->repo->storeBatch( $storeBatch, FileRepo::OVERWRITE_SAME );
01985                 $status->merge( $storeStatus );
01986 
01987                 if ( !$status->isGood() ) {
01988                         // Even if some files could be copied, fail entirely as that is the
01989                         // easiest thing to do without data loss
01990                         $this->cleanupFailedBatch( $storeStatus, $storeBatch );
01991                         $status->ok = false;
01992                         $this->file->unlock();
01993 
01994                         return $status;
01995                 }
01996 
01997                 // Run the DB updates
01998                 // Because we have locked the image row, key conflicts should be rare.
01999                 // If they do occur, we can roll back the transaction at this time with
02000                 // no data loss, but leaving unregistered files scattered throughout the
02001                 // public zone.
02002                 // This is not ideal, which is why it's important to lock the image row.
02003                 if ( $insertCurrent ) {
02004                         $dbw->insert( 'image', $insertCurrent, __METHOD__ );
02005                 }
02006 
02007                 if ( $insertBatch ) {
02008                         $dbw->insert( 'oldimage', $insertBatch, __METHOD__ );
02009                 }
02010 
02011                 if ( $deleteIds ) {
02012                         $dbw->delete( 'filearchive',
02013                                 array( 'fa_id IN (' . $dbw->makeList( $deleteIds ) . ')' ),
02014                                 __METHOD__ );
02015                 }
02016 
02017                 // If store batch is empty (all files are missing), deletion is to be considered successful
02018                 if ( $status->successCount > 0 || !$storeBatch ) {
02019                         if ( !$exists ) {
02020                                 wfDebug( __METHOD__ . " restored {$status->successCount} items, creating a new current\n" );
02021 
02022                                 // Update site_stats
02023                                 $site_stats = $dbw->tableName( 'site_stats' );
02024                                 $dbw->query( "UPDATE $site_stats SET ss_images=ss_images+1", __METHOD__ );
02025 
02026                                 $this->file->purgeEverything();
02027                         } else {
02028                                 wfDebug( __METHOD__ . " restored {$status->successCount} as archived versions\n" );
02029                                 $this->file->purgeDescription();
02030                                 $this->file->purgeHistory();
02031                         }
02032                 }
02033 
02034                 $this->file->unlock();
02035 
02036                 return $status;
02037         }
02038 
02042         function removeNonexistentFiles( $triplets ) {
02043                 $files = $filteredTriplets = array();
02044                 foreach ( $triplets as $file )
02045                         $files[$file[0]] = $file[0];
02046 
02047                 $result = $this->file->repo->fileExistsBatch( $files, FileRepo::FILES_ONLY );
02048 
02049                 foreach ( $triplets as $file ) {
02050                         if ( $result[$file[0]] ) {
02051                                 $filteredTriplets[] = $file;
02052                         }
02053                 }
02054 
02055                 return $filteredTriplets;
02056         }
02057 
02061         function removeNonexistentFromCleanup( $batch ) {
02062                 $files = $newBatch = array();
02063                 $repo = $this->file->repo;
02064 
02065                 foreach ( $batch as $file ) {
02066                         $files[$file] = $repo->getVirtualUrl( 'deleted' ) . '/' .
02067                                 rawurlencode( $repo->getDeletedHashPath( $file ) . $file );
02068                 }
02069 
02070                 $result = $repo->fileExistsBatch( $files, FileRepo::FILES_ONLY );
02071 
02072                 foreach ( $batch as $file ) {
02073                         if ( $result[$file] ) {
02074                                 $newBatch[] = $file;
02075                         }
02076                 }
02077 
02078                 return $newBatch;
02079         }
02080 
02085         function cleanup() {
02086                 if ( !$this->cleanupBatch ) {
02087                         return $this->file->repo->newGood();
02088                 }
02089 
02090                 $this->cleanupBatch = $this->removeNonexistentFromCleanup( $this->cleanupBatch );
02091 
02092                 $status = $this->file->repo->cleanupDeletedBatch( $this->cleanupBatch );
02093 
02094                 return $status;
02095         }
02096 
02104         function cleanupFailedBatch( $storeStatus, $storeBatch ) {
02105                 $cleanupBatch = array();
02106 
02107                 foreach ( $storeStatus->success as $i => $success ) {
02108                         // Check if this item of the batch was successfully copied
02109                         if ( $success ) {
02110                                 // Item was successfully copied and needs to be removed again
02111                                 // Extract ($dstZone, $dstRel) from the batch
02112                                 $cleanupBatch[] = array( $storeBatch[$i][1], $storeBatch[$i][2] );
02113                         }
02114                 }
02115                 $this->file->repo->cleanupBatch( $cleanupBatch );
02116         }
02117 }
02118 
02119 # ------------------------------------------------------------------------------
02120 
02125 class LocalFileMoveBatch {
02126 
02130         var $file;
02131 
02135         var $target;
02136 
02137         var $cur, $olds, $oldCount, $archive, $db;
02138 
02139         function __construct( File $file, Title $target ) {
02140                 $this->file = $file;
02141                 $this->target = $target;
02142                 $this->oldHash = $this->file->repo->getHashPath( $this->file->getName() );
02143                 $this->newHash = $this->file->repo->getHashPath( $this->target->getDBkey() );
02144                 $this->oldName = $this->file->getName();
02145                 $this->newName = $this->file->repo->getNameFromTitle( $this->target );
02146                 $this->oldRel = $this->oldHash . $this->oldName;
02147                 $this->newRel = $this->newHash . $this->newName;
02148                 $this->db = $file->repo->getMasterDb();
02149         }
02150 
02154         function addCurrent() {
02155                 $this->cur = array( $this->oldRel, $this->newRel );
02156         }
02157 
02161         function addOlds() {
02162                 $archiveBase = 'archive';
02163                 $this->olds = array();
02164                 $this->oldCount = 0;
02165 
02166                 $result = $this->db->select( 'oldimage',
02167                         array( 'oi_archive_name', 'oi_deleted' ),
02168                         array( 'oi_name' => $this->oldName ),
02169                         __METHOD__
02170                 );
02171 
02172                 foreach ( $result as $row ) {
02173                         $oldName = $row->oi_archive_name;
02174                         $bits = explode( '!', $oldName, 2 );
02175 
02176                         if ( count( $bits ) != 2 ) {
02177                                 wfDebug( "Old file name missing !: '$oldName' \n" );
02178                                 continue;
02179                         }
02180 
02181                         list( $timestamp, $filename ) = $bits;
02182 
02183                         if ( $this->oldName != $filename ) {
02184                                 wfDebug( "Old file name doesn't match: '$oldName' \n" );
02185                                 continue;
02186                         }
02187 
02188                         $this->oldCount++;
02189 
02190                         // Do we want to add those to oldCount?
02191                         if ( $row->oi_deleted & File::DELETED_FILE ) {
02192                                 continue;
02193                         }
02194 
02195                         $this->olds[] = array(
02196                                 "{$archiveBase}/{$this->oldHash}{$oldName}",
02197                                 "{$archiveBase}/{$this->newHash}{$timestamp}!{$this->newName}"
02198                         );
02199                 }
02200         }
02201 
02205         function execute() {
02206                 $repo = $this->file->repo;
02207                 $status = $repo->newGood();
02208                 $triplets = $this->getMoveTriplets();
02209 
02210                 $triplets = $this->removeNonexistentFiles( $triplets );
02211 
02212                 // Copy the files into their new location
02213                 $statusMove = $repo->storeBatch( $triplets );
02214                 wfDebugLog( 'imagemove', "Moved files for {$this->file->getName()}: {$statusMove->successCount} successes, {$statusMove->failCount} failures" );
02215                 if ( !$statusMove->isGood() ) {
02216                         wfDebugLog( 'imagemove', "Error in moving files: " . $statusMove->getWikiText() );
02217                         $this->cleanupTarget( $triplets );
02218                         $statusMove->ok = false;
02219                         return $statusMove;
02220                 }
02221 
02222                 $this->db->begin();
02223                 $statusDb = $this->doDBUpdates();
02224                 wfDebugLog( 'imagemove', "Renamed {$this->file->getName()} in database: {$statusDb->successCount} successes, {$statusDb->failCount} failures" );
02225                 if ( !$statusDb->isGood() ) {
02226                         $this->db->rollback();
02227                         // Something went wrong with the DB updates, so remove the target files
02228                         $this->cleanupTarget( $triplets );
02229                         $statusDb->ok = false;
02230                         return $statusDb;
02231                 }
02232                 $this->db->commit();
02233 
02234                 // Everything went ok, remove the source files
02235                 $this->cleanupSource( $triplets );
02236 
02237                 $status->merge( $statusDb );
02238                 $status->merge( $statusMove );
02239 
02240                 return $status;
02241         }
02242 
02249         function doDBUpdates() {
02250                 $repo = $this->file->repo;
02251                 $status = $repo->newGood();
02252                 $dbw = $this->db;
02253 
02254                 // Update current image
02255                 $dbw->update(
02256                         'image',
02257                         array( 'img_name' => $this->newName ),
02258                         array( 'img_name' => $this->oldName ),
02259                         __METHOD__
02260                 );
02261 
02262                 if ( $dbw->affectedRows() ) {
02263                         $status->successCount++;
02264                 } else {
02265                         $status->failCount++;
02266                         $status->fatal( 'imageinvalidfilename' );
02267                         return $status;
02268                 }
02269 
02270                 // Update old images
02271                 $dbw->update(
02272                         'oldimage',
02273                         array(
02274                                 'oi_name' => $this->newName,
02275                                 'oi_archive_name = ' . $dbw->strreplace( 'oi_archive_name', $dbw->addQuotes( $this->oldName ), $dbw->addQuotes( $this->newName ) ),
02276                         ),
02277                         array( 'oi_name' => $this->oldName ),
02278                         __METHOD__
02279                 );
02280 
02281                 $affected = $dbw->affectedRows();
02282                 $total = $this->oldCount;
02283                 $status->successCount += $affected;
02284                 $status->failCount += $total - $affected;
02285                 if ( $status->failCount ) {
02286                         $status->error( 'imageinvalidfilename' );
02287                 }
02288 
02289                 return $status;
02290         }
02291 
02295         function getMoveTriplets() {
02296                 $moves = array_merge( array( $this->cur ), $this->olds );
02297                 $triplets = array();    // The format is: (srcUrl, destZone, destUrl)
02298 
02299                 foreach ( $moves as $move ) {
02300                         // $move: (oldRelativePath, newRelativePath)
02301                         $srcUrl = $this->file->repo->getVirtualUrl() . '/public/' . rawurlencode( $move[0] );
02302                         $triplets[] = array( $srcUrl, 'public', $move[1] );
02303                         wfDebugLog( 'imagemove', "Generated move triplet for {$this->file->getName()}: {$srcUrl} :: public :: {$move[1]}" );
02304                 }
02305 
02306                 return $triplets;
02307         }
02308 
02312         function removeNonexistentFiles( $triplets ) {
02313                 $files = array();
02314 
02315                 foreach ( $triplets as $file ) {
02316                         $files[$file[0]] = $file[0];
02317                 }
02318 
02319                 $result = $this->file->repo->fileExistsBatch( $files, FileRepo::FILES_ONLY );
02320                 $filteredTriplets = array();
02321 
02322                 foreach ( $triplets as $file ) {
02323                         if ( $result[$file[0]] ) {
02324                                 $filteredTriplets[] = $file;
02325                         } else {
02326                                 wfDebugLog( 'imagemove', "File {$file[0]} does not exist" );
02327                         }
02328                 }
02329 
02330                 return $filteredTriplets;
02331         }
02332 
02337         function cleanupTarget( $triplets ) {
02338                 // Create dest pairs from the triplets
02339                 $pairs = array();
02340                 foreach ( $triplets as $triplet ) {
02341                         $pairs[] = array( $triplet[1], $triplet[2] );
02342                 }
02343 
02344                 $this->file->repo->cleanupBatch( $pairs );
02345         }
02346 
02351         function cleanupSource( $triplets ) {
02352                 // Create source file names from the triplets
02353                 $files = array();
02354                 foreach ( $triplets as $triplet ) {
02355                         $files[] = $triplet[0];
02356                 }
02357 
02358                 $this->file->repo->cleanupBatch( $files );
02359         }
02360 }