MediaWiki
REL1_19
|
00001 <?php 00022 class FileRepo { 00023 const FILES_ONLY = 1; 00024 00025 const DELETE_SOURCE = 1; 00026 const OVERWRITE = 2; 00027 const OVERWRITE_SAME = 4; 00028 const SKIP_LOCKING = 8; 00029 00031 protected $backend; 00033 protected $zones = array(); 00034 00035 var $thumbScriptUrl, $transformVia404; 00036 var $descBaseUrl, $scriptDirUrl, $scriptExtension, $articleUrl; 00037 var $fetchDescription, $initialCapital; 00038 var $pathDisclosureProtection = 'simple'; // 'paranoid' 00039 var $descriptionCacheExpiry, $url, $thumbUrl; 00040 var $hashLevels, $deletedHashLevels; 00041 00046 var $fileFactory = array( 'UnregisteredLocalFile', 'newFromTitle' ); 00047 var $oldFileFactory = false; 00048 var $fileFactoryKey = false, $oldFileFactoryKey = false; 00049 00050 function __construct( Array $info = null ) { 00051 // Verify required settings presence 00052 if( 00053 $info === null 00054 || !array_key_exists( 'name', $info ) 00055 || !array_key_exists( 'backend', $info ) 00056 ) { 00057 throw new MWException( __CLASS__ . " requires an array of options having both 'name' and 'backend' keys.\n" ); 00058 } 00059 00060 // Required settings 00061 $this->name = $info['name']; 00062 if ( $info['backend'] instanceof FileBackend ) { 00063 $this->backend = $info['backend']; // useful for testing 00064 } else { 00065 $this->backend = FileBackendGroup::singleton()->get( $info['backend'] ); 00066 } 00067 00068 // Optional settings that can have no value 00069 $optionalSettings = array( 00070 'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription', 00071 'thumbScriptUrl', 'pathDisclosureProtection', 'descriptionCacheExpiry', 00072 'scriptExtension' 00073 ); 00074 foreach ( $optionalSettings as $var ) { 00075 if ( isset( $info[$var] ) ) { 00076 $this->$var = $info[$var]; 00077 } 00078 } 00079 00080 // Optional settings that have a default 00081 $this->initialCapital = isset( $info['initialCapital'] ) 00082 ? $info['initialCapital'] 00083 : MWNamespace::isCapitalized( NS_FILE ); 00084 $this->url = isset( $info['url'] ) 00085 ? $info['url'] 00086 : false; // a subclass may set the URL (e.g. ForeignAPIRepo) 00087 if ( isset( $info['thumbUrl'] ) ) { 00088 $this->thumbUrl = $info['thumbUrl']; 00089 } else { 00090 $this->thumbUrl = $this->url ? "{$this->url}/thumb" : false; 00091 } 00092 $this->hashLevels = isset( $info['hashLevels'] ) 00093 ? $info['hashLevels'] 00094 : 2; 00095 $this->deletedHashLevels = isset( $info['deletedHashLevels'] ) 00096 ? $info['deletedHashLevels'] 00097 : $this->hashLevels; 00098 $this->transformVia404 = !empty( $info['transformVia404'] ); 00099 $this->zones = isset( $info['zones'] ) 00100 ? $info['zones'] 00101 : array(); 00102 // Give defaults for the basic zones... 00103 foreach ( array( 'public', 'thumb', 'temp', 'deleted' ) as $zone ) { 00104 if ( !isset( $this->zones[$zone] ) ) { 00105 $this->zones[$zone] = array( 00106 'container' => "{$this->name}-{$zone}", 00107 'directory' => '' // container root 00108 ); 00109 } 00110 } 00111 } 00112 00118 public function getBackend() { 00119 return $this->backend; 00120 } 00121 00129 protected function initZones( $doZones = array() ) { 00130 $status = $this->newGood(); 00131 foreach ( (array)$doZones as $zone ) { 00132 $root = $this->getZonePath( $zone ); 00133 if ( $root === null ) { 00134 throw new MWException( "No '$zone' zone defined in the {$this->name} repo." ); 00135 } 00136 } 00137 return $status; 00138 } 00139 00147 protected function initDeletedDir( $dir ) { 00148 $this->backend->secure( // prevent web access & dir listings 00149 array( 'dir' => $dir, 'noAccess' => true, 'noListing' => true ) ); 00150 } 00151 00158 public static function isVirtualUrl( $url ) { 00159 return substr( $url, 0, 9 ) == 'mwrepo://'; 00160 } 00161 00170 public function getVirtualUrl( $suffix = false ) { 00171 $path = 'mwrepo://' . $this->name; 00172 if ( $suffix !== false ) { 00173 $path .= '/' . rawurlencode( $suffix ); 00174 } 00175 return $path; 00176 } 00177 00184 public function getZoneUrl( $zone ) { 00185 switch ( $zone ) { 00186 case 'public': 00187 return $this->url; 00188 case 'temp': 00189 return "{$this->url}/temp"; 00190 case 'deleted': 00191 return false; // no public URL 00192 case 'thumb': 00193 return $this->thumbUrl; 00194 default: 00195 return false; 00196 } 00197 } 00198 00205 function resolveVirtualUrl( $url ) { 00206 if ( substr( $url, 0, 9 ) != 'mwrepo://' ) { 00207 throw new MWException( __METHOD__.': unknown protocol' ); 00208 } 00209 $bits = explode( '/', substr( $url, 9 ), 3 ); 00210 if ( count( $bits ) != 3 ) { 00211 throw new MWException( __METHOD__.": invalid mwrepo URL: $url" ); 00212 } 00213 list( $repo, $zone, $rel ) = $bits; 00214 if ( $repo !== $this->name ) { 00215 throw new MWException( __METHOD__.": fetching from a foreign repo is not supported" ); 00216 } 00217 $base = $this->getZonePath( $zone ); 00218 if ( !$base ) { 00219 throw new MWException( __METHOD__.": invalid zone: $zone" ); 00220 } 00221 return $base . '/' . rawurldecode( $rel ); 00222 } 00223 00230 protected function getZoneLocation( $zone ) { 00231 if ( !isset( $this->zones[$zone] ) ) { 00232 return array( null, null ); // bogus 00233 } 00234 return array( $this->zones[$zone]['container'], $this->zones[$zone]['directory'] ); 00235 } 00236 00243 public function getZonePath( $zone ) { 00244 list( $container, $base ) = $this->getZoneLocation( $zone ); 00245 if ( $container === null || $base === null ) { 00246 return null; 00247 } 00248 $backendName = $this->backend->getName(); 00249 if ( $base != '' ) { // may not be set 00250 $base = "/{$base}"; 00251 } 00252 return "mwstore://$backendName/{$container}{$base}"; 00253 } 00254 00266 public function newFile( $title, $time = false ) { 00267 $title = File::normalizeTitle( $title ); 00268 if ( !$title ) { 00269 return null; 00270 } 00271 if ( $time ) { 00272 if ( $this->oldFileFactory ) { 00273 return call_user_func( $this->oldFileFactory, $title, $this, $time ); 00274 } else { 00275 return false; 00276 } 00277 } else { 00278 return call_user_func( $this->fileFactory, $title, $this ); 00279 } 00280 } 00281 00300 public function findFile( $title, $options = array() ) { 00301 $title = File::normalizeTitle( $title ); 00302 if ( !$title ) { 00303 return false; 00304 } 00305 $time = isset( $options['time'] ) ? $options['time'] : false; 00306 # First try the current version of the file to see if it precedes the timestamp 00307 $img = $this->newFile( $title ); 00308 if ( !$img ) { 00309 return false; 00310 } 00311 if ( $img->exists() && ( !$time || $img->getTimestamp() == $time ) ) { 00312 return $img; 00313 } 00314 # Now try an old version of the file 00315 if ( $time !== false ) { 00316 $img = $this->newFile( $title, $time ); 00317 if ( $img && $img->exists() ) { 00318 if ( !$img->isDeleted( File::DELETED_FILE ) ) { 00319 return $img; // always OK 00320 } elseif ( !empty( $options['private'] ) && $img->userCan( File::DELETED_FILE ) ) { 00321 return $img; 00322 } 00323 } 00324 } 00325 00326 # Now try redirects 00327 if ( !empty( $options['ignoreRedirect'] ) ) { 00328 return false; 00329 } 00330 $redir = $this->checkRedirect( $title ); 00331 if ( $redir && $title->getNamespace() == NS_FILE) { 00332 $img = $this->newFile( $redir ); 00333 if ( !$img ) { 00334 return false; 00335 } 00336 if ( $img->exists() ) { 00337 $img->redirectedFrom( $title->getDBkey() ); 00338 return $img; 00339 } 00340 } 00341 return false; 00342 } 00343 00355 public function findFiles( $items ) { 00356 $result = array(); 00357 foreach ( $items as $item ) { 00358 if ( is_array( $item ) ) { 00359 $title = $item['title']; 00360 $options = $item; 00361 unset( $options['title'] ); 00362 } else { 00363 $title = $item; 00364 $options = array(); 00365 } 00366 $file = $this->findFile( $title, $options ); 00367 if ( $file ) { 00368 $result[$file->getTitle()->getDBkey()] = $file; 00369 } 00370 } 00371 return $result; 00372 } 00373 00383 public function findFileFromKey( $sha1, $options = array() ) { 00384 $time = isset( $options['time'] ) ? $options['time'] : false; 00385 00386 # First try to find a matching current version of a file... 00387 if ( $this->fileFactoryKey ) { 00388 $img = call_user_func( $this->fileFactoryKey, $sha1, $this, $time ); 00389 } else { 00390 return false; // find-by-sha1 not supported 00391 } 00392 if ( $img && $img->exists() ) { 00393 return $img; 00394 } 00395 # Now try to find a matching old version of a file... 00396 if ( $time !== false && $this->oldFileFactoryKey ) { // find-by-sha1 supported? 00397 $img = call_user_func( $this->oldFileFactoryKey, $sha1, $this, $time ); 00398 if ( $img && $img->exists() ) { 00399 if ( !$img->isDeleted( File::DELETED_FILE ) ) { 00400 return $img; // always OK 00401 } elseif ( !empty( $options['private'] ) && $img->userCan( File::DELETED_FILE ) ) { 00402 return $img; 00403 } 00404 } 00405 } 00406 return false; 00407 } 00408 00415 public function findBySha1( $hash ) { 00416 return array(); 00417 } 00418 00424 public function getRootUrl() { 00425 return $this->url; 00426 } 00427 00433 public function isHashed() { 00434 return (bool)$this->hashLevels; 00435 } 00436 00442 public function getThumbScriptUrl() { 00443 return $this->thumbScriptUrl; 00444 } 00445 00451 public function canTransformVia404() { 00452 return $this->transformVia404; 00453 } 00454 00460 public function getNameFromTitle( Title $title ) { 00461 global $wgContLang; 00462 if ( $this->initialCapital != MWNamespace::isCapitalized( NS_FILE ) ) { 00463 $name = $title->getUserCaseDBKey(); 00464 if ( $this->initialCapital ) { 00465 $name = $wgContLang->ucfirst( $name ); 00466 } 00467 } else { 00468 $name = $title->getDBkey(); 00469 } 00470 return $name; 00471 } 00472 00478 public function getRootDirectory() { 00479 return $this->getZonePath( 'public' ); 00480 } 00481 00489 public function getHashPath( $name ) { 00490 return self::getHashPathForLevel( $name, $this->hashLevels ); 00491 } 00492 00498 static function getHashPathForLevel( $name, $levels ) { 00499 if ( $levels == 0 ) { 00500 return ''; 00501 } else { 00502 $hash = md5( $name ); 00503 $path = ''; 00504 for ( $i = 1; $i <= $levels; $i++ ) { 00505 $path .= substr( $hash, 0, $i ) . '/'; 00506 } 00507 return $path; 00508 } 00509 } 00510 00516 public function getHashLevels() { 00517 return $this->hashLevels; 00518 } 00519 00525 public function getName() { 00526 return $this->name; 00527 } 00528 00536 public function makeUrl( $query = '', $entry = 'index' ) { 00537 if ( isset( $this->scriptDirUrl ) ) { 00538 $ext = isset( $this->scriptExtension ) ? $this->scriptExtension : '.php'; 00539 return wfAppendQuery( "{$this->scriptDirUrl}/{$entry}{$ext}", $query ); 00540 } 00541 return false; 00542 } 00543 00556 public function getDescriptionUrl( $name ) { 00557 $encName = wfUrlencode( $name ); 00558 if ( !is_null( $this->descBaseUrl ) ) { 00559 # "http://example.com/wiki/Image:" 00560 return $this->descBaseUrl . $encName; 00561 } 00562 if ( !is_null( $this->articleUrl ) ) { 00563 # "http://example.com/wiki/$1" 00564 # 00565 # We use "Image:" as the canonical namespace for 00566 # compatibility across all MediaWiki versions. 00567 return str_replace( '$1', 00568 "Image:$encName", $this->articleUrl ); 00569 } 00570 if ( !is_null( $this->scriptDirUrl ) ) { 00571 # "http://example.com/w" 00572 # 00573 # We use "Image:" as the canonical namespace for 00574 # compatibility across all MediaWiki versions, 00575 # and just sort of hope index.php is right. ;) 00576 return $this->makeUrl( "title=Image:$encName" ); 00577 } 00578 return false; 00579 } 00580 00591 public function getDescriptionRenderUrl( $name, $lang = null ) { 00592 $query = 'action=render'; 00593 if ( !is_null( $lang ) ) { 00594 $query .= '&uselang=' . $lang; 00595 } 00596 if ( isset( $this->scriptDirUrl ) ) { 00597 return $this->makeUrl( 00598 'title=' . 00599 wfUrlencode( 'Image:' . $name ) . 00600 "&$query" ); 00601 } else { 00602 $descUrl = $this->getDescriptionUrl( $name ); 00603 if ( $descUrl ) { 00604 return wfAppendQuery( $descUrl, $query ); 00605 } else { 00606 return false; 00607 } 00608 } 00609 } 00610 00616 public function getDescriptionStylesheetUrl() { 00617 if ( isset( $this->scriptDirUrl ) ) { 00618 return $this->makeUrl( 'title=MediaWiki:Filepage.css&' . 00619 wfArrayToCGI( Skin::getDynamicStylesheetQuery() ) ); 00620 } 00621 return false; 00622 } 00623 00638 public function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) { 00639 $status = $this->storeBatch( array( array( $srcPath, $dstZone, $dstRel ) ), $flags ); 00640 if ( $status->successCount == 0 ) { 00641 $status->ok = false; 00642 } 00643 return $status; 00644 } 00645 00658 public function storeBatch( $triplets, $flags = 0 ) { 00659 $backend = $this->backend; // convenience 00660 00661 $status = $this->newGood(); 00662 00663 $operations = array(); 00664 $sourceFSFilesToDelete = array(); // cleanup for disk source files 00665 // Validate each triplet and get the store operation... 00666 foreach ( $triplets as $triplet ) { 00667 list( $srcPath, $dstZone, $dstRel ) = $triplet; 00668 wfDebug( __METHOD__ 00669 . "( \$src='$srcPath', \$dstZone='$dstZone', \$dstRel='$dstRel' )\n" 00670 ); 00671 00672 // Resolve destination path 00673 $root = $this->getZonePath( $dstZone ); 00674 if ( !$root ) { 00675 throw new MWException( "Invalid zone: $dstZone" ); 00676 } 00677 if ( !$this->validateFilename( $dstRel ) ) { 00678 throw new MWException( 'Validation error in $dstRel' ); 00679 } 00680 $dstPath = "$root/$dstRel"; 00681 $dstDir = dirname( $dstPath ); 00682 // Create destination directories for this triplet 00683 if ( !$backend->prepare( array( 'dir' => $dstDir ) )->isOK() ) { 00684 return $this->newFatal( 'directorycreateerror', $dstDir ); 00685 } 00686 00687 if ( $dstZone == 'deleted' ) { 00688 $this->initDeletedDir( $dstDir ); 00689 } 00690 00691 // Resolve source to a storage path if virtual 00692 if ( self::isVirtualUrl( $srcPath ) ) { 00693 $srcPath = $this->resolveVirtualUrl( $srcPath ); 00694 } 00695 00696 // Get the appropriate file operation 00697 if ( FileBackend::isStoragePath( $srcPath ) ) { 00698 $opName = ( $flags & self::DELETE_SOURCE ) ? 'move' : 'copy'; 00699 } else { 00700 $opName = 'store'; 00701 if ( $flags & self::DELETE_SOURCE ) { 00702 $sourceFSFilesToDelete[] = $srcPath; 00703 } 00704 } 00705 $operations[] = array( 00706 'op' => $opName, 00707 'src' => $srcPath, 00708 'dst' => $dstPath, 00709 'overwrite' => $flags & self::OVERWRITE, 00710 'overwriteSame' => $flags & self::OVERWRITE_SAME, 00711 ); 00712 } 00713 00714 // Execute the store operation for each triplet 00715 $opts = array( 'force' => true ); 00716 if ( $flags & self::SKIP_LOCKING ) { 00717 $opts['nonLocking'] = true; 00718 } 00719 $status->merge( $backend->doOperations( $operations, $opts ) ); 00720 // Cleanup for disk source files... 00721 foreach ( $sourceFSFilesToDelete as $file ) { 00722 wfSuppressWarnings(); 00723 unlink( $file ); // FS cleanup 00724 wfRestoreWarnings(); 00725 } 00726 00727 return $status; 00728 } 00729 00740 public function cleanupBatch( $files, $flags = 0 ) { 00741 $operations = array(); 00742 $sourceFSFilesToDelete = array(); // cleanup for disk source files 00743 foreach ( $files as $file ) { 00744 if ( is_array( $file ) ) { 00745 // This is a pair, extract it 00746 list( $zone, $rel ) = $file; 00747 $root = $this->getZonePath( $zone ); 00748 $path = "$root/$rel"; 00749 } else { 00750 if ( self::isVirtualUrl( $file ) ) { 00751 // This is a virtual url, resolve it 00752 $path = $this->resolveVirtualUrl( $file ); 00753 } else { 00754 // This is a full file name 00755 $path = $file; 00756 } 00757 } 00758 // Get a file operation if needed 00759 if ( FileBackend::isStoragePath( $path ) ) { 00760 $operations[] = array( 00761 'op' => 'delete', 00762 'src' => $path, 00763 ); 00764 } else { 00765 $sourceFSFilesToDelete[] = $path; 00766 } 00767 } 00768 // Actually delete files from storage... 00769 $opts = array( 'force' => true ); 00770 if ( $flags & self::SKIP_LOCKING ) { 00771 $opts['nonLocking'] = true; 00772 } 00773 $this->backend->doOperations( $operations, $opts ); 00774 // Cleanup for disk source files... 00775 foreach ( $sourceFSFilesToDelete as $file ) { 00776 wfSuppressWarnings(); 00777 unlink( $file ); // FS cleanup 00778 wfRestoreWarnings(); 00779 } 00780 } 00781 00793 public function storeTemp( $originalName, $srcPath ) { 00794 $date = gmdate( "YmdHis" ); 00795 $hashPath = $this->getHashPath( $originalName ); 00796 $dstRel = "{$hashPath}{$date}!{$originalName}"; 00797 $dstUrlRel = $hashPath . $date . '!' . rawurlencode( $originalName ); 00798 00799 $result = $this->store( $srcPath, 'temp', $dstRel, self::SKIP_LOCKING ); 00800 $result->value = $this->getVirtualUrl( 'temp' ) . '/' . $dstUrlRel; 00801 return $result; 00802 } 00803 00813 function concatenate( $srcPaths, $dstPath, $flags = 0 ) { 00814 $status = $this->newGood(); 00815 00816 $sources = array(); 00817 $deleteOperations = array(); // post-concatenate ops 00818 foreach ( $srcPaths as $srcPath ) { 00819 // Resolve source to a storage path if virtual 00820 $source = $this->resolveToStoragePath( $srcPath ); 00821 $sources[] = $source; // chunk to merge 00822 if ( $flags & self::DELETE_SOURCE ) { 00823 $deleteOperations[] = array( 'op' => 'delete', 'src' => $source ); 00824 } 00825 } 00826 00827 // Concatenate the chunks into one FS file 00828 $params = array( 'srcs' => $sources, 'dst' => $dstPath ); 00829 $status->merge( $this->backend->concatenate( $params ) ); 00830 if ( !$status->isOK() ) { 00831 return $status; 00832 } 00833 00834 // Delete the sources if required 00835 if ( $deleteOperations ) { 00836 $opts = array( 'force' => true ); 00837 $status->merge( $this->backend->doOperations( $deleteOperations, $opts ) ); 00838 } 00839 00840 // Make sure status is OK, despite any $deleteOperations fatals 00841 $status->setResult( true ); 00842 00843 return $status; 00844 } 00845 00852 public function freeTemp( $virtualUrl ) { 00853 $temp = "mwrepo://{$this->name}/temp"; 00854 if ( substr( $virtualUrl, 0, strlen( $temp ) ) != $temp ) { 00855 wfDebug( __METHOD__.": Invalid temp virtual URL\n" ); 00856 return false; 00857 } 00858 $path = $this->resolveVirtualUrl( $virtualUrl ); 00859 $op = array( 'op' => 'delete', 'src' => $path ); 00860 $status = $this->backend->doOperation( $op ); 00861 return $status->isOK(); 00862 } 00863 00878 public function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) { 00879 $status = $this->publishBatch( array( array( $srcPath, $dstRel, $archiveRel ) ), $flags ); 00880 if ( $status->successCount == 0 ) { 00881 $status->ok = false; 00882 } 00883 if ( isset( $status->value[0] ) ) { 00884 $status->value = $status->value[0]; 00885 } else { 00886 $status->value = false; 00887 } 00888 return $status; 00889 } 00890 00899 public function publishBatch( $triplets, $flags = 0 ) { 00900 $backend = $this->backend; // convenience 00901 00902 // Try creating directories 00903 $status = $this->initZones( 'public' ); 00904 if ( !$status->isOK() ) { 00905 return $status; 00906 } 00907 00908 $status = $this->newGood( array() ); 00909 00910 $operations = array(); 00911 $sourceFSFilesToDelete = array(); // cleanup for disk source files 00912 // Validate each triplet and get the store operation... 00913 foreach ( $triplets as $i => $triplet ) { 00914 list( $srcPath, $dstRel, $archiveRel ) = $triplet; 00915 // Resolve source to a storage path if virtual 00916 if ( substr( $srcPath, 0, 9 ) == 'mwrepo://' ) { 00917 $srcPath = $this->resolveVirtualUrl( $srcPath ); 00918 } 00919 if ( !$this->validateFilename( $dstRel ) ) { 00920 throw new MWException( 'Validation error in $dstRel' ); 00921 } 00922 if ( !$this->validateFilename( $archiveRel ) ) { 00923 throw new MWException( 'Validation error in $archiveRel' ); 00924 } 00925 00926 $publicRoot = $this->getZonePath( 'public' ); 00927 $dstPath = "$publicRoot/$dstRel"; 00928 $archivePath = "$publicRoot/$archiveRel"; 00929 00930 $dstDir = dirname( $dstPath ); 00931 $archiveDir = dirname( $archivePath ); 00932 // Abort immediately on directory creation errors since they're likely to be repetitive 00933 if ( !$backend->prepare( array( 'dir' => $dstDir ) )->isOK() ) { 00934 return $this->newFatal( 'directorycreateerror', $dstDir ); 00935 } 00936 if ( !$backend->prepare( array( 'dir' => $archiveDir ) )->isOK() ) { 00937 return $this->newFatal( 'directorycreateerror', $archiveDir ); 00938 } 00939 00940 // Archive destination file if it exists 00941 if ( $backend->fileExists( array( 'src' => $dstPath ) ) ) { 00942 // Check if the archive file exists 00943 // This is a sanity check to avoid data loss. In UNIX, the rename primitive 00944 // unlinks the destination file if it exists. DB-based synchronisation in 00945 // publishBatch's caller should prevent races. In Windows there's no 00946 // problem because the rename primitive fails if the destination exists. 00947 if ( $backend->fileExists( array( 'src' => $archivePath ) ) ) { 00948 $operations[] = array( 'op' => 'null' ); 00949 continue; 00950 } else { 00951 $operations[] = array( 00952 'op' => 'move', 00953 'src' => $dstPath, 00954 'dst' => $archivePath 00955 ); 00956 } 00957 $status->value[$i] = 'archived'; 00958 } else { 00959 $status->value[$i] = 'new'; 00960 } 00961 // Copy (or move) the source file to the destination 00962 if ( FileBackend::isStoragePath( $srcPath ) ) { 00963 if ( $flags & self::DELETE_SOURCE ) { 00964 $operations[] = array( 00965 'op' => 'move', 00966 'src' => $srcPath, 00967 'dst' => $dstPath 00968 ); 00969 } else { 00970 $operations[] = array( 00971 'op' => 'copy', 00972 'src' => $srcPath, 00973 'dst' => $dstPath 00974 ); 00975 } 00976 } else { // FS source path 00977 $operations[] = array( 00978 'op' => 'store', 00979 'src' => $srcPath, 00980 'dst' => $dstPath 00981 ); 00982 if ( $flags & self::DELETE_SOURCE ) { 00983 $sourceFSFilesToDelete[] = $srcPath; 00984 } 00985 } 00986 } 00987 00988 // Execute the operations for each triplet 00989 $opts = array( 'force' => true ); 00990 $status->merge( $backend->doOperations( $operations, $opts ) ); 00991 // Cleanup for disk source files... 00992 foreach ( $sourceFSFilesToDelete as $file ) { 00993 wfSuppressWarnings(); 00994 unlink( $file ); // FS cleanup 00995 wfRestoreWarnings(); 00996 } 00997 00998 return $status; 00999 } 01000 01009 public function fileExists( $file, $flags = 0 ) { 01010 $result = $this->fileExistsBatch( array( $file ), $flags ); 01011 return $result[0]; 01012 } 01013 01022 public function fileExistsBatch( $files, $flags = 0 ) { 01023 $result = array(); 01024 foreach ( $files as $key => $file ) { 01025 if ( self::isVirtualUrl( $file ) ) { 01026 $file = $this->resolveVirtualUrl( $file ); 01027 } 01028 if ( FileBackend::isStoragePath( $file ) ) { 01029 $result[$key] = $this->backend->fileExists( array( 'src' => $file ) ); 01030 } else { 01031 if ( $flags & self::FILES_ONLY ) { 01032 $result[$key] = is_file( $file ); // FS only 01033 } else { 01034 $result[$key] = file_exists( $file ); // FS only 01035 } 01036 } 01037 } 01038 01039 return $result; 01040 } 01041 01052 public function delete( $srcRel, $archiveRel ) { 01053 return $this->deleteBatch( array( array( $srcRel, $archiveRel ) ) ); 01054 } 01055 01072 public function deleteBatch( $sourceDestPairs ) { 01073 $backend = $this->backend; // convenience 01074 01075 // Try creating directories 01076 $status = $this->initZones( array( 'public', 'deleted' ) ); 01077 if ( !$status->isOK() ) { 01078 return $status; 01079 } 01080 01081 $status = $this->newGood(); 01082 01083 $operations = array(); 01084 // Validate filenames and create archive directories 01085 foreach ( $sourceDestPairs as $pair ) { 01086 list( $srcRel, $archiveRel ) = $pair; 01087 if ( !$this->validateFilename( $srcRel ) ) { 01088 throw new MWException( __METHOD__.':Validation error in $srcRel' ); 01089 } 01090 if ( !$this->validateFilename( $archiveRel ) ) { 01091 throw new MWException( __METHOD__.':Validation error in $archiveRel' ); 01092 } 01093 01094 $publicRoot = $this->getZonePath( 'public' ); 01095 $srcPath = "{$publicRoot}/$srcRel"; 01096 01097 $deletedRoot = $this->getZonePath( 'deleted' ); 01098 $archivePath = "{$deletedRoot}/{$archiveRel}"; 01099 $archiveDir = dirname( $archivePath ); // does not touch FS 01100 01101 // Create destination directories 01102 if ( !$backend->prepare( array( 'dir' => $archiveDir ) )->isOK() ) { 01103 return $this->newFatal( 'directorycreateerror', $archiveDir ); 01104 } 01105 $this->initDeletedDir( $archiveDir ); 01106 01107 $operations[] = array( 01108 'op' => 'move', 01109 'src' => $srcPath, 01110 'dst' => $archivePath, 01111 // We may have 2+ identical files being deleted, 01112 // all of which will map to the same destination file 01113 'overwriteSame' => true // also see bug 31792 01114 ); 01115 } 01116 01117 // Move the files by execute the operations for each pair. 01118 // We're now committed to returning an OK result, which will 01119 // lead to the files being moved in the DB also. 01120 $opts = array( 'force' => true ); 01121 $status->merge( $backend->doOperations( $operations, $opts ) ); 01122 01123 return $status; 01124 } 01125 01132 public function getDeletedHashPath( $key ) { 01133 $path = ''; 01134 for ( $i = 0; $i < $this->deletedHashLevels; $i++ ) { 01135 $path .= $key[$i] . '/'; 01136 } 01137 return $path; 01138 } 01139 01148 protected function resolveToStoragePath( $path ) { 01149 if ( $this->isVirtualUrl( $path ) ) { 01150 return $this->resolveVirtualUrl( $path ); 01151 } 01152 return $path; 01153 } 01154 01162 public function getLocalCopy( $virtualUrl ) { 01163 $path = $this->resolveToStoragePath( $virtualUrl ); 01164 return $this->backend->getLocalCopy( array( 'src' => $path ) ); 01165 } 01166 01175 public function getLocalReference( $virtualUrl ) { 01176 $path = $this->resolveToStoragePath( $virtualUrl ); 01177 return $this->backend->getLocalReference( array( 'src' => $path ) ); 01178 } 01179 01187 public function getFileProps( $virtualUrl ) { 01188 $path = $this->resolveToStoragePath( $virtualUrl ); 01189 return $this->backend->getFileProps( array( 'src' => $path ) ); 01190 } 01191 01198 public function getFileTimestamp( $virtualUrl ) { 01199 $path = $this->resolveToStoragePath( $virtualUrl ); 01200 return $this->backend->getFileTimestamp( array( 'src' => $path ) ); 01201 } 01202 01209 public function getFileSha1( $virtualUrl ) { 01210 $path = $this->resolveToStoragePath( $virtualUrl ); 01211 $tmpFile = $this->backend->getLocalReference( array( 'src' => $path ) ); 01212 if ( !$tmpFile ) { 01213 return false; 01214 } 01215 return $tmpFile->getSha1Base36(); 01216 } 01217 01225 public function streamFile( $virtualUrl, $headers = array() ) { 01226 $path = $this->resolveToStoragePath( $virtualUrl ); 01227 $params = array( 'src' => $path, 'headers' => $headers ); 01228 return $this->backend->streamFile( $params )->isOK(); 01229 } 01230 01239 public function enumFiles( $callback ) { 01240 $this->enumFilesInStorage( $callback ); 01241 } 01242 01250 protected function enumFilesInStorage( $callback ) { 01251 $publicRoot = $this->getZonePath( 'public' ); 01252 $numDirs = 1 << ( $this->hashLevels * 4 ); 01253 // Use a priori assumptions about directory structure 01254 // to reduce the tree height of the scanning process. 01255 for ( $flatIndex = 0; $flatIndex < $numDirs; $flatIndex++ ) { 01256 $hexString = sprintf( "%0{$this->hashLevels}x", $flatIndex ); 01257 $path = $publicRoot; 01258 for ( $hexPos = 0; $hexPos < $this->hashLevels; $hexPos++ ) { 01259 $path .= '/' . substr( $hexString, 0, $hexPos + 1 ); 01260 } 01261 $iterator = $this->backend->getFileList( array( 'dir' => $path ) ); 01262 foreach ( $iterator as $name ) { 01263 // Each item returned is a public file 01264 call_user_func( $callback, "{$path}/{$name}" ); 01265 } 01266 } 01267 } 01268 01275 public function validateFilename( $filename ) { 01276 if ( strval( $filename ) == '' ) { 01277 return false; 01278 } 01279 if ( wfIsWindows() ) { 01280 $filename = strtr( $filename, '\\', '/' ); 01281 } 01285 if ( strpos( $filename, '.' ) !== false && 01286 ( $filename === '.' || $filename === '..' || 01287 strpos( $filename, './' ) === 0 || 01288 strpos( $filename, '../' ) === 0 || 01289 strpos( $filename, '/./' ) !== false || 01290 strpos( $filename, '/../' ) !== false ) ) 01291 { 01292 return false; 01293 } else { 01294 return true; 01295 } 01296 } 01297 01303 function getErrorCleanupFunction() { 01304 switch ( $this->pathDisclosureProtection ) { 01305 case 'none': 01306 $callback = array( $this, 'passThrough' ); 01307 break; 01308 case 'simple': 01309 $callback = array( $this, 'simpleClean' ); 01310 break; 01311 default: // 'paranoid' 01312 $callback = array( $this, 'paranoidClean' ); 01313 } 01314 return $callback; 01315 } 01316 01323 function paranoidClean( $param ) { 01324 return '[hidden]'; 01325 } 01326 01333 function simpleClean( $param ) { 01334 global $IP; 01335 if ( !isset( $this->simpleCleanPairs ) ) { 01336 $this->simpleCleanPairs = array( 01337 $IP => '$IP', // sanity 01338 ); 01339 } 01340 return strtr( $param, $this->simpleCleanPairs ); 01341 } 01342 01349 function passThrough( $param ) { 01350 return $param; 01351 } 01352 01358 function newFatal( $message /*, parameters...*/ ) { 01359 $params = func_get_args(); 01360 array_unshift( $params, $this ); 01361 return MWInit::callStaticMethod( 'FileRepoStatus', 'newFatal', $params ); 01362 } 01363 01369 function newGood( $value = null ) { 01370 return FileRepoStatus::newGood( $this, $value ); 01371 } 01372 01378 public function cleanupDeletedBatch( $storageKeys ) {} 01379 01388 public function checkRedirect( Title $title ) { 01389 return false; 01390 } 01391 01399 public function invalidateImageRedirect( Title $title ) {} 01400 01406 public function getDisplayName() { 01407 // We don't name our own repo, return nothing 01408 if ( $this->isLocal() ) { 01409 return null; 01410 } 01411 // 'shared-repo-name-wikimediacommons' is used when $wgUseInstantCommons = true 01412 return wfMessageFallback( 'shared-repo-name-' . $this->name, 'shared-repo' )->text(); 01413 } 01414 01420 public function isLocal() { 01421 return $this->getName() == 'local'; 01422 } 01423 01431 function getSharedCacheKey( /*...*/ ) { 01432 return false; 01433 } 01434 01442 function getLocalCacheKey( /*...*/ ) { 01443 $args = func_get_args(); 01444 array_unshift( $args, 'filerepo', $this->getName() ); 01445 return call_user_func_array( 'wfMemcKey', $args ); 01446 } 01447 01453 public function getUploadStash() { 01454 return new UploadStash( $this ); 01455 } 01456 }