MediaWiki
REL1_19
|
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 }