MediaWiki  REL1_23
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;
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 }