MediaWiki
REL1_23
|
00001 <?php 00085 abstract class FileBackend { 00087 protected $name; 00088 00090 protected $wikiId; 00091 00093 protected $readOnly; 00094 00096 protected $parallelize; 00097 00099 protected $concurrency; 00100 00102 protected $lockManager; 00103 00105 protected $fileJournal; 00106 00108 const ATTR_HEADERS = 1; 00109 const ATTR_METADATA = 2; 00110 00135 public function __construct( array $config ) { 00136 $this->name = $config['name']; 00137 if ( !preg_match( '!^[a-zA-Z0-9-_]{1,255}$!', $this->name ) ) { 00138 throw new FileBackendException( "Backend name `{$this->name}` is invalid." ); 00139 } 00140 if ( !isset( $config['wikiId'] ) ) { 00141 $config['wikiId'] = wfWikiID(); 00142 wfDeprecated( __METHOD__ . ' called without "wikiID".', '1.23' ); 00143 } 00144 if ( isset( $config['lockManager'] ) && !is_object( $config['lockManager'] ) ) { 00145 $config['lockManager'] = 00146 LockManagerGroup::singleton( $config['wikiId'] )->get( $config['lockManager'] ); 00147 wfDeprecated( __METHOD__ . ' called with non-object "lockManager".', '1.23' ); 00148 } 00149 $this->wikiId = $config['wikiId']; // e.g. "my_wiki-en_" 00150 $this->lockManager = isset( $config['lockManager'] ) 00151 ? $config['lockManager'] 00152 : new NullLockManager( array() ); 00153 $this->fileJournal = isset( $config['fileJournal'] ) 00154 ? $config['fileJournal'] 00155 : FileJournal::factory( array( 'class' => 'NullFileJournal' ), $this->name ); 00156 $this->readOnly = isset( $config['readOnly'] ) 00157 ? (string)$config['readOnly'] 00158 : ''; 00159 $this->parallelize = isset( $config['parallelize'] ) 00160 ? (string)$config['parallelize'] 00161 : 'off'; 00162 $this->concurrency = isset( $config['concurrency'] ) 00163 ? (int)$config['concurrency'] 00164 : 50; 00165 } 00166 00174 final public function getName() { 00175 return $this->name; 00176 } 00177 00185 final public function getWikiId() { 00186 return $this->wikiId; 00187 } 00188 00194 final public function isReadOnly() { 00195 return ( $this->readOnly != '' ); 00196 } 00197 00203 final public function getReadOnlyReason() { 00204 return ( $this->readOnly != '' ) ? $this->readOnly : false; 00205 } 00206 00213 public function getFeatures() { 00214 return 0; 00215 } 00216 00224 final public function hasFeatures( $bitfield ) { 00225 return ( $this->getFeatures() & $bitfield ) === $bitfield; 00226 } 00227 00374 final public function doOperations( array $ops, array $opts = array() ) { 00375 if ( empty( $opts['bypassReadOnly'] ) && $this->isReadOnly() ) { 00376 return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly ); 00377 } 00378 if ( !count( $ops ) ) { 00379 return Status::newGood(); // nothing to do 00380 } 00381 if ( empty( $opts['force'] ) ) { // sanity 00382 unset( $opts['nonLocking'] ); 00383 } 00384 foreach ( $ops as &$op ) { 00385 if ( isset( $op['disposition'] ) ) { // b/c (MW 1.20) 00386 $op['headers']['Content-Disposition'] = $op['disposition']; 00387 } 00388 } 00389 $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts 00390 return $this->doOperationsInternal( $ops, $opts ); 00391 } 00392 00396 abstract protected function doOperationsInternal( array $ops, array $opts ); 00397 00409 final public function doOperation( array $op, array $opts = array() ) { 00410 return $this->doOperations( array( $op ), $opts ); 00411 } 00412 00423 final public function create( array $params, array $opts = array() ) { 00424 return $this->doOperation( array( 'op' => 'create' ) + $params, $opts ); 00425 } 00426 00437 final public function store( array $params, array $opts = array() ) { 00438 return $this->doOperation( array( 'op' => 'store' ) + $params, $opts ); 00439 } 00440 00451 final public function copy( array $params, array $opts = array() ) { 00452 return $this->doOperation( array( 'op' => 'copy' ) + $params, $opts ); 00453 } 00454 00465 final public function move( array $params, array $opts = array() ) { 00466 return $this->doOperation( array( 'op' => 'move' ) + $params, $opts ); 00467 } 00468 00479 final public function delete( array $params, array $opts = array() ) { 00480 return $this->doOperation( array( 'op' => 'delete' ) + $params, $opts ); 00481 } 00482 00494 final public function describe( array $params, array $opts = array() ) { 00495 return $this->doOperation( array( 'op' => 'describe' ) + $params, $opts ); 00496 } 00497 00608 final public function doQuickOperations( array $ops, array $opts = array() ) { 00609 if ( empty( $opts['bypassReadOnly'] ) && $this->isReadOnly() ) { 00610 return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly ); 00611 } 00612 if ( !count( $ops ) ) { 00613 return Status::newGood(); // nothing to do 00614 } 00615 foreach ( $ops as &$op ) { 00616 $op['overwrite'] = true; // avoids RTTs in key/value stores 00617 if ( isset( $op['disposition'] ) ) { // b/c (MW 1.20) 00618 $op['headers']['Content-Disposition'] = $op['disposition']; 00619 } 00620 } 00621 $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts 00622 return $this->doQuickOperationsInternal( $ops ); 00623 } 00624 00629 abstract protected function doQuickOperationsInternal( array $ops ); 00630 00641 final public function doQuickOperation( array $op ) { 00642 return $this->doQuickOperations( array( $op ) ); 00643 } 00644 00655 final public function quickCreate( array $params ) { 00656 return $this->doQuickOperation( array( 'op' => 'create' ) + $params ); 00657 } 00658 00669 final public function quickStore( array $params ) { 00670 return $this->doQuickOperation( array( 'op' => 'store' ) + $params ); 00671 } 00672 00683 final public function quickCopy( array $params ) { 00684 return $this->doQuickOperation( array( 'op' => 'copy' ) + $params ); 00685 } 00686 00697 final public function quickMove( array $params ) { 00698 return $this->doQuickOperation( array( 'op' => 'move' ) + $params ); 00699 } 00700 00711 final public function quickDelete( array $params ) { 00712 return $this->doQuickOperation( array( 'op' => 'delete' ) + $params ); 00713 } 00714 00725 final public function quickDescribe( array $params ) { 00726 return $this->doQuickOperation( array( 'op' => 'describe' ) + $params ); 00727 } 00728 00741 abstract public function concatenate( array $params ); 00742 00761 final public function prepare( array $params ) { 00762 if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) { 00763 return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly ); 00764 } 00765 $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts 00766 return $this->doPrepare( $params ); 00767 } 00768 00772 abstract protected function doPrepare( array $params ); 00773 00790 final public function secure( array $params ) { 00791 if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) { 00792 return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly ); 00793 } 00794 $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts 00795 return $this->doSecure( $params ); 00796 } 00797 00801 abstract protected function doSecure( array $params ); 00802 00821 final public function publish( array $params ) { 00822 if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) { 00823 return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly ); 00824 } 00825 $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts 00826 return $this->doPublish( $params ); 00827 } 00828 00832 abstract protected function doPublish( array $params ); 00833 00845 final public function clean( array $params ) { 00846 if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) { 00847 return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly ); 00848 } 00849 $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts 00850 return $this->doClean( $params ); 00851 } 00852 00856 abstract protected function doClean( array $params ); 00857 00865 final protected function getScopedPHPBehaviorForOps() { 00866 if ( PHP_SAPI != 'cli' ) { // http://bugs.php.net/bug.php?id=47540 00867 $old = ignore_user_abort( true ); // avoid half-finished operations 00868 return new ScopedCallback( function () use ( $old ) { 00869 ignore_user_abort( $old ); 00870 } ); 00871 } 00872 00873 return null; 00874 } 00875 00885 abstract public function fileExists( array $params ); 00886 00895 abstract public function getFileTimestamp( array $params ); 00896 00906 final public function getFileContents( array $params ) { 00907 $contents = $this->getFileContentsMulti( 00908 array( 'srcs' => array( $params['src'] ) ) + $params ); 00909 00910 return $contents[$params['src']]; 00911 } 00912 00927 abstract public function getFileContentsMulti( array $params ); 00928 00947 abstract public function getFileXAttributes( array $params ); 00948 00957 abstract public function getFileSize( array $params ); 00958 00972 abstract public function getFileStat( array $params ); 00973 00982 abstract public function getFileSha1Base36( array $params ); 00983 00993 abstract public function getFileProps( array $params ); 00994 01008 abstract public function streamFile( array $params ); 01009 01028 final public function getLocalReference( array $params ) { 01029 $fsFiles = $this->getLocalReferenceMulti( 01030 array( 'srcs' => array( $params['src'] ) ) + $params ); 01031 01032 return $fsFiles[$params['src']]; 01033 } 01034 01049 abstract public function getLocalReferenceMulti( array $params ); 01050 01061 final public function getLocalCopy( array $params ) { 01062 $tmpFiles = $this->getLocalCopyMulti( 01063 array( 'srcs' => array( $params['src'] ) ) + $params ); 01064 01065 return $tmpFiles[$params['src']]; 01066 } 01067 01082 abstract public function getLocalCopyMulti( array $params ); 01083 01100 abstract public function getFileHttpUrl( array $params ); 01101 01114 abstract public function directoryExists( array $params ); 01115 01134 abstract public function getDirectoryList( array $params ); 01135 01149 final public function getTopDirectoryList( array $params ) { 01150 return $this->getDirectoryList( array( 'topOnly' => true ) + $params ); 01151 } 01152 01171 abstract public function getFileList( array $params ); 01172 01187 final public function getTopFileList( array $params ) { 01188 return $this->getFileList( array( 'topOnly' => true ) + $params ); 01189 } 01190 01199 abstract public function preloadCache( array $paths ); 01200 01209 abstract public function clearCache( array $paths = null ); 01210 01222 public function preloadFileStat( array $params ) { 01223 } 01224 01235 final public function lockFiles( array $paths, $type ) { 01236 $paths = array_map( 'FileBackend::normalizeStoragePath', $paths ); 01237 01238 return $this->lockManager->lock( $paths, $type ); 01239 } 01240 01248 final public function unlockFiles( array $paths, $type ) { 01249 $paths = array_map( 'FileBackend::normalizeStoragePath', $paths ); 01250 01251 return $this->lockManager->unlock( $paths, $type ); 01252 } 01253 01269 final public function getScopedFileLocks( array $paths, $type, Status $status ) { 01270 if ( $type === 'mixed' ) { 01271 foreach ( $paths as &$typePaths ) { 01272 $typePaths = array_map( 'FileBackend::normalizeStoragePath', $typePaths ); 01273 } 01274 } else { 01275 $paths = array_map( 'FileBackend::normalizeStoragePath', $paths ); 01276 } 01277 01278 return ScopedLock::factory( $this->lockManager, $paths, $type, $status ); 01279 } 01280 01297 abstract public function getScopedLocksForOps( array $ops, Status $status ); 01298 01306 final public function getRootStoragePath() { 01307 return "mwstore://{$this->name}"; 01308 } 01309 01317 final public function getContainerStoragePath( $container ) { 01318 return $this->getRootStoragePath() . "/{$container}"; 01319 } 01320 01326 final public function getJournal() { 01327 return $this->fileJournal; 01328 } 01329 01337 final public static function isStoragePath( $path ) { 01338 return ( strpos( $path, 'mwstore://' ) === 0 ); 01339 } 01340 01349 final public static function splitStoragePath( $storagePath ) { 01350 if ( self::isStoragePath( $storagePath ) ) { 01351 // Remove the "mwstore://" prefix and split the path 01352 $parts = explode( '/', substr( $storagePath, 10 ), 3 ); 01353 if ( count( $parts ) >= 2 && $parts[0] != '' && $parts[1] != '' ) { 01354 if ( count( $parts ) == 3 ) { 01355 return $parts; // e.g. "backend/container/path" 01356 } else { 01357 return array( $parts[0], $parts[1], '' ); // e.g. "backend/container" 01358 } 01359 } 01360 } 01361 01362 return array( null, null, null ); 01363 } 01364 01372 final public static function normalizeStoragePath( $storagePath ) { 01373 list( $backend, $container, $relPath ) = self::splitStoragePath( $storagePath ); 01374 if ( $relPath !== null ) { // must be for this backend 01375 $relPath = self::normalizeContainerPath( $relPath ); 01376 if ( $relPath !== null ) { 01377 return ( $relPath != '' ) 01378 ? "mwstore://{$backend}/{$container}/{$relPath}" 01379 : "mwstore://{$backend}/{$container}"; 01380 } 01381 } 01382 01383 return null; 01384 } 01385 01394 final public static function parentStoragePath( $storagePath ) { 01395 $storagePath = dirname( $storagePath ); 01396 list( , , $rel ) = self::splitStoragePath( $storagePath ); 01397 01398 return ( $rel === null ) ? null : $storagePath; 01399 } 01400 01407 final public static function extensionFromPath( $path ) { 01408 $i = strrpos( $path, '.' ); 01409 01410 return strtolower( $i ? substr( $path, $i + 1 ) : '' ); 01411 } 01412 01420 final public static function isPathTraversalFree( $path ) { 01421 return ( self::normalizeContainerPath( $path ) !== null ); 01422 } 01423 01433 final public static function makeContentDisposition( $type, $filename = '' ) { 01434 $parts = array(); 01435 01436 $type = strtolower( $type ); 01437 if ( !in_array( $type, array( 'inline', 'attachment' ) ) ) { 01438 throw new FileBackendError( "Invalid Content-Disposition type '$type'." ); 01439 } 01440 $parts[] = $type; 01441 01442 if ( strlen( $filename ) ) { 01443 $parts[] = "filename*=UTF-8''" . rawurlencode( basename( $filename ) ); 01444 } 01445 01446 return implode( ';', $parts ); 01447 } 01448 01459 final protected static function normalizeContainerPath( $path ) { 01460 // Normalize directory separators 01461 $path = strtr( $path, '\\', '/' ); 01462 // Collapse any consecutive directory separators 01463 $path = preg_replace( '![/]{2,}!', '/', $path ); 01464 // Remove any leading directory separator 01465 $path = ltrim( $path, '/' ); 01466 // Use the same traversal protection as Title::secureAndSplit() 01467 if ( strpos( $path, '.' ) !== false ) { 01468 if ( 01469 $path === '.' || 01470 $path === '..' || 01471 strpos( $path, './' ) === 0 || 01472 strpos( $path, '../' ) === 0 || 01473 strpos( $path, '/./' ) !== false || 01474 strpos( $path, '/../' ) !== false 01475 ) { 01476 return null; 01477 } 01478 } 01479 01480 return $path; 01481 } 01482 } 01483 01490 class FileBackendException extends MWException { 01491 } 01492 01499 class FileBackendError extends FileBackendException { 01500 }