MediaWiki
REL1_22
|
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', 'transcoded', '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 if ( !isset( $this->zones[$zone]['urlsByExt'] ) ) { 00131 $this->zones[$zone]['urlsByExt'] = array(); 00132 } 00133 } 00134 } 00135 00141 public function getBackend() { 00142 return $this->backend; 00143 } 00144 00151 public function getReadOnlyReason() { 00152 return $this->backend->getReadOnlyReason(); 00153 } 00154 00162 protected function initZones( $doZones = array() ) { 00163 $status = $this->newGood(); 00164 foreach ( (array)$doZones as $zone ) { 00165 $root = $this->getZonePath( $zone ); 00166 if ( $root === null ) { 00167 throw new MWException( "No '$zone' zone defined in the {$this->name} repo." ); 00168 } 00169 } 00170 return $status; 00171 } 00172 00179 public static function isVirtualUrl( $url ) { 00180 return substr( $url, 0, 9 ) == 'mwrepo://'; 00181 } 00182 00191 public function getVirtualUrl( $suffix = false ) { 00192 $path = 'mwrepo://' . $this->name; 00193 if ( $suffix !== false ) { 00194 $path .= '/' . rawurlencode( $suffix ); 00195 } 00196 return $path; 00197 } 00198 00206 public function getZoneUrl( $zone, $ext = null ) { 00207 if ( in_array( $zone, array( 'public', 'temp', 'thumb', 'transcoded' ) ) ) { // standard public zones 00208 if ( $ext !== null && isset( $this->zones[$zone]['urlsByExt'][$ext] ) ) { 00209 return $this->zones[$zone]['urlsByExt'][$ext]; // custom URL for extension/zone 00210 } elseif ( isset( $this->zones[$zone]['url'] ) ) { 00211 return $this->zones[$zone]['url']; // custom URL for zone 00212 } 00213 } 00214 switch ( $zone ) { 00215 case 'public': 00216 return $this->url; 00217 case 'temp': 00218 return "{$this->url}/temp"; 00219 case 'deleted': 00220 return false; // no public URL 00221 case 'thumb': 00222 return $this->thumbUrl; 00223 case 'transcoded': 00224 return "{$this->url}/transcoded"; 00225 default: 00226 return false; 00227 } 00228 } 00229 00243 public function getZoneHandlerUrl( $zone ) { 00244 if ( isset( $this->zones[$zone]['handlerUrl'] ) 00245 && in_array( $zone, array( 'public', 'temp', 'thumb', 'transcoded' ) ) ) 00246 { 00247 return $this->zones[$zone]['handlerUrl']; 00248 } 00249 return false; 00250 } 00251 00260 public function resolveVirtualUrl( $url ) { 00261 if ( substr( $url, 0, 9 ) != 'mwrepo://' ) { 00262 throw new MWException( __METHOD__ . ': unknown protocol' ); 00263 } 00264 $bits = explode( '/', substr( $url, 9 ), 3 ); 00265 if ( count( $bits ) != 3 ) { 00266 throw new MWException( __METHOD__ . ": invalid mwrepo URL: $url" ); 00267 } 00268 list( $repo, $zone, $rel ) = $bits; 00269 if ( $repo !== $this->name ) { 00270 throw new MWException( __METHOD__ . ": fetching from a foreign repo is not supported" ); 00271 } 00272 $base = $this->getZonePath( $zone ); 00273 if ( !$base ) { 00274 throw new MWException( __METHOD__ . ": invalid zone: $zone" ); 00275 } 00276 return $base . '/' . rawurldecode( $rel ); 00277 } 00278 00285 protected function getZoneLocation( $zone ) { 00286 if ( !isset( $this->zones[$zone] ) ) { 00287 return array( null, null ); // bogus 00288 } 00289 return array( $this->zones[$zone]['container'], $this->zones[$zone]['directory'] ); 00290 } 00291 00298 public function getZonePath( $zone ) { 00299 list( $container, $base ) = $this->getZoneLocation( $zone ); 00300 if ( $container === null || $base === null ) { 00301 return null; 00302 } 00303 $backendName = $this->backend->getName(); 00304 if ( $base != '' ) { // may not be set 00305 $base = "/{$base}"; 00306 } 00307 return "mwstore://$backendName/{$container}{$base}"; 00308 } 00309 00321 public function newFile( $title, $time = false ) { 00322 $title = File::normalizeTitle( $title ); 00323 if ( !$title ) { 00324 return null; 00325 } 00326 if ( $time ) { 00327 if ( $this->oldFileFactory ) { 00328 return call_user_func( $this->oldFileFactory, $title, $this, $time ); 00329 } else { 00330 return false; 00331 } 00332 } else { 00333 return call_user_func( $this->fileFactory, $title, $this ); 00334 } 00335 } 00336 00355 public function findFile( $title, $options = array() ) { 00356 $title = File::normalizeTitle( $title ); 00357 if ( !$title ) { 00358 return false; 00359 } 00360 $time = isset( $options['time'] ) ? $options['time'] : false; 00361 # First try the current version of the file to see if it precedes the timestamp 00362 $img = $this->newFile( $title ); 00363 if ( !$img ) { 00364 return false; 00365 } 00366 if ( $img->exists() && ( !$time || $img->getTimestamp() == $time ) ) { 00367 return $img; 00368 } 00369 # Now try an old version of the file 00370 if ( $time !== false ) { 00371 $img = $this->newFile( $title, $time ); 00372 if ( $img && $img->exists() ) { 00373 if ( !$img->isDeleted( File::DELETED_FILE ) ) { 00374 return $img; // always OK 00375 } elseif ( !empty( $options['private'] ) && $img->userCan( File::DELETED_FILE ) ) { 00376 return $img; 00377 } 00378 } 00379 } 00380 00381 # Now try redirects 00382 if ( !empty( $options['ignoreRedirect'] ) ) { 00383 return false; 00384 } 00385 $redir = $this->checkRedirect( $title ); 00386 if ( $redir && $title->getNamespace() == NS_FILE ) { 00387 $img = $this->newFile( $redir ); 00388 if ( !$img ) { 00389 return false; 00390 } 00391 if ( $img->exists() ) { 00392 $img->redirectedFrom( $title->getDBkey() ); 00393 return $img; 00394 } 00395 } 00396 return false; 00397 } 00398 00410 public function findFiles( array $items ) { 00411 $result = array(); 00412 foreach ( $items as $item ) { 00413 if ( is_array( $item ) ) { 00414 $title = $item['title']; 00415 $options = $item; 00416 unset( $options['title'] ); 00417 } else { 00418 $title = $item; 00419 $options = array(); 00420 } 00421 $file = $this->findFile( $title, $options ); 00422 if ( $file ) { 00423 $result[$file->getTitle()->getDBkey()] = $file; 00424 } 00425 } 00426 return $result; 00427 } 00428 00438 public function findFileFromKey( $sha1, $options = array() ) { 00439 $time = isset( $options['time'] ) ? $options['time'] : false; 00440 # First try to find a matching current version of a file... 00441 if ( $this->fileFactoryKey ) { 00442 $img = call_user_func( $this->fileFactoryKey, $sha1, $this, $time ); 00443 } else { 00444 return false; // find-by-sha1 not supported 00445 } 00446 if ( $img && $img->exists() ) { 00447 return $img; 00448 } 00449 # Now try to find a matching old version of a file... 00450 if ( $time !== false && $this->oldFileFactoryKey ) { // find-by-sha1 supported? 00451 $img = call_user_func( $this->oldFileFactoryKey, $sha1, $this, $time ); 00452 if ( $img && $img->exists() ) { 00453 if ( !$img->isDeleted( File::DELETED_FILE ) ) { 00454 return $img; // always OK 00455 } elseif ( !empty( $options['private'] ) && $img->userCan( File::DELETED_FILE ) ) { 00456 return $img; 00457 } 00458 } 00459 } 00460 return false; 00461 } 00462 00471 public function findBySha1( $hash ) { 00472 return array(); 00473 } 00474 00482 public function findBySha1s( array $hashes ) { 00483 $result = array(); 00484 foreach ( $hashes as $hash ) { 00485 $files = $this->findBySha1( $hash ); 00486 if ( count( $files ) ) { 00487 $result[$hash] = $files; 00488 } 00489 } 00490 return $result; 00491 } 00492 00501 public function findFilesByPrefix( $prefix, $limit ) { 00502 return array(); 00503 } 00504 00511 public function getRootUrl() { 00512 return $this->getZoneUrl( 'public' ); 00513 } 00514 00520 public function getThumbScriptUrl() { 00521 return $this->thumbScriptUrl; 00522 } 00523 00529 public function canTransformVia404() { 00530 return $this->transformVia404; 00531 } 00532 00539 public function getNameFromTitle( Title $title ) { 00540 global $wgContLang; 00541 if ( $this->initialCapital != MWNamespace::isCapitalized( NS_FILE ) ) { 00542 $name = $title->getUserCaseDBKey(); 00543 if ( $this->initialCapital ) { 00544 $name = $wgContLang->ucfirst( $name ); 00545 } 00546 } else { 00547 $name = $title->getDBkey(); 00548 } 00549 return $name; 00550 } 00551 00557 public function getRootDirectory() { 00558 return $this->getZonePath( 'public' ); 00559 } 00560 00568 public function getHashPath( $name ) { 00569 return self::getHashPathForLevel( $name, $this->hashLevels ); 00570 } 00571 00579 public function getTempHashPath( $suffix ) { 00580 $parts = explode( '!', $suffix, 2 ); // format is <timestamp>!<name> or just <name> 00581 $name = isset( $parts[1] ) ? $parts[1] : $suffix; // hash path is not based on timestamp 00582 return self::getHashPathForLevel( $name, $this->hashLevels ); 00583 } 00584 00590 protected static function getHashPathForLevel( $name, $levels ) { 00591 if ( $levels == 0 ) { 00592 return ''; 00593 } else { 00594 $hash = md5( $name ); 00595 $path = ''; 00596 for ( $i = 1; $i <= $levels; $i++ ) { 00597 $path .= substr( $hash, 0, $i ) . '/'; 00598 } 00599 return $path; 00600 } 00601 } 00602 00608 public function getHashLevels() { 00609 return $this->hashLevels; 00610 } 00611 00617 public function getName() { 00618 return $this->name; 00619 } 00620 00628 public function makeUrl( $query = '', $entry = 'index' ) { 00629 if ( isset( $this->scriptDirUrl ) ) { 00630 $ext = isset( $this->scriptExtension ) ? $this->scriptExtension : '.php'; 00631 return wfAppendQuery( "{$this->scriptDirUrl}/{$entry}{$ext}", $query ); 00632 } 00633 return false; 00634 } 00635 00648 public function getDescriptionUrl( $name ) { 00649 $encName = wfUrlencode( $name ); 00650 if ( !is_null( $this->descBaseUrl ) ) { 00651 # "http://example.com/wiki/Image:" 00652 return $this->descBaseUrl . $encName; 00653 } 00654 if ( !is_null( $this->articleUrl ) ) { 00655 # "http://example.com/wiki/$1" 00656 # 00657 # We use "Image:" as the canonical namespace for 00658 # compatibility across all MediaWiki versions. 00659 return str_replace( '$1', 00660 "Image:$encName", $this->articleUrl ); 00661 } 00662 if ( !is_null( $this->scriptDirUrl ) ) { 00663 # "http://example.com/w" 00664 # 00665 # We use "Image:" as the canonical namespace for 00666 # compatibility across all MediaWiki versions, 00667 # and just sort of hope index.php is right. ;) 00668 return $this->makeUrl( "title=Image:$encName" ); 00669 } 00670 return false; 00671 } 00672 00683 public function getDescriptionRenderUrl( $name, $lang = null ) { 00684 $query = 'action=render'; 00685 if ( !is_null( $lang ) ) { 00686 $query .= '&uselang=' . $lang; 00687 } 00688 if ( isset( $this->scriptDirUrl ) ) { 00689 return $this->makeUrl( 00690 'title=' . 00691 wfUrlencode( 'Image:' . $name ) . 00692 "&$query" ); 00693 } else { 00694 $descUrl = $this->getDescriptionUrl( $name ); 00695 if ( $descUrl ) { 00696 return wfAppendQuery( $descUrl, $query ); 00697 } else { 00698 return false; 00699 } 00700 } 00701 } 00702 00708 public function getDescriptionStylesheetUrl() { 00709 if ( isset( $this->scriptDirUrl ) ) { 00710 return $this->makeUrl( 'title=MediaWiki:Filepage.css&' . 00711 wfArrayToCgi( Skin::getDynamicStylesheetQuery() ) ); 00712 } 00713 return false; 00714 } 00715 00730 public function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) { 00731 $this->assertWritableRepo(); // fail out if read-only 00732 00733 $status = $this->storeBatch( array( array( $srcPath, $dstZone, $dstRel ) ), $flags ); 00734 if ( $status->successCount == 0 ) { 00735 $status->ok = false; 00736 } 00737 00738 return $status; 00739 } 00740 00754 public function storeBatch( array $triplets, $flags = 0 ) { 00755 $this->assertWritableRepo(); // fail out if read-only 00756 00757 $status = $this->newGood(); 00758 $backend = $this->backend; // convenience 00759 00760 $operations = array(); 00761 $sourceFSFilesToDelete = array(); // cleanup for disk source files 00762 // Validate each triplet and get the store operation... 00763 foreach ( $triplets as $triplet ) { 00764 list( $srcPath, $dstZone, $dstRel ) = $triplet; 00765 wfDebug( __METHOD__ 00766 . "( \$src='$srcPath', \$dstZone='$dstZone', \$dstRel='$dstRel' )\n" 00767 ); 00768 00769 // Resolve destination path 00770 $root = $this->getZonePath( $dstZone ); 00771 if ( !$root ) { 00772 throw new MWException( "Invalid zone: $dstZone" ); 00773 } 00774 if ( !$this->validateFilename( $dstRel ) ) { 00775 throw new MWException( 'Validation error in $dstRel' ); 00776 } 00777 $dstPath = "$root/$dstRel"; 00778 $dstDir = dirname( $dstPath ); 00779 // Create destination directories for this triplet 00780 if ( !$this->initDirectory( $dstDir )->isOK() ) { 00781 return $this->newFatal( 'directorycreateerror', $dstDir ); 00782 } 00783 00784 // Resolve source to a storage path if virtual 00785 $srcPath = $this->resolveToStoragePath( $srcPath ); 00786 00787 // Get the appropriate file operation 00788 if ( FileBackend::isStoragePath( $srcPath ) ) { 00789 $opName = ( $flags & self::DELETE_SOURCE ) ? 'move' : 'copy'; 00790 } else { 00791 $opName = 'store'; 00792 if ( $flags & self::DELETE_SOURCE ) { 00793 $sourceFSFilesToDelete[] = $srcPath; 00794 } 00795 } 00796 $operations[] = array( 00797 'op' => $opName, 00798 'src' => $srcPath, 00799 'dst' => $dstPath, 00800 'overwrite' => $flags & self::OVERWRITE, 00801 'overwriteSame' => $flags & self::OVERWRITE_SAME, 00802 ); 00803 } 00804 00805 // Execute the store operation for each triplet 00806 $opts = array( 'force' => true ); 00807 if ( $flags & self::SKIP_LOCKING ) { 00808 $opts['nonLocking'] = true; 00809 } 00810 $status->merge( $backend->doOperations( $operations, $opts ) ); 00811 // Cleanup for disk source files... 00812 foreach ( $sourceFSFilesToDelete as $file ) { 00813 wfSuppressWarnings(); 00814 unlink( $file ); // FS cleanup 00815 wfRestoreWarnings(); 00816 } 00817 00818 return $status; 00819 } 00820 00831 public function cleanupBatch( array $files, $flags = 0 ) { 00832 $this->assertWritableRepo(); // fail out if read-only 00833 00834 $status = $this->newGood(); 00835 00836 $operations = array(); 00837 foreach ( $files as $path ) { 00838 if ( is_array( $path ) ) { 00839 // This is a pair, extract it 00840 list( $zone, $rel ) = $path; 00841 $path = $this->getZonePath( $zone ) . "/$rel"; 00842 } else { 00843 // Resolve source to a storage path if virtual 00844 $path = $this->resolveToStoragePath( $path ); 00845 } 00846 $operations[] = array( 'op' => 'delete', 'src' => $path ); 00847 } 00848 // Actually delete files from storage... 00849 $opts = array( 'force' => true ); 00850 if ( $flags & self::SKIP_LOCKING ) { 00851 $opts['nonLocking'] = true; 00852 } 00853 $status->merge( $this->backend->doOperations( $operations, $opts ) ); 00854 00855 return $status; 00856 } 00857 00869 final public function quickImport( $src, $dst, $disposition = null ) { 00870 return $this->quickImportBatch( array( array( $src, $dst, $disposition ) ) ); 00871 } 00872 00881 final public function quickPurge( $path ) { 00882 return $this->quickPurgeBatch( array( $path ) ); 00883 } 00884 00892 public function quickCleanDir( $dir ) { 00893 $status = $this->newGood(); 00894 $status->merge( $this->backend->clean( 00895 array( 'dir' => $this->resolveToStoragePath( $dir ) ) ) ); 00896 00897 return $status; 00898 } 00899 00912 public function quickImportBatch( array $triples ) { 00913 $status = $this->newGood(); 00914 $operations = array(); 00915 foreach ( $triples as $triple ) { 00916 list( $src, $dst ) = $triple; 00917 $src = $this->resolveToStoragePath( $src ); 00918 $dst = $this->resolveToStoragePath( $dst ); 00919 $operations[] = array( 00920 'op' => FileBackend::isStoragePath( $src ) ? 'copy' : 'store', 00921 'src' => $src, 00922 'dst' => $dst, 00923 'disposition' => isset( $triple[2] ) ? $triple[2] : null 00924 ); 00925 $status->merge( $this->initDirectory( dirname( $dst ) ) ); 00926 } 00927 $status->merge( $this->backend->doQuickOperations( $operations ) ); 00928 00929 return $status; 00930 } 00931 00940 public function quickPurgeBatch( array $paths ) { 00941 $status = $this->newGood(); 00942 $operations = array(); 00943 foreach ( $paths as $path ) { 00944 $operations[] = array( 00945 'op' => 'delete', 00946 'src' => $this->resolveToStoragePath( $path ), 00947 'ignoreMissingSource' => true 00948 ); 00949 } 00950 $status->merge( $this->backend->doQuickOperations( $operations ) ); 00951 00952 return $status; 00953 } 00954 00965 public function storeTemp( $originalName, $srcPath ) { 00966 $this->assertWritableRepo(); // fail out if read-only 00967 00968 $date = MWTimestamp::getInstance()->format( 'YmdHis' ); 00969 $hashPath = $this->getHashPath( $originalName ); 00970 $dstUrlRel = $hashPath . $date . '!' . rawurlencode( $originalName ); 00971 $virtualUrl = $this->getVirtualUrl( 'temp' ) . '/' . $dstUrlRel; 00972 00973 $result = $this->quickImport( $srcPath, $virtualUrl ); 00974 $result->value = $virtualUrl; 00975 00976 return $result; 00977 } 00978 00985 public function freeTemp( $virtualUrl ) { 00986 $this->assertWritableRepo(); // fail out if read-only 00987 00988 $temp = $this->getVirtualUrl( 'temp' ); 00989 if ( substr( $virtualUrl, 0, strlen( $temp ) ) != $temp ) { 00990 wfDebug( __METHOD__ . ": Invalid temp virtual URL\n" ); 00991 return false; 00992 } 00993 00994 return $this->quickPurge( $virtualUrl )->isOK(); 00995 } 00996 01006 public function concatenate( array $srcPaths, $dstPath, $flags = 0 ) { 01007 $this->assertWritableRepo(); // fail out if read-only 01008 01009 $status = $this->newGood(); 01010 01011 $sources = array(); 01012 foreach ( $srcPaths as $srcPath ) { 01013 // Resolve source to a storage path if virtual 01014 $source = $this->resolveToStoragePath( $srcPath ); 01015 $sources[] = $source; // chunk to merge 01016 } 01017 01018 // Concatenate the chunks into one FS file 01019 $params = array( 'srcs' => $sources, 'dst' => $dstPath ); 01020 $status->merge( $this->backend->concatenate( $params ) ); 01021 if ( !$status->isOK() ) { 01022 return $status; 01023 } 01024 01025 // Delete the sources if required 01026 if ( $flags & self::DELETE_SOURCE ) { 01027 $status->merge( $this->quickPurgeBatch( $srcPaths ) ); 01028 } 01029 01030 // Make sure status is OK, despite any quickPurgeBatch() fatals 01031 $status->setResult( true ); 01032 01033 return $status; 01034 } 01035 01055 public function publish( 01056 $srcPath, $dstRel, $archiveRel, $flags = 0, array $options = array() 01057 ) { 01058 $this->assertWritableRepo(); // fail out if read-only 01059 01060 $status = $this->publishBatch( 01061 array( array( $srcPath, $dstRel, $archiveRel, $options ) ), $flags ); 01062 if ( $status->successCount == 0 ) { 01063 $status->ok = false; 01064 } 01065 if ( isset( $status->value[0] ) ) { 01066 $status->value = $status->value[0]; 01067 } else { 01068 $status->value = false; 01069 } 01070 01071 return $status; 01072 } 01073 01084 public function publishBatch( array $ntuples, $flags = 0 ) { 01085 $this->assertWritableRepo(); // fail out if read-only 01086 01087 $backend = $this->backend; // convenience 01088 // Try creating directories 01089 $status = $this->initZones( 'public' ); 01090 if ( !$status->isOK() ) { 01091 return $status; 01092 } 01093 01094 $status = $this->newGood( array() ); 01095 01096 $operations = array(); 01097 $sourceFSFilesToDelete = array(); // cleanup for disk source files 01098 // Validate each triplet and get the store operation... 01099 foreach ( $ntuples as $ntuple ) { 01100 list( $srcPath, $dstRel, $archiveRel ) = $ntuple; 01101 $options = isset( $ntuple[3] ) ? $ntuple[3] : array(); 01102 // Resolve source to a storage path if virtual 01103 $srcPath = $this->resolveToStoragePath( $srcPath ); 01104 if ( !$this->validateFilename( $dstRel ) ) { 01105 throw new MWException( 'Validation error in $dstRel' ); 01106 } 01107 if ( !$this->validateFilename( $archiveRel ) ) { 01108 throw new MWException( 'Validation error in $archiveRel' ); 01109 } 01110 01111 $publicRoot = $this->getZonePath( 'public' ); 01112 $dstPath = "$publicRoot/$dstRel"; 01113 $archivePath = "$publicRoot/$archiveRel"; 01114 01115 $dstDir = dirname( $dstPath ); 01116 $archiveDir = dirname( $archivePath ); 01117 // Abort immediately on directory creation errors since they're likely to be repetitive 01118 if ( !$this->initDirectory( $dstDir )->isOK() ) { 01119 return $this->newFatal( 'directorycreateerror', $dstDir ); 01120 } 01121 if ( !$this->initDirectory( $archiveDir )->isOK() ) { 01122 return $this->newFatal( 'directorycreateerror', $archiveDir ); 01123 } 01124 01125 // Set any desired headers to be use in GET/HEAD responses 01126 $headers = isset( $options['headers'] ) ? $options['headers'] : array(); 01127 01128 // Archive destination file if it exists. 01129 // This will check if the archive file also exists and fail if does. 01130 // This is a sanity check to avoid data loss. On Windows and Linux, 01131 // copy() will overwrite, so the existence check is vulnerable to 01132 // race conditions unless an functioning LockManager is used. 01133 // LocalFile also uses SELECT FOR UPDATE for synchronization. 01134 $operations[] = array( 01135 'op' => 'copy', 01136 'src' => $dstPath, 01137 'dst' => $archivePath, 01138 'ignoreMissingSource' => true 01139 ); 01140 01141 // Copy (or move) the source file to the destination 01142 if ( FileBackend::isStoragePath( $srcPath ) ) { 01143 if ( $flags & self::DELETE_SOURCE ) { 01144 $operations[] = array( 01145 'op' => 'move', 01146 'src' => $srcPath, 01147 'dst' => $dstPath, 01148 'overwrite' => true, // replace current 01149 'headers' => $headers 01150 ); 01151 } else { 01152 $operations[] = array( 01153 'op' => 'copy', 01154 'src' => $srcPath, 01155 'dst' => $dstPath, 01156 'overwrite' => true, // replace current 01157 'headers' => $headers 01158 ); 01159 } 01160 } else { // FS source path 01161 $operations[] = array( 01162 'op' => 'store', 01163 'src' => $srcPath, 01164 'dst' => $dstPath, 01165 'overwrite' => true, // replace current 01166 'headers' => $headers 01167 ); 01168 if ( $flags & self::DELETE_SOURCE ) { 01169 $sourceFSFilesToDelete[] = $srcPath; 01170 } 01171 } 01172 } 01173 01174 // Execute the operations for each triplet 01175 $status->merge( $backend->doOperations( $operations ) ); 01176 // Find out which files were archived... 01177 foreach ( $ntuples as $i => $ntuple ) { 01178 list( , , $archiveRel ) = $ntuple; 01179 $archivePath = $this->getZonePath( 'public' ) . "/$archiveRel"; 01180 if ( $this->fileExists( $archivePath ) ) { 01181 $status->value[$i] = 'archived'; 01182 } else { 01183 $status->value[$i] = 'new'; 01184 } 01185 } 01186 // Cleanup for disk source files... 01187 foreach ( $sourceFSFilesToDelete as $file ) { 01188 wfSuppressWarnings(); 01189 unlink( $file ); // FS cleanup 01190 wfRestoreWarnings(); 01191 } 01192 01193 return $status; 01194 } 01195 01203 protected function initDirectory( $dir ) { 01204 $path = $this->resolveToStoragePath( $dir ); 01205 list( , $container, ) = FileBackend::splitStoragePath( $path ); 01206 01207 $params = array( 'dir' => $path ); 01208 if ( $this->isPrivate || $container === $this->zones['deleted']['container'] ) { 01209 # Take all available measures to prevent web accessibility of new deleted 01210 # directories, in case the user has not configured offline storage 01211 $params = array( 'noAccess' => true, 'noListing' => true ) + $params; 01212 } 01213 01214 return $this->backend->prepare( $params ); 01215 } 01216 01223 public function cleanDir( $dir ) { 01224 $this->assertWritableRepo(); // fail out if read-only 01225 01226 $status = $this->newGood(); 01227 $status->merge( $this->backend->clean( 01228 array( 'dir' => $this->resolveToStoragePath( $dir ) ) ) ); 01229 01230 return $status; 01231 } 01232 01239 public function fileExists( $file ) { 01240 $result = $this->fileExistsBatch( array( $file ) ); 01241 return $result[0]; 01242 } 01243 01250 public function fileExistsBatch( array $files ) { 01251 $result = array(); 01252 foreach ( $files as $key => $file ) { 01253 $file = $this->resolveToStoragePath( $file ); 01254 $result[$key] = $this->backend->fileExists( array( 'src' => $file ) ); 01255 } 01256 return $result; 01257 } 01258 01269 public function delete( $srcRel, $archiveRel ) { 01270 $this->assertWritableRepo(); // fail out if read-only 01271 01272 return $this->deleteBatch( array( array( $srcRel, $archiveRel ) ) ); 01273 } 01274 01292 public function deleteBatch( array $sourceDestPairs ) { 01293 $this->assertWritableRepo(); // fail out if read-only 01294 01295 // Try creating directories 01296 $status = $this->initZones( array( 'public', 'deleted' ) ); 01297 if ( !$status->isOK() ) { 01298 return $status; 01299 } 01300 01301 $status = $this->newGood(); 01302 01303 $backend = $this->backend; // convenience 01304 $operations = array(); 01305 // Validate filenames and create archive directories 01306 foreach ( $sourceDestPairs as $pair ) { 01307 list( $srcRel, $archiveRel ) = $pair; 01308 if ( !$this->validateFilename( $srcRel ) ) { 01309 throw new MWException( __METHOD__ . ':Validation error in $srcRel' ); 01310 } elseif ( !$this->validateFilename( $archiveRel ) ) { 01311 throw new MWException( __METHOD__ . ':Validation error in $archiveRel' ); 01312 } 01313 01314 $publicRoot = $this->getZonePath( 'public' ); 01315 $srcPath = "{$publicRoot}/$srcRel"; 01316 01317 $deletedRoot = $this->getZonePath( 'deleted' ); 01318 $archivePath = "{$deletedRoot}/{$archiveRel}"; 01319 $archiveDir = dirname( $archivePath ); // does not touch FS 01320 01321 // Create destination directories 01322 if ( !$this->initDirectory( $archiveDir )->isOK() ) { 01323 return $this->newFatal( 'directorycreateerror', $archiveDir ); 01324 } 01325 01326 $operations[] = array( 01327 'op' => 'move', 01328 'src' => $srcPath, 01329 'dst' => $archivePath, 01330 // We may have 2+ identical files being deleted, 01331 // all of which will map to the same destination file 01332 'overwriteSame' => true // also see bug 31792 01333 ); 01334 } 01335 01336 // Move the files by execute the operations for each pair. 01337 // We're now committed to returning an OK result, which will 01338 // lead to the files being moved in the DB also. 01339 $opts = array( 'force' => true ); 01340 $status->merge( $backend->doOperations( $operations, $opts ) ); 01341 01342 return $status; 01343 } 01344 01350 public function cleanupDeletedBatch( array $storageKeys ) { 01351 $this->assertWritableRepo(); 01352 } 01353 01362 public function getDeletedHashPath( $key ) { 01363 if ( strlen( $key ) < 31 ) { 01364 throw new MWException( "Invalid storage key '$key'." ); 01365 } 01366 $path = ''; 01367 for ( $i = 0; $i < $this->deletedHashLevels; $i++ ) { 01368 $path .= $key[$i] . '/'; 01369 } 01370 return $path; 01371 } 01372 01381 protected function resolveToStoragePath( $path ) { 01382 if ( $this->isVirtualUrl( $path ) ) { 01383 return $this->resolveVirtualUrl( $path ); 01384 } 01385 return $path; 01386 } 01387 01395 public function getLocalCopy( $virtualUrl ) { 01396 $path = $this->resolveToStoragePath( $virtualUrl ); 01397 return $this->backend->getLocalCopy( array( 'src' => $path ) ); 01398 } 01399 01408 public function getLocalReference( $virtualUrl ) { 01409 $path = $this->resolveToStoragePath( $virtualUrl ); 01410 return $this->backend->getLocalReference( array( 'src' => $path ) ); 01411 } 01412 01420 public function getFileProps( $virtualUrl ) { 01421 $path = $this->resolveToStoragePath( $virtualUrl ); 01422 return $this->backend->getFileProps( array( 'src' => $path ) ); 01423 } 01424 01431 public function getFileTimestamp( $virtualUrl ) { 01432 $path = $this->resolveToStoragePath( $virtualUrl ); 01433 return $this->backend->getFileTimestamp( array( 'src' => $path ) ); 01434 } 01435 01442 public function getFileSize( $virtualUrl ) { 01443 $path = $this->resolveToStoragePath( $virtualUrl ); 01444 return $this->backend->getFileSize( array( 'src' => $path ) ); 01445 } 01446 01453 public function getFileSha1( $virtualUrl ) { 01454 $path = $this->resolveToStoragePath( $virtualUrl ); 01455 return $this->backend->getFileSha1Base36( array( 'src' => $path ) ); 01456 } 01457 01465 public function streamFile( $virtualUrl, $headers = array() ) { 01466 $path = $this->resolveToStoragePath( $virtualUrl ); 01467 $params = array( 'src' => $path, 'headers' => $headers ); 01468 return $this->backend->streamFile( $params )->isOK(); 01469 } 01470 01479 public function enumFiles( $callback ) { 01480 $this->enumFilesInStorage( $callback ); 01481 } 01482 01490 protected function enumFilesInStorage( $callback ) { 01491 $publicRoot = $this->getZonePath( 'public' ); 01492 $numDirs = 1 << ( $this->hashLevels * 4 ); 01493 // Use a priori assumptions about directory structure 01494 // to reduce the tree height of the scanning process. 01495 for ( $flatIndex = 0; $flatIndex < $numDirs; $flatIndex++ ) { 01496 $hexString = sprintf( "%0{$this->hashLevels}x", $flatIndex ); 01497 $path = $publicRoot; 01498 for ( $hexPos = 0; $hexPos < $this->hashLevels; $hexPos++ ) { 01499 $path .= '/' . substr( $hexString, 0, $hexPos + 1 ); 01500 } 01501 $iterator = $this->backend->getFileList( array( 'dir' => $path ) ); 01502 foreach ( $iterator as $name ) { 01503 // Each item returned is a public file 01504 call_user_func( $callback, "{$path}/{$name}" ); 01505 } 01506 } 01507 } 01508 01515 public function validateFilename( $filename ) { 01516 if ( strval( $filename ) == '' ) { 01517 return false; 01518 } 01519 return FileBackend::isPathTraversalFree( $filename ); 01520 } 01521 01527 function getErrorCleanupFunction() { 01528 switch ( $this->pathDisclosureProtection ) { 01529 case 'none': 01530 case 'simple': // b/c 01531 $callback = array( $this, 'passThrough' ); 01532 break; 01533 default: // 'paranoid' 01534 $callback = array( $this, 'paranoidClean' ); 01535 } 01536 return $callback; 01537 } 01538 01545 function paranoidClean( $param ) { 01546 return '[hidden]'; 01547 } 01548 01555 function passThrough( $param ) { 01556 return $param; 01557 } 01558 01564 public function newFatal( $message /*, parameters...*/ ) { 01565 $params = func_get_args(); 01566 array_unshift( $params, $this ); 01567 return call_user_func_array( array( 'FileRepoStatus', 'newFatal' ), $params ); 01568 } 01569 01576 public function newGood( $value = null ) { 01577 return FileRepoStatus::newGood( $this, $value ); 01578 } 01579 01588 public function checkRedirect( Title $title ) { 01589 return false; 01590 } 01591 01599 public function invalidateImageRedirect( Title $title ) {} 01600 01606 public function getDisplayName() { 01607 // We don't name our own repo, return nothing 01608 if ( $this->isLocal() ) { 01609 return null; 01610 } 01611 // 'shared-repo-name-wikimediacommons' is used when $wgUseInstantCommons = true 01612 return wfMessageFallback( 'shared-repo-name-' . $this->name, 'shared-repo' )->text(); 01613 } 01614 01622 public function nameForThumb( $name ) { 01623 if ( strlen( $name ) > $this->abbrvThreshold ) { 01624 $ext = FileBackend::extensionFromPath( $name ); 01625 $name = ( $ext == '' ) ? 'thumbnail' : "thumbnail.$ext"; 01626 } 01627 return $name; 01628 } 01629 01635 public function isLocal() { 01636 return $this->getName() == 'local'; 01637 } 01638 01647 public function getSharedCacheKey( /*...*/ ) { 01648 return false; 01649 } 01650 01658 public function getLocalCacheKey( /*...*/ ) { 01659 $args = func_get_args(); 01660 array_unshift( $args, 'filerepo', $this->getName() ); 01661 return call_user_func_array( 'wfMemcKey', $args ); 01662 } 01663 01672 public function getTempRepo() { 01673 return new TempFileRepo( array( 01674 'name' => "{$this->name}-temp", 01675 'backend' => $this->backend, 01676 'zones' => array( 01677 'public' => array( 01678 'container' => $this->zones['temp']['container'], 01679 'directory' => $this->zones['temp']['directory'] 01680 ), 01681 'thumb' => array( 01682 'container' => $this->zones['thumb']['container'], 01683 'directory' => ( $this->zones['thumb']['directory'] == '' ) 01684 ? 'temp' 01685 : $this->zones['thumb']['directory'] . '/temp' 01686 ), 01687 'transcoded' => array( 01688 'container' => $this->zones['transcoded']['container'], 01689 'directory' => ( $this->zones['transcoded']['directory'] == '' ) 01690 ? 'temp' 01691 : $this->zones['transcoded']['directory'] . '/temp' 01692 ) 01693 ), 01694 'url' => $this->getZoneUrl( 'temp' ), 01695 'thumbUrl' => $this->getZoneUrl( 'thumb' ) . '/temp', 01696 'transcodedUrl' => $this->getZoneUrl( 'transcoded' ) . '/temp', 01697 'hashLevels' => $this->hashLevels // performance 01698 ) ); 01699 } 01700 01707 public function getUploadStash( User $user = null ) { 01708 return new UploadStash( $this, $user ); 01709 } 01710 01718 protected function assertWritableRepo() {} 01719 01720 01727 public function getInfo() { 01728 return array( 01729 'name' => $this->getName(), 01730 'displayname' => $this->getDisplayName(), 01731 'rootUrl' => $this->getRootUrl(), 01732 'local' => $this->isLocal(), 01733 ); 01734 } 01735 } 01736 01740 class TempFileRepo extends FileRepo { 01741 public function getTempRepo() { 01742 throw new MWException( "Cannot get a temp repo from a temp repo." ); 01743 } 01744 }