MediaWiki
REL1_20
|
00001 <?php 00037 class FileRepo { 00038 const DELETE_SOURCE = 1; 00039 const OVERWRITE = 2; 00040 const OVERWRITE_SAME = 4; 00041 const SKIP_LOCKING = 8; 00042 00044 protected $backend; 00046 protected $zones = array(); 00047 00048 var $thumbScriptUrl, $transformVia404; 00049 var $descBaseUrl, $scriptDirUrl, $scriptExtension, $articleUrl; 00050 var $fetchDescription, $initialCapital; 00051 var $pathDisclosureProtection = 'simple'; // 'paranoid' 00052 var $descriptionCacheExpiry, $url, $thumbUrl; 00053 var $hashLevels, $deletedHashLevels; 00054 protected $abbrvThreshold; 00055 00060 var $fileFactory = array( 'UnregisteredLocalFile', 'newFromTitle' ); 00061 var $oldFileFactory = false; 00062 var $fileFactoryKey = false, $oldFileFactoryKey = false; 00063 00068 public function __construct( array $info = null ) { 00069 // Verify required settings presence 00070 if( 00071 $info === null 00072 || !array_key_exists( 'name', $info ) 00073 || !array_key_exists( 'backend', $info ) 00074 ) { 00075 throw new MWException( __CLASS__ . " requires an array of options having both 'name' and 'backend' keys.\n" ); 00076 } 00077 00078 // Required settings 00079 $this->name = $info['name']; 00080 if ( $info['backend'] instanceof FileBackend ) { 00081 $this->backend = $info['backend']; // useful for testing 00082 } else { 00083 $this->backend = FileBackendGroup::singleton()->get( $info['backend'] ); 00084 } 00085 00086 // Optional settings that can have no value 00087 $optionalSettings = array( 00088 'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription', 00089 'thumbScriptUrl', 'pathDisclosureProtection', 'descriptionCacheExpiry', 00090 'scriptExtension' 00091 ); 00092 foreach ( $optionalSettings as $var ) { 00093 if ( isset( $info[$var] ) ) { 00094 $this->$var = $info[$var]; 00095 } 00096 } 00097 00098 // Optional settings that have a default 00099 $this->initialCapital = isset( $info['initialCapital'] ) 00100 ? $info['initialCapital'] 00101 : MWNamespace::isCapitalized( NS_FILE ); 00102 $this->url = isset( $info['url'] ) 00103 ? $info['url'] 00104 : false; // a subclass may set the URL (e.g. ForeignAPIRepo) 00105 if ( isset( $info['thumbUrl'] ) ) { 00106 $this->thumbUrl = $info['thumbUrl']; 00107 } else { 00108 $this->thumbUrl = $this->url ? "{$this->url}/thumb" : false; 00109 } 00110 $this->hashLevels = isset( $info['hashLevels'] ) 00111 ? $info['hashLevels'] 00112 : 2; 00113 $this->deletedHashLevels = isset( $info['deletedHashLevels'] ) 00114 ? $info['deletedHashLevels'] 00115 : $this->hashLevels; 00116 $this->transformVia404 = !empty( $info['transformVia404'] ); 00117 $this->abbrvThreshold = isset( $info['abbrvThreshold'] ) 00118 ? $info['abbrvThreshold'] 00119 : 255; 00120 $this->isPrivate = !empty( $info['isPrivate'] ); 00121 // Give defaults for the basic zones... 00122 $this->zones = isset( $info['zones'] ) ? $info['zones'] : array(); 00123 foreach ( array( 'public', 'thumb', 'temp', 'deleted' ) as $zone ) { 00124 if ( !isset( $this->zones[$zone]['container'] ) ) { 00125 $this->zones[$zone]['container'] = "{$this->name}-{$zone}"; 00126 } 00127 if ( !isset( $this->zones[$zone]['directory'] ) ) { 00128 $this->zones[$zone]['directory'] = ''; 00129 } 00130 } 00131 } 00132 00138 public function getBackend() { 00139 return $this->backend; 00140 } 00141 00148 public function getReadOnlyReason() { 00149 return $this->backend->getReadOnlyReason(); 00150 } 00151 00159 protected function initZones( $doZones = array() ) { 00160 $status = $this->newGood(); 00161 foreach ( (array)$doZones as $zone ) { 00162 $root = $this->getZonePath( $zone ); 00163 if ( $root === null ) { 00164 throw new MWException( "No '$zone' zone defined in the {$this->name} repo." ); 00165 } 00166 } 00167 return $status; 00168 } 00169 00176 public static function isVirtualUrl( $url ) { 00177 return substr( $url, 0, 9 ) == 'mwrepo://'; 00178 } 00179 00188 public function getVirtualUrl( $suffix = false ) { 00189 $path = 'mwrepo://' . $this->name; 00190 if ( $suffix !== false ) { 00191 $path .= '/' . rawurlencode( $suffix ); 00192 } 00193 return $path; 00194 } 00195 00202 public function getZoneUrl( $zone ) { 00203 if ( isset( $this->zones[$zone]['url'] ) 00204 && in_array( $zone, array( 'public', 'temp', 'thumb' ) ) ) 00205 { 00206 return $this->zones[$zone]['url']; // custom URL 00207 } 00208 switch ( $zone ) { 00209 case 'public': 00210 return $this->url; 00211 case 'temp': 00212 return "{$this->url}/temp"; 00213 case 'deleted': 00214 return false; // no public URL 00215 case 'thumb': 00216 return $this->thumbUrl; 00217 default: 00218 return false; 00219 } 00220 } 00221 00235 public function getZoneHandlerUrl( $zone ) { 00236 if ( isset( $this->zones[$zone]['handlerUrl'] ) 00237 && in_array( $zone, array( 'public', 'temp', 'thumb' ) ) ) 00238 { 00239 return $this->zones[$zone]['handlerUrl']; 00240 } 00241 return false; 00242 } 00243 00252 public function resolveVirtualUrl( $url ) { 00253 if ( substr( $url, 0, 9 ) != 'mwrepo://' ) { 00254 throw new MWException( __METHOD__.': unknown protocol' ); 00255 } 00256 $bits = explode( '/', substr( $url, 9 ), 3 ); 00257 if ( count( $bits ) != 3 ) { 00258 throw new MWException( __METHOD__.": invalid mwrepo URL: $url" ); 00259 } 00260 list( $repo, $zone, $rel ) = $bits; 00261 if ( $repo !== $this->name ) { 00262 throw new MWException( __METHOD__.": fetching from a foreign repo is not supported" ); 00263 } 00264 $base = $this->getZonePath( $zone ); 00265 if ( !$base ) { 00266 throw new MWException( __METHOD__.": invalid zone: $zone" ); 00267 } 00268 return $base . '/' . rawurldecode( $rel ); 00269 } 00270 00277 protected function getZoneLocation( $zone ) { 00278 if ( !isset( $this->zones[$zone] ) ) { 00279 return array( null, null ); // bogus 00280 } 00281 return array( $this->zones[$zone]['container'], $this->zones[$zone]['directory'] ); 00282 } 00283 00290 public function getZonePath( $zone ) { 00291 list( $container, $base ) = $this->getZoneLocation( $zone ); 00292 if ( $container === null || $base === null ) { 00293 return null; 00294 } 00295 $backendName = $this->backend->getName(); 00296 if ( $base != '' ) { // may not be set 00297 $base = "/{$base}"; 00298 } 00299 return "mwstore://$backendName/{$container}{$base}"; 00300 } 00301 00313 public function newFile( $title, $time = false ) { 00314 $title = File::normalizeTitle( $title ); 00315 if ( !$title ) { 00316 return null; 00317 } 00318 if ( $time ) { 00319 if ( $this->oldFileFactory ) { 00320 return call_user_func( $this->oldFileFactory, $title, $this, $time ); 00321 } else { 00322 return false; 00323 } 00324 } else { 00325 return call_user_func( $this->fileFactory, $title, $this ); 00326 } 00327 } 00328 00347 public function findFile( $title, $options = array() ) { 00348 $title = File::normalizeTitle( $title ); 00349 if ( !$title ) { 00350 return false; 00351 } 00352 $time = isset( $options['time'] ) ? $options['time'] : false; 00353 # First try the current version of the file to see if it precedes the timestamp 00354 $img = $this->newFile( $title ); 00355 if ( !$img ) { 00356 return false; 00357 } 00358 if ( $img->exists() && ( !$time || $img->getTimestamp() == $time ) ) { 00359 return $img; 00360 } 00361 # Now try an old version of the file 00362 if ( $time !== false ) { 00363 $img = $this->newFile( $title, $time ); 00364 if ( $img && $img->exists() ) { 00365 if ( !$img->isDeleted( File::DELETED_FILE ) ) { 00366 return $img; // always OK 00367 } elseif ( !empty( $options['private'] ) && $img->userCan( File::DELETED_FILE ) ) { 00368 return $img; 00369 } 00370 } 00371 } 00372 00373 # Now try redirects 00374 if ( !empty( $options['ignoreRedirect'] ) ) { 00375 return false; 00376 } 00377 $redir = $this->checkRedirect( $title ); 00378 if ( $redir && $title->getNamespace() == NS_FILE) { 00379 $img = $this->newFile( $redir ); 00380 if ( !$img ) { 00381 return false; 00382 } 00383 if ( $img->exists() ) { 00384 $img->redirectedFrom( $title->getDBkey() ); 00385 return $img; 00386 } 00387 } 00388 return false; 00389 } 00390 00402 public function findFiles( array $items ) { 00403 $result = array(); 00404 foreach ( $items as $item ) { 00405 if ( is_array( $item ) ) { 00406 $title = $item['title']; 00407 $options = $item; 00408 unset( $options['title'] ); 00409 } else { 00410 $title = $item; 00411 $options = array(); 00412 } 00413 $file = $this->findFile( $title, $options ); 00414 if ( $file ) { 00415 $result[$file->getTitle()->getDBkey()] = $file; 00416 } 00417 } 00418 return $result; 00419 } 00420 00430 public function findFileFromKey( $sha1, $options = array() ) { 00431 $time = isset( $options['time'] ) ? $options['time'] : false; 00432 # First try to find a matching current version of a file... 00433 if ( $this->fileFactoryKey ) { 00434 $img = call_user_func( $this->fileFactoryKey, $sha1, $this, $time ); 00435 } else { 00436 return false; // find-by-sha1 not supported 00437 } 00438 if ( $img && $img->exists() ) { 00439 return $img; 00440 } 00441 # Now try to find a matching old version of a file... 00442 if ( $time !== false && $this->oldFileFactoryKey ) { // find-by-sha1 supported? 00443 $img = call_user_func( $this->oldFileFactoryKey, $sha1, $this, $time ); 00444 if ( $img && $img->exists() ) { 00445 if ( !$img->isDeleted( File::DELETED_FILE ) ) { 00446 return $img; // always OK 00447 } elseif ( !empty( $options['private'] ) && $img->userCan( File::DELETED_FILE ) ) { 00448 return $img; 00449 } 00450 } 00451 } 00452 return false; 00453 } 00454 00463 public function findBySha1( $hash ) { 00464 return array(); 00465 } 00466 00474 public function findBySha1s( array $hashes ) { 00475 $result = array(); 00476 foreach ( $hashes as $hash ) { 00477 $files = $this->findBySha1( $hash ); 00478 if ( count( $files ) ) { 00479 $result[$hash] = $files; 00480 } 00481 } 00482 return $result; 00483 } 00484 00491 public function getRootUrl() { 00492 return $this->getZoneUrl( 'public' ); 00493 } 00494 00500 public function getThumbScriptUrl() { 00501 return $this->thumbScriptUrl; 00502 } 00503 00509 public function canTransformVia404() { 00510 return $this->transformVia404; 00511 } 00512 00519 public function getNameFromTitle( Title $title ) { 00520 global $wgContLang; 00521 if ( $this->initialCapital != MWNamespace::isCapitalized( NS_FILE ) ) { 00522 $name = $title->getUserCaseDBKey(); 00523 if ( $this->initialCapital ) { 00524 $name = $wgContLang->ucfirst( $name ); 00525 } 00526 } else { 00527 $name = $title->getDBkey(); 00528 } 00529 return $name; 00530 } 00531 00537 public function getRootDirectory() { 00538 return $this->getZonePath( 'public' ); 00539 } 00540 00548 public function getHashPath( $name ) { 00549 return self::getHashPathForLevel( $name, $this->hashLevels ); 00550 } 00551 00559 public function getTempHashPath( $suffix ) { 00560 $parts = explode( '!', $suffix, 2 ); // format is <timestamp>!<name> or just <name> 00561 $name = isset( $parts[1] ) ? $parts[1] : $suffix; // hash path is not based on timestamp 00562 return self::getHashPathForLevel( $name, $this->hashLevels ); 00563 } 00564 00570 protected static function getHashPathForLevel( $name, $levels ) { 00571 if ( $levels == 0 ) { 00572 return ''; 00573 } else { 00574 $hash = md5( $name ); 00575 $path = ''; 00576 for ( $i = 1; $i <= $levels; $i++ ) { 00577 $path .= substr( $hash, 0, $i ) . '/'; 00578 } 00579 return $path; 00580 } 00581 } 00582 00588 public function getHashLevels() { 00589 return $this->hashLevels; 00590 } 00591 00597 public function getName() { 00598 return $this->name; 00599 } 00600 00608 public function makeUrl( $query = '', $entry = 'index' ) { 00609 if ( isset( $this->scriptDirUrl ) ) { 00610 $ext = isset( $this->scriptExtension ) ? $this->scriptExtension : '.php'; 00611 return wfAppendQuery( "{$this->scriptDirUrl}/{$entry}{$ext}", $query ); 00612 } 00613 return false; 00614 } 00615 00628 public function getDescriptionUrl( $name ) { 00629 $encName = wfUrlencode( $name ); 00630 if ( !is_null( $this->descBaseUrl ) ) { 00631 # "http://example.com/wiki/Image:" 00632 return $this->descBaseUrl . $encName; 00633 } 00634 if ( !is_null( $this->articleUrl ) ) { 00635 # "http://example.com/wiki/$1" 00636 # 00637 # We use "Image:" as the canonical namespace for 00638 # compatibility across all MediaWiki versions. 00639 return str_replace( '$1', 00640 "Image:$encName", $this->articleUrl ); 00641 } 00642 if ( !is_null( $this->scriptDirUrl ) ) { 00643 # "http://example.com/w" 00644 # 00645 # We use "Image:" as the canonical namespace for 00646 # compatibility across all MediaWiki versions, 00647 # and just sort of hope index.php is right. ;) 00648 return $this->makeUrl( "title=Image:$encName" ); 00649 } 00650 return false; 00651 } 00652 00663 public function getDescriptionRenderUrl( $name, $lang = null ) { 00664 $query = 'action=render'; 00665 if ( !is_null( $lang ) ) { 00666 $query .= '&uselang=' . $lang; 00667 } 00668 if ( isset( $this->scriptDirUrl ) ) { 00669 return $this->makeUrl( 00670 'title=' . 00671 wfUrlencode( 'Image:' . $name ) . 00672 "&$query" ); 00673 } else { 00674 $descUrl = $this->getDescriptionUrl( $name ); 00675 if ( $descUrl ) { 00676 return wfAppendQuery( $descUrl, $query ); 00677 } else { 00678 return false; 00679 } 00680 } 00681 } 00682 00688 public function getDescriptionStylesheetUrl() { 00689 if ( isset( $this->scriptDirUrl ) ) { 00690 return $this->makeUrl( 'title=MediaWiki:Filepage.css&' . 00691 wfArrayToCGI( Skin::getDynamicStylesheetQuery() ) ); 00692 } 00693 return false; 00694 } 00695 00710 public function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) { 00711 $this->assertWritableRepo(); // fail out if read-only 00712 00713 $status = $this->storeBatch( array( array( $srcPath, $dstZone, $dstRel ) ), $flags ); 00714 if ( $status->successCount == 0 ) { 00715 $status->ok = false; 00716 } 00717 00718 return $status; 00719 } 00720 00734 public function storeBatch( array $triplets, $flags = 0 ) { 00735 $this->assertWritableRepo(); // fail out if read-only 00736 00737 $status = $this->newGood(); 00738 $backend = $this->backend; // convenience 00739 00740 $operations = array(); 00741 $sourceFSFilesToDelete = array(); // cleanup for disk source files 00742 // Validate each triplet and get the store operation... 00743 foreach ( $triplets as $triplet ) { 00744 list( $srcPath, $dstZone, $dstRel ) = $triplet; 00745 wfDebug( __METHOD__ 00746 . "( \$src='$srcPath', \$dstZone='$dstZone', \$dstRel='$dstRel' )\n" 00747 ); 00748 00749 // Resolve destination path 00750 $root = $this->getZonePath( $dstZone ); 00751 if ( !$root ) { 00752 throw new MWException( "Invalid zone: $dstZone" ); 00753 } 00754 if ( !$this->validateFilename( $dstRel ) ) { 00755 throw new MWException( 'Validation error in $dstRel' ); 00756 } 00757 $dstPath = "$root/$dstRel"; 00758 $dstDir = dirname( $dstPath ); 00759 // Create destination directories for this triplet 00760 if ( !$this->initDirectory( $dstDir )->isOK() ) { 00761 return $this->newFatal( 'directorycreateerror', $dstDir ); 00762 } 00763 00764 // Resolve source to a storage path if virtual 00765 $srcPath = $this->resolveToStoragePath( $srcPath ); 00766 00767 // Get the appropriate file operation 00768 if ( FileBackend::isStoragePath( $srcPath ) ) { 00769 $opName = ( $flags & self::DELETE_SOURCE ) ? 'move' : 'copy'; 00770 } else { 00771 $opName = 'store'; 00772 if ( $flags & self::DELETE_SOURCE ) { 00773 $sourceFSFilesToDelete[] = $srcPath; 00774 } 00775 } 00776 $operations[] = array( 00777 'op' => $opName, 00778 'src' => $srcPath, 00779 'dst' => $dstPath, 00780 'overwrite' => $flags & self::OVERWRITE, 00781 'overwriteSame' => $flags & self::OVERWRITE_SAME, 00782 ); 00783 } 00784 00785 // Execute the store operation for each triplet 00786 $opts = array( 'force' => true ); 00787 if ( $flags & self::SKIP_LOCKING ) { 00788 $opts['nonLocking'] = true; 00789 } 00790 $status->merge( $backend->doOperations( $operations, $opts ) ); 00791 // Cleanup for disk source files... 00792 foreach ( $sourceFSFilesToDelete as $file ) { 00793 wfSuppressWarnings(); 00794 unlink( $file ); // FS cleanup 00795 wfRestoreWarnings(); 00796 } 00797 00798 return $status; 00799 } 00800 00811 public function cleanupBatch( array $files, $flags = 0 ) { 00812 $this->assertWritableRepo(); // fail out if read-only 00813 00814 $status = $this->newGood(); 00815 00816 $operations = array(); 00817 foreach ( $files as $path ) { 00818 if ( is_array( $path ) ) { 00819 // This is a pair, extract it 00820 list( $zone, $rel ) = $path; 00821 $path = $this->getZonePath( $zone ) . "/$rel"; 00822 } else { 00823 // Resolve source to a storage path if virtual 00824 $path = $this->resolveToStoragePath( $path ); 00825 } 00826 $operations[] = array( 'op' => 'delete', 'src' => $path ); 00827 } 00828 // Actually delete files from storage... 00829 $opts = array( 'force' => true ); 00830 if ( $flags & self::SKIP_LOCKING ) { 00831 $opts['nonLocking'] = true; 00832 } 00833 $status->merge( $this->backend->doOperations( $operations, $opts ) ); 00834 00835 return $status; 00836 } 00837 00849 final public function quickImport( $src, $dst, $disposition = null ) { 00850 return $this->quickImportBatch( array( array( $src, $dst, $disposition ) ) ); 00851 } 00852 00861 final public function quickPurge( $path ) { 00862 return $this->quickPurgeBatch( array( $path ) ); 00863 } 00864 00872 public function quickCleanDir( $dir ) { 00873 $status = $this->newGood(); 00874 $status->merge( $this->backend->clean( 00875 array( 'dir' => $this->resolveToStoragePath( $dir ) ) ) ); 00876 00877 return $status; 00878 } 00879 00892 public function quickImportBatch( array $triples ) { 00893 $status = $this->newGood(); 00894 $operations = array(); 00895 foreach ( $triples as $triple ) { 00896 list( $src, $dst ) = $triple; 00897 $src = $this->resolveToStoragePath( $src ); 00898 $dst = $this->resolveToStoragePath( $dst ); 00899 $operations[] = array( 00900 'op' => FileBackend::isStoragePath( $src ) ? 'copy' : 'store', 00901 'src' => $src, 00902 'dst' => $dst, 00903 'disposition' => isset( $triple[2] ) ? $triple[2] : null 00904 ); 00905 $status->merge( $this->initDirectory( dirname( $dst ) ) ); 00906 } 00907 $status->merge( $this->backend->doQuickOperations( $operations ) ); 00908 00909 return $status; 00910 } 00911 00920 public function quickPurgeBatch( array $paths ) { 00921 $status = $this->newGood(); 00922 $operations = array(); 00923 foreach ( $paths as $path ) { 00924 $operations[] = array( 00925 'op' => 'delete', 00926 'src' => $this->resolveToStoragePath( $path ), 00927 'ignoreMissingSource' => true 00928 ); 00929 } 00930 $status->merge( $this->backend->doQuickOperations( $operations ) ); 00931 00932 return $status; 00933 } 00934 00945 public function storeTemp( $originalName, $srcPath ) { 00946 $this->assertWritableRepo(); // fail out if read-only 00947 00948 $date = gmdate( "YmdHis" ); 00949 $hashPath = $this->getHashPath( $originalName ); 00950 $dstRel = "{$hashPath}{$date}!{$originalName}"; 00951 $dstUrlRel = $hashPath . $date . '!' . rawurlencode( $originalName ); 00952 $virtualUrl = $this->getVirtualUrl( 'temp' ) . '/' . $dstUrlRel; 00953 00954 $result = $this->quickImport( $srcPath, $virtualUrl ); 00955 $result->value = $virtualUrl; 00956 00957 return $result; 00958 } 00959 00966 public function freeTemp( $virtualUrl ) { 00967 $this->assertWritableRepo(); // fail out if read-only 00968 00969 $temp = $this->getVirtualUrl( 'temp' ); 00970 if ( substr( $virtualUrl, 0, strlen( $temp ) ) != $temp ) { 00971 wfDebug( __METHOD__.": Invalid temp virtual URL\n" ); 00972 return false; 00973 } 00974 00975 return $this->quickPurge( $virtualUrl )->isOK(); 00976 } 00977 00987 public function concatenate( array $srcPaths, $dstPath, $flags = 0 ) { 00988 $this->assertWritableRepo(); // fail out if read-only 00989 00990 $status = $this->newGood(); 00991 00992 $sources = array(); 00993 foreach ( $srcPaths as $srcPath ) { 00994 // Resolve source to a storage path if virtual 00995 $source = $this->resolveToStoragePath( $srcPath ); 00996 $sources[] = $source; // chunk to merge 00997 } 00998 00999 // Concatenate the chunks into one FS file 01000 $params = array( 'srcs' => $sources, 'dst' => $dstPath ); 01001 $status->merge( $this->backend->concatenate( $params ) ); 01002 if ( !$status->isOK() ) { 01003 return $status; 01004 } 01005 01006 // Delete the sources if required 01007 if ( $flags & self::DELETE_SOURCE ) { 01008 $status->merge( $this->quickPurgeBatch( $srcPaths ) ); 01009 } 01010 01011 // Make sure status is OK, despite any quickPurgeBatch() fatals 01012 $status->setResult( true ); 01013 01014 return $status; 01015 } 01016 01032 public function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) { 01033 $this->assertWritableRepo(); // fail out if read-only 01034 01035 $status = $this->publishBatch( array( array( $srcPath, $dstRel, $archiveRel ) ), $flags ); 01036 if ( $status->successCount == 0 ) { 01037 $status->ok = false; 01038 } 01039 if ( isset( $status->value[0] ) ) { 01040 $status->value = $status->value[0]; 01041 } else { 01042 $status->value = false; 01043 } 01044 01045 return $status; 01046 } 01047 01057 public function publishBatch( array $triplets, $flags = 0 ) { 01058 $this->assertWritableRepo(); // fail out if read-only 01059 01060 $backend = $this->backend; // convenience 01061 // Try creating directories 01062 $status = $this->initZones( 'public' ); 01063 if ( !$status->isOK() ) { 01064 return $status; 01065 } 01066 01067 $status = $this->newGood( array() ); 01068 01069 $operations = array(); 01070 $sourceFSFilesToDelete = array(); // cleanup for disk source files 01071 // Validate each triplet and get the store operation... 01072 foreach ( $triplets as $i => $triplet ) { 01073 list( $srcPath, $dstRel, $archiveRel ) = $triplet; 01074 // Resolve source to a storage path if virtual 01075 $srcPath = $this->resolveToStoragePath( $srcPath ); 01076 if ( !$this->validateFilename( $dstRel ) ) { 01077 throw new MWException( 'Validation error in $dstRel' ); 01078 } 01079 if ( !$this->validateFilename( $archiveRel ) ) { 01080 throw new MWException( 'Validation error in $archiveRel' ); 01081 } 01082 01083 $publicRoot = $this->getZonePath( 'public' ); 01084 $dstPath = "$publicRoot/$dstRel"; 01085 $archivePath = "$publicRoot/$archiveRel"; 01086 01087 $dstDir = dirname( $dstPath ); 01088 $archiveDir = dirname( $archivePath ); 01089 // Abort immediately on directory creation errors since they're likely to be repetitive 01090 if ( !$this->initDirectory( $dstDir )->isOK() ) { 01091 return $this->newFatal( 'directorycreateerror', $dstDir ); 01092 } 01093 if ( !$this->initDirectory($archiveDir )->isOK() ) { 01094 return $this->newFatal( 'directorycreateerror', $archiveDir ); 01095 } 01096 01097 // Archive destination file if it exists 01098 if ( $backend->fileExists( array( 'src' => $dstPath ) ) ) { 01099 // Check if the archive file exists 01100 // This is a sanity check to avoid data loss. In UNIX, the rename primitive 01101 // unlinks the destination file if it exists. DB-based synchronisation in 01102 // publishBatch's caller should prevent races. In Windows there's no 01103 // problem because the rename primitive fails if the destination exists. 01104 if ( $backend->fileExists( array( 'src' => $archivePath ) ) ) { 01105 $operations[] = array( 'op' => 'null' ); 01106 continue; 01107 } else { 01108 $operations[] = array( 01109 'op' => 'move', 01110 'src' => $dstPath, 01111 'dst' => $archivePath 01112 ); 01113 } 01114 $status->value[$i] = 'archived'; 01115 } else { 01116 $status->value[$i] = 'new'; 01117 } 01118 // Copy (or move) the source file to the destination 01119 if ( FileBackend::isStoragePath( $srcPath ) ) { 01120 if ( $flags & self::DELETE_SOURCE ) { 01121 $operations[] = array( 01122 'op' => 'move', 01123 'src' => $srcPath, 01124 'dst' => $dstPath 01125 ); 01126 } else { 01127 $operations[] = array( 01128 'op' => 'copy', 01129 'src' => $srcPath, 01130 'dst' => $dstPath 01131 ); 01132 } 01133 } else { // FS source path 01134 $operations[] = array( 01135 'op' => 'store', 01136 'src' => $srcPath, 01137 'dst' => $dstPath 01138 ); 01139 if ( $flags & self::DELETE_SOURCE ) { 01140 $sourceFSFilesToDelete[] = $srcPath; 01141 } 01142 } 01143 } 01144 01145 // Execute the operations for each triplet 01146 $opts = array( 'force' => true ); 01147 $status->merge( $backend->doOperations( $operations, $opts ) ); 01148 // Cleanup for disk source files... 01149 foreach ( $sourceFSFilesToDelete as $file ) { 01150 wfSuppressWarnings(); 01151 unlink( $file ); // FS cleanup 01152 wfRestoreWarnings(); 01153 } 01154 01155 return $status; 01156 } 01157 01165 protected function initDirectory( $dir ) { 01166 $path = $this->resolveToStoragePath( $dir ); 01167 list( $b, $container, $r ) = FileBackend::splitStoragePath( $path ); 01168 01169 $params = array( 'dir' => $path ); 01170 if ( $this->isPrivate || $container === $this->zones['deleted']['container'] ) { 01171 # Take all available measures to prevent web accessibility of new deleted 01172 # directories, in case the user has not configured offline storage 01173 $params = array( 'noAccess' => true, 'noListing' => true ) + $params; 01174 } 01175 01176 return $this->backend->prepare( $params ); 01177 } 01178 01185 public function cleanDir( $dir ) { 01186 $this->assertWritableRepo(); // fail out if read-only 01187 01188 $status = $this->newGood(); 01189 $status->merge( $this->backend->clean( 01190 array( 'dir' => $this->resolveToStoragePath( $dir ) ) ) ); 01191 01192 return $status; 01193 } 01194 01201 public function fileExists( $file ) { 01202 $result = $this->fileExistsBatch( array( $file ) ); 01203 return $result[0]; 01204 } 01205 01212 public function fileExistsBatch( array $files ) { 01213 $result = array(); 01214 foreach ( $files as $key => $file ) { 01215 $file = $this->resolveToStoragePath( $file ); 01216 $result[$key] = $this->backend->fileExists( array( 'src' => $file ) ); 01217 } 01218 return $result; 01219 } 01220 01231 public function delete( $srcRel, $archiveRel ) { 01232 $this->assertWritableRepo(); // fail out if read-only 01233 01234 return $this->deleteBatch( array( array( $srcRel, $archiveRel ) ) ); 01235 } 01236 01254 public function deleteBatch( array $sourceDestPairs ) { 01255 $this->assertWritableRepo(); // fail out if read-only 01256 01257 // Try creating directories 01258 $status = $this->initZones( array( 'public', 'deleted' ) ); 01259 if ( !$status->isOK() ) { 01260 return $status; 01261 } 01262 01263 $status = $this->newGood(); 01264 01265 $backend = $this->backend; // convenience 01266 $operations = array(); 01267 // Validate filenames and create archive directories 01268 foreach ( $sourceDestPairs as $pair ) { 01269 list( $srcRel, $archiveRel ) = $pair; 01270 if ( !$this->validateFilename( $srcRel ) ) { 01271 throw new MWException( __METHOD__.':Validation error in $srcRel' ); 01272 } elseif ( !$this->validateFilename( $archiveRel ) ) { 01273 throw new MWException( __METHOD__.':Validation error in $archiveRel' ); 01274 } 01275 01276 $publicRoot = $this->getZonePath( 'public' ); 01277 $srcPath = "{$publicRoot}/$srcRel"; 01278 01279 $deletedRoot = $this->getZonePath( 'deleted' ); 01280 $archivePath = "{$deletedRoot}/{$archiveRel}"; 01281 $archiveDir = dirname( $archivePath ); // does not touch FS 01282 01283 // Create destination directories 01284 if ( !$this->initDirectory( $archiveDir )->isOK() ) { 01285 return $this->newFatal( 'directorycreateerror', $archiveDir ); 01286 } 01287 01288 $operations[] = array( 01289 'op' => 'move', 01290 'src' => $srcPath, 01291 'dst' => $archivePath, 01292 // We may have 2+ identical files being deleted, 01293 // all of which will map to the same destination file 01294 'overwriteSame' => true // also see bug 31792 01295 ); 01296 } 01297 01298 // Move the files by execute the operations for each pair. 01299 // We're now committed to returning an OK result, which will 01300 // lead to the files being moved in the DB also. 01301 $opts = array( 'force' => true ); 01302 $status->merge( $backend->doOperations( $operations, $opts ) ); 01303 01304 return $status; 01305 } 01306 01312 public function cleanupDeletedBatch( array $storageKeys ) { 01313 $this->assertWritableRepo(); 01314 } 01315 01323 public function getDeletedHashPath( $key ) { 01324 $path = ''; 01325 for ( $i = 0; $i < $this->deletedHashLevels; $i++ ) { 01326 $path .= $key[$i] . '/'; 01327 } 01328 return $path; 01329 } 01330 01339 protected function resolveToStoragePath( $path ) { 01340 if ( $this->isVirtualUrl( $path ) ) { 01341 return $this->resolveVirtualUrl( $path ); 01342 } 01343 return $path; 01344 } 01345 01353 public function getLocalCopy( $virtualUrl ) { 01354 $path = $this->resolveToStoragePath( $virtualUrl ); 01355 return $this->backend->getLocalCopy( array( 'src' => $path ) ); 01356 } 01357 01366 public function getLocalReference( $virtualUrl ) { 01367 $path = $this->resolveToStoragePath( $virtualUrl ); 01368 return $this->backend->getLocalReference( array( 'src' => $path ) ); 01369 } 01370 01378 public function getFileProps( $virtualUrl ) { 01379 $path = $this->resolveToStoragePath( $virtualUrl ); 01380 return $this->backend->getFileProps( array( 'src' => $path ) ); 01381 } 01382 01389 public function getFileTimestamp( $virtualUrl ) { 01390 $path = $this->resolveToStoragePath( $virtualUrl ); 01391 return $this->backend->getFileTimestamp( array( 'src' => $path ) ); 01392 } 01393 01400 public function getFileSha1( $virtualUrl ) { 01401 $path = $this->resolveToStoragePath( $virtualUrl ); 01402 return $this->backend->getFileSha1Base36( array( 'src' => $path ) ); 01403 } 01404 01412 public function streamFile( $virtualUrl, $headers = array() ) { 01413 $path = $this->resolveToStoragePath( $virtualUrl ); 01414 $params = array( 'src' => $path, 'headers' => $headers ); 01415 return $this->backend->streamFile( $params )->isOK(); 01416 } 01417 01426 public function enumFiles( $callback ) { 01427 $this->enumFilesInStorage( $callback ); 01428 } 01429 01437 protected function enumFilesInStorage( $callback ) { 01438 $publicRoot = $this->getZonePath( 'public' ); 01439 $numDirs = 1 << ( $this->hashLevels * 4 ); 01440 // Use a priori assumptions about directory structure 01441 // to reduce the tree height of the scanning process. 01442 for ( $flatIndex = 0; $flatIndex < $numDirs; $flatIndex++ ) { 01443 $hexString = sprintf( "%0{$this->hashLevels}x", $flatIndex ); 01444 $path = $publicRoot; 01445 for ( $hexPos = 0; $hexPos < $this->hashLevels; $hexPos++ ) { 01446 $path .= '/' . substr( $hexString, 0, $hexPos + 1 ); 01447 } 01448 $iterator = $this->backend->getFileList( array( 'dir' => $path ) ); 01449 foreach ( $iterator as $name ) { 01450 // Each item returned is a public file 01451 call_user_func( $callback, "{$path}/{$name}" ); 01452 } 01453 } 01454 } 01455 01462 public function validateFilename( $filename ) { 01463 if ( strval( $filename ) == '' ) { 01464 return false; 01465 } 01466 return FileBackend::isPathTraversalFree( $filename ); 01467 } 01468 01474 function getErrorCleanupFunction() { 01475 switch ( $this->pathDisclosureProtection ) { 01476 case 'none': 01477 case 'simple': // b/c 01478 $callback = array( $this, 'passThrough' ); 01479 break; 01480 default: // 'paranoid' 01481 $callback = array( $this, 'paranoidClean' ); 01482 } 01483 return $callback; 01484 } 01485 01492 function paranoidClean( $param ) { 01493 return '[hidden]'; 01494 } 01495 01502 function passThrough( $param ) { 01503 return $param; 01504 } 01505 01511 public function newFatal( $message /*, parameters...*/ ) { 01512 $params = func_get_args(); 01513 array_unshift( $params, $this ); 01514 return MWInit::callStaticMethod( 'FileRepoStatus', 'newFatal', $params ); 01515 } 01516 01523 public function newGood( $value = null ) { 01524 return FileRepoStatus::newGood( $this, $value ); 01525 } 01526 01535 public function checkRedirect( Title $title ) { 01536 return false; 01537 } 01538 01546 public function invalidateImageRedirect( Title $title ) {} 01547 01553 public function getDisplayName() { 01554 // We don't name our own repo, return nothing 01555 if ( $this->isLocal() ) { 01556 return null; 01557 } 01558 // 'shared-repo-name-wikimediacommons' is used when $wgUseInstantCommons = true 01559 return wfMessageFallback( 'shared-repo-name-' . $this->name, 'shared-repo' )->text(); 01560 } 01561 01569 public function nameForThumb( $name ) { 01570 if ( strlen( $name ) > $this->abbrvThreshold ) { 01571 $ext = FileBackend::extensionFromPath( $name ); 01572 $name = ( $ext == '' ) ? 'thumbnail' : "thumbnail.$ext"; 01573 } 01574 return $name; 01575 } 01576 01582 public function isLocal() { 01583 return $this->getName() == 'local'; 01584 } 01585 01594 public function getSharedCacheKey( /*...*/ ) { 01595 return false; 01596 } 01597 01605 public function getLocalCacheKey( /*...*/ ) { 01606 $args = func_get_args(); 01607 array_unshift( $args, 'filerepo', $this->getName() ); 01608 return call_user_func_array( 'wfMemcKey', $args ); 01609 } 01610 01619 public function getTempRepo() { 01620 return new TempFileRepo( array( 01621 'name' => "{$this->name}-temp", 01622 'backend' => $this->backend, 01623 'zones' => array( 01624 'public' => array( 01625 'container' => $this->zones['temp']['container'], 01626 'directory' => $this->zones['temp']['directory'] 01627 ), 01628 'thumb' => array( 01629 'container' => $this->zones['thumb']['container'], 01630 'directory' => ( $this->zones['thumb']['directory'] == '' ) 01631 ? 'temp' 01632 : $this->zones['thumb']['directory'] . '/temp' 01633 ) 01634 ), 01635 'url' => $this->getZoneUrl( 'temp' ), 01636 'thumbUrl' => $this->getZoneUrl( 'thumb' ) . '/temp', 01637 'hashLevels' => $this->hashLevels // performance 01638 ) ); 01639 } 01640 01646 public function getUploadStash() { 01647 return new UploadStash( $this ); 01648 } 01649 01657 protected function assertWritableRepo() {} 01658 } 01659 01663 class TempFileRepo extends FileRepo { 01664 public function getTempRepo() { 01665 throw new MWException( "Cannot get a temp repo from a temp repo." ); 01666 } 01667 }