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