MediaWiki  REL1_24
FileBackend.php
Go to the documentation of this file.
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; // files can be tagged with standard HTTP headers
00109     const ATTR_METADATA = 2; // files can be stored with metadata key/values
00110     const ATTR_UNICODE_PATHS = 4; // files can have Unicode paths (not just ASCII)
00111 
00136     public function __construct( array $config ) {
00137         $this->name = $config['name'];
00138         $this->wikiId = $config['wikiId']; // e.g. "my_wiki-en_"
00139         if ( !preg_match( '!^[a-zA-Z0-9-_]{1,255}$!', $this->name ) ) {
00140             throw new FileBackendException( "Backend name '{$this->name}' is invalid." );
00141         } elseif ( !is_string( $this->wikiId ) ) {
00142             throw new FileBackendException( "Backend wiki ID not provided for '{$this->name}'." );
00143         }
00144         $this->lockManager = isset( $config['lockManager'] )
00145             ? $config['lockManager']
00146             : new NullLockManager( array() );
00147         $this->fileJournal = isset( $config['fileJournal'] )
00148             ? $config['fileJournal']
00149             : FileJournal::factory( array( 'class' => 'NullFileJournal' ), $this->name );
00150         $this->readOnly = isset( $config['readOnly'] )
00151             ? (string)$config['readOnly']
00152             : '';
00153         $this->parallelize = isset( $config['parallelize'] )
00154             ? (string)$config['parallelize']
00155             : 'off';
00156         $this->concurrency = isset( $config['concurrency'] )
00157             ? (int)$config['concurrency']
00158             : 50;
00159     }
00160 
00168     final public function getName() {
00169         return $this->name;
00170     }
00171 
00179     final public function getWikiId() {
00180         return $this->wikiId;
00181     }
00182 
00188     final public function isReadOnly() {
00189         return ( $this->readOnly != '' );
00190     }
00191 
00197     final public function getReadOnlyReason() {
00198         return ( $this->readOnly != '' ) ? $this->readOnly : false;
00199     }
00200 
00207     public function getFeatures() {
00208         return self::ATTR_UNICODE_PATHS;
00209     }
00210 
00218     final public function hasFeatures( $bitfield ) {
00219         return ( $this->getFeatures() & $bitfield ) === $bitfield;
00220     }
00221 
00368     final public function doOperations( array $ops, array $opts = array() ) {
00369         if ( empty( $opts['bypassReadOnly'] ) && $this->isReadOnly() ) {
00370             return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
00371         }
00372         if ( !count( $ops ) ) {
00373             return Status::newGood(); // nothing to do
00374         }
00375         if ( empty( $opts['force'] ) ) { // sanity
00376             unset( $opts['nonLocking'] );
00377         }
00378         foreach ( $ops as &$op ) {
00379             if ( isset( $op['disposition'] ) ) { // b/c (MW 1.20)
00380                 $op['headers']['Content-Disposition'] = $op['disposition'];
00381             }
00382         }
00383         $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
00384         return $this->doOperationsInternal( $ops, $opts );
00385     }
00386 
00390     abstract protected function doOperationsInternal( array $ops, array $opts );
00391 
00403     final public function doOperation( array $op, array $opts = array() ) {
00404         return $this->doOperations( array( $op ), $opts );
00405     }
00406 
00417     final public function create( array $params, array $opts = array() ) {
00418         return $this->doOperation( array( 'op' => 'create' ) + $params, $opts );
00419     }
00420 
00431     final public function store( array $params, array $opts = array() ) {
00432         return $this->doOperation( array( 'op' => 'store' ) + $params, $opts );
00433     }
00434 
00445     final public function copy( array $params, array $opts = array() ) {
00446         return $this->doOperation( array( 'op' => 'copy' ) + $params, $opts );
00447     }
00448 
00459     final public function move( array $params, array $opts = array() ) {
00460         return $this->doOperation( array( 'op' => 'move' ) + $params, $opts );
00461     }
00462 
00473     final public function delete( array $params, array $opts = array() ) {
00474         return $this->doOperation( array( 'op' => 'delete' ) + $params, $opts );
00475     }
00476 
00488     final public function describe( array $params, array $opts = array() ) {
00489         return $this->doOperation( array( 'op' => 'describe' ) + $params, $opts );
00490     }
00491 
00602     final public function doQuickOperations( array $ops, array $opts = array() ) {
00603         if ( empty( $opts['bypassReadOnly'] ) && $this->isReadOnly() ) {
00604             return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
00605         }
00606         if ( !count( $ops ) ) {
00607             return Status::newGood(); // nothing to do
00608         }
00609         foreach ( $ops as &$op ) {
00610             $op['overwrite'] = true; // avoids RTTs in key/value stores
00611             if ( isset( $op['disposition'] ) ) { // b/c (MW 1.20)
00612                 $op['headers']['Content-Disposition'] = $op['disposition'];
00613             }
00614         }
00615         $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
00616         return $this->doQuickOperationsInternal( $ops );
00617     }
00618 
00623     abstract protected function doQuickOperationsInternal( array $ops );
00624 
00635     final public function doQuickOperation( array $op ) {
00636         return $this->doQuickOperations( array( $op ) );
00637     }
00638 
00649     final public function quickCreate( array $params ) {
00650         return $this->doQuickOperation( array( 'op' => 'create' ) + $params );
00651     }
00652 
00663     final public function quickStore( array $params ) {
00664         return $this->doQuickOperation( array( 'op' => 'store' ) + $params );
00665     }
00666 
00677     final public function quickCopy( array $params ) {
00678         return $this->doQuickOperation( array( 'op' => 'copy' ) + $params );
00679     }
00680 
00691     final public function quickMove( array $params ) {
00692         return $this->doQuickOperation( array( 'op' => 'move' ) + $params );
00693     }
00694 
00705     final public function quickDelete( array $params ) {
00706         return $this->doQuickOperation( array( 'op' => 'delete' ) + $params );
00707     }
00708 
00719     final public function quickDescribe( array $params ) {
00720         return $this->doQuickOperation( array( 'op' => 'describe' ) + $params );
00721     }
00722 
00735     abstract public function concatenate( array $params );
00736 
00755     final public function prepare( array $params ) {
00756         if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
00757             return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
00758         }
00759         $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
00760         return $this->doPrepare( $params );
00761     }
00762 
00766     abstract protected function doPrepare( array $params );
00767 
00784     final public function secure( array $params ) {
00785         if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
00786             return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
00787         }
00788         $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
00789         return $this->doSecure( $params );
00790     }
00791 
00795     abstract protected function doSecure( array $params );
00796 
00815     final public function publish( array $params ) {
00816         if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
00817             return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
00818         }
00819         $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
00820         return $this->doPublish( $params );
00821     }
00822 
00826     abstract protected function doPublish( array $params );
00827 
00839     final public function clean( array $params ) {
00840         if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
00841             return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
00842         }
00843         $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
00844         return $this->doClean( $params );
00845     }
00846 
00850     abstract protected function doClean( array $params );
00851 
00859     final protected function getScopedPHPBehaviorForOps() {
00860         if ( PHP_SAPI != 'cli' ) { // http://bugs.php.net/bug.php?id=47540
00861             $old = ignore_user_abort( true ); // avoid half-finished operations
00862             return new ScopedCallback( function () use ( $old ) {
00863                 ignore_user_abort( $old );
00864             } );
00865         }
00866 
00867         return null;
00868     }
00869 
00879     abstract public function fileExists( array $params );
00880 
00889     abstract public function getFileTimestamp( array $params );
00890 
00900     final public function getFileContents( array $params ) {
00901         $contents = $this->getFileContentsMulti(
00902             array( 'srcs' => array( $params['src'] ) ) + $params );
00903 
00904         return $contents[$params['src']];
00905     }
00906 
00921     abstract public function getFileContentsMulti( array $params );
00922 
00941     abstract public function getFileXAttributes( array $params );
00942 
00951     abstract public function getFileSize( array $params );
00952 
00966     abstract public function getFileStat( array $params );
00967 
00976     abstract public function getFileSha1Base36( array $params );
00977 
00987     abstract public function getFileProps( array $params );
00988 
01002     abstract public function streamFile( array $params );
01003 
01022     final public function getLocalReference( array $params ) {
01023         $fsFiles = $this->getLocalReferenceMulti(
01024             array( 'srcs' => array( $params['src'] ) ) + $params );
01025 
01026         return $fsFiles[$params['src']];
01027     }
01028 
01043     abstract public function getLocalReferenceMulti( array $params );
01044 
01055     final public function getLocalCopy( array $params ) {
01056         $tmpFiles = $this->getLocalCopyMulti(
01057             array( 'srcs' => array( $params['src'] ) ) + $params );
01058 
01059         return $tmpFiles[$params['src']];
01060     }
01061 
01076     abstract public function getLocalCopyMulti( array $params );
01077 
01094     abstract public function getFileHttpUrl( array $params );
01095 
01108     abstract public function directoryExists( array $params );
01109 
01128     abstract public function getDirectoryList( array $params );
01129 
01143     final public function getTopDirectoryList( array $params ) {
01144         return $this->getDirectoryList( array( 'topOnly' => true ) + $params );
01145     }
01146 
01165     abstract public function getFileList( array $params );
01166 
01181     final public function getTopFileList( array $params ) {
01182         return $this->getFileList( array( 'topOnly' => true ) + $params );
01183     }
01184 
01193     abstract public function preloadCache( array $paths );
01194 
01203     abstract public function clearCache( array $paths = null );
01204 
01217     abstract public function preloadFileStat( array $params );
01218 
01230     final public function lockFiles( array $paths, $type, $timeout = 0 ) {
01231         $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
01232 
01233         return $this->lockManager->lock( $paths, $type, $timeout );
01234     }
01235 
01243     final public function unlockFiles( array $paths, $type ) {
01244         $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
01245 
01246         return $this->lockManager->unlock( $paths, $type );
01247     }
01248 
01265     final public function getScopedFileLocks( array $paths, $type, Status $status, $timeout = 0 ) {
01266         if ( $type === 'mixed' ) {
01267             foreach ( $paths as &$typePaths ) {
01268                 $typePaths = array_map( 'FileBackend::normalizeStoragePath', $typePaths );
01269             }
01270         } else {
01271             $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
01272         }
01273 
01274         return ScopedLock::factory( $this->lockManager, $paths, $type, $status, $timeout );
01275     }
01276 
01293     abstract public function getScopedLocksForOps( array $ops, Status $status );
01294 
01302     final public function getRootStoragePath() {
01303         return "mwstore://{$this->name}";
01304     }
01305 
01313     final public function getContainerStoragePath( $container ) {
01314         return $this->getRootStoragePath() . "/{$container}";
01315     }
01316 
01322     final public function getJournal() {
01323         return $this->fileJournal;
01324     }
01325 
01333     final public static function isStoragePath( $path ) {
01334         return ( strpos( $path, 'mwstore://' ) === 0 );
01335     }
01336 
01345     final public static function splitStoragePath( $storagePath ) {
01346         if ( self::isStoragePath( $storagePath ) ) {
01347             // Remove the "mwstore://" prefix and split the path
01348             $parts = explode( '/', substr( $storagePath, 10 ), 3 );
01349             if ( count( $parts ) >= 2 && $parts[0] != '' && $parts[1] != '' ) {
01350                 if ( count( $parts ) == 3 ) {
01351                     return $parts; // e.g. "backend/container/path"
01352                 } else {
01353                     return array( $parts[0], $parts[1], '' ); // e.g. "backend/container"
01354                 }
01355             }
01356         }
01357 
01358         return array( null, null, null );
01359     }
01360 
01368     final public static function normalizeStoragePath( $storagePath ) {
01369         list( $backend, $container, $relPath ) = self::splitStoragePath( $storagePath );
01370         if ( $relPath !== null ) { // must be for this backend
01371             $relPath = self::normalizeContainerPath( $relPath );
01372             if ( $relPath !== null ) {
01373                 return ( $relPath != '' )
01374                     ? "mwstore://{$backend}/{$container}/{$relPath}"
01375                     : "mwstore://{$backend}/{$container}";
01376             }
01377         }
01378 
01379         return null;
01380     }
01381 
01390     final public static function parentStoragePath( $storagePath ) {
01391         $storagePath = dirname( $storagePath );
01392         list( , , $rel ) = self::splitStoragePath( $storagePath );
01393 
01394         return ( $rel === null ) ? null : $storagePath;
01395     }
01396 
01404     final public static function extensionFromPath( $path, $case = 'lowercase' ) {
01405         $i = strrpos( $path, '.' );
01406         $ext = $i ? substr( $path, $i + 1 ) : '';
01407 
01408         if ( $case === 'lowercase' ) {
01409             $ext = strtolower( $ext );
01410         } elseif ( $case === 'uppercase' ) {
01411             $ext = strtoupper( $ext );
01412         }
01413 
01414         return $ext;
01415     }
01416 
01424     final public static function isPathTraversalFree( $path ) {
01425         return ( self::normalizeContainerPath( $path ) !== null );
01426     }
01427 
01437     final public static function makeContentDisposition( $type, $filename = '' ) {
01438         $parts = array();
01439 
01440         $type = strtolower( $type );
01441         if ( !in_array( $type, array( 'inline', 'attachment' ) ) ) {
01442             throw new FileBackendError( "Invalid Content-Disposition type '$type'." );
01443         }
01444         $parts[] = $type;
01445 
01446         if ( strlen( $filename ) ) {
01447             $parts[] = "filename*=UTF-8''" . rawurlencode( basename( $filename ) );
01448         }
01449 
01450         return implode( ';', $parts );
01451     }
01452 
01463     final protected static function normalizeContainerPath( $path ) {
01464         // Normalize directory separators
01465         $path = strtr( $path, '\\', '/' );
01466         // Collapse any consecutive directory separators
01467         $path = preg_replace( '![/]{2,}!', '/', $path );
01468         // Remove any leading directory separator
01469         $path = ltrim( $path, '/' );
01470         // Use the same traversal protection as Title::secureAndSplit()
01471         if ( strpos( $path, '.' ) !== false ) {
01472             if (
01473                 $path === '.' ||
01474                 $path === '..' ||
01475                 strpos( $path, './' ) === 0 ||
01476                 strpos( $path, '../' ) === 0 ||
01477                 strpos( $path, '/./' ) !== false ||
01478                 strpos( $path, '/../' ) !== false
01479             ) {
01480                 return null;
01481             }
01482         }
01483 
01484         return $path;
01485     }
01486 }
01487 
01494 class FileBackendException extends MWException {
01495 }
01496 
01503 class FileBackendError extends FileBackendException {
01504 }