MediaWiki  REL1_22
FileBackend.php
Go to the documentation of this file.
00001 <?php
00062 abstract class FileBackend {
00063     protected $name; // string; unique backend name
00064     protected $wikiId; // string; unique wiki name
00065     protected $readOnly; // string; read-only explanation message
00066     protected $parallelize; // string; when to do operations in parallel
00067     protected $concurrency; // integer; how many operations can be done in parallel
00068 
00070     protected $lockManager;
00072     protected $fileJournal;
00073 
00100     public function __construct( array $config ) {
00101         $this->name = $config['name'];
00102         if ( !preg_match( '!^[a-zA-Z0-9-_]{1,255}$!', $this->name ) ) {
00103             throw new MWException( "Backend name `{$this->name}` is invalid." );
00104         }
00105         $this->wikiId = isset( $config['wikiId'] )
00106             ? $config['wikiId']
00107             : wfWikiID(); // e.g. "my_wiki-en_"
00108         $this->lockManager = ( $config['lockManager'] instanceof LockManager )
00109             ? $config['lockManager']
00110             : LockManagerGroup::singleton( $this->wikiId )->get( $config['lockManager'] );
00111         $this->fileJournal = isset( $config['fileJournal'] )
00112             ? ( ( $config['fileJournal'] instanceof FileJournal )
00113                 ? $config['fileJournal']
00114                 : FileJournal::factory( $config['fileJournal'], $this->name ) )
00115             : FileJournal::factory( array( 'class' => 'NullFileJournal' ), $this->name );
00116         $this->readOnly = isset( $config['readOnly'] )
00117             ? (string)$config['readOnly']
00118             : '';
00119         $this->parallelize = isset( $config['parallelize'] )
00120             ? (string)$config['parallelize']
00121             : 'off';
00122         $this->concurrency = isset( $config['concurrency'] )
00123             ? (int)$config['concurrency']
00124             : 50;
00125     }
00126 
00134     final public function getName() {
00135         return $this->name;
00136     }
00137 
00145     final public function getWikiId() {
00146         return $this->wikiId;
00147     }
00148 
00154     final public function isReadOnly() {
00155         return ( $this->readOnly != '' );
00156     }
00157 
00163     final public function getReadOnlyReason() {
00164         return ( $this->readOnly != '' ) ? $this->readOnly : false;
00165     }
00166 
00313     final public function doOperations( array $ops, array $opts = array() ) {
00314         if ( empty( $opts['bypassReadOnly'] ) && $this->isReadOnly() ) {
00315             return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
00316         }
00317         if ( !count( $ops ) ) {
00318             return Status::newGood(); // nothing to do
00319         }
00320         if ( empty( $opts['force'] ) ) { // sanity
00321             unset( $opts['nonLocking'] );
00322         }
00323         foreach ( $ops as &$op ) {
00324             if ( isset( $op['disposition'] ) ) { // b/c (MW 1.20)
00325                 $op['headers']['Content-Disposition'] = $op['disposition'];
00326             }
00327         }
00328         $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
00329         return $this->doOperationsInternal( $ops, $opts );
00330     }
00331 
00335     abstract protected function doOperationsInternal( array $ops, array $opts );
00336 
00348     final public function doOperation( array $op, array $opts = array() ) {
00349         return $this->doOperations( array( $op ), $opts );
00350     }
00351 
00362     final public function create( array $params, array $opts = array() ) {
00363         return $this->doOperation( array( 'op' => 'create' ) + $params, $opts );
00364     }
00365 
00376     final public function store( array $params, array $opts = array() ) {
00377         return $this->doOperation( array( 'op' => 'store' ) + $params, $opts );
00378     }
00379 
00390     final public function copy( array $params, array $opts = array() ) {
00391         return $this->doOperation( array( 'op' => 'copy' ) + $params, $opts );
00392     }
00393 
00404     final public function move( array $params, array $opts = array() ) {
00405         return $this->doOperation( array( 'op' => 'move' ) + $params, $opts );
00406     }
00407 
00418     final public function delete( array $params, array $opts = array() ) {
00419         return $this->doOperation( array( 'op' => 'delete' ) + $params, $opts );
00420     }
00421 
00433     final public function describe( array $params, array $opts = array() ) {
00434         return $this->doOperation( array( 'op' => 'describe' ) + $params, $opts );
00435     }
00436 
00547     final public function doQuickOperations( array $ops, array $opts = array() ) {
00548         if ( empty( $opts['bypassReadOnly'] ) && $this->isReadOnly() ) {
00549             return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
00550         }
00551         if ( !count( $ops ) ) {
00552             return Status::newGood(); // nothing to do
00553         }
00554         foreach ( $ops as &$op ) {
00555             $op['overwrite'] = true; // avoids RTTs in key/value stores
00556             if ( isset( $op['disposition'] ) ) { // b/c (MW 1.20)
00557                 $op['headers']['Content-Disposition'] = $op['disposition'];
00558             }
00559         }
00560         $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
00561         return $this->doQuickOperationsInternal( $ops );
00562     }
00563 
00568     abstract protected function doQuickOperationsInternal( array $ops );
00569 
00580     final public function doQuickOperation( array $op ) {
00581         return $this->doQuickOperations( array( $op ) );
00582     }
00583 
00594     final public function quickCreate( array $params ) {
00595         return $this->doQuickOperation( array( 'op' => 'create' ) + $params );
00596     }
00597 
00608     final public function quickStore( array $params ) {
00609         return $this->doQuickOperation( array( 'op' => 'store' ) + $params );
00610     }
00611 
00622     final public function quickCopy( array $params ) {
00623         return $this->doQuickOperation( array( 'op' => 'copy' ) + $params );
00624     }
00625 
00636     final public function quickMove( array $params ) {
00637         return $this->doQuickOperation( array( 'op' => 'move' ) + $params );
00638     }
00639 
00650     final public function quickDelete( array $params ) {
00651         return $this->doQuickOperation( array( 'op' => 'delete' ) + $params );
00652     }
00653 
00664     final public function quickDescribe( array $params ) {
00665         return $this->doQuickOperation( array( 'op' => 'describe' ) + $params );
00666     }
00667 
00681     abstract public function concatenate( array $params );
00682 
00702     final public function prepare( array $params ) {
00703         if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
00704             return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
00705         }
00706         $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
00707         return $this->doPrepare( $params );
00708     }
00709 
00713     abstract protected function doPrepare( array $params );
00714 
00732     final public function secure( array $params ) {
00733         if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
00734             return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
00735         }
00736         $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
00737         return $this->doSecure( $params );
00738     }
00739 
00743     abstract protected function doSecure( array $params );
00744 
00764     final public function publish( array $params ) {
00765         if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
00766             return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
00767         }
00768         $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
00769         return $this->doPublish( $params );
00770     }
00771 
00775     abstract protected function doPublish( array $params );
00776 
00789     final public function clean( array $params ) {
00790         if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
00791             return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
00792         }
00793         $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
00794         return $this->doClean( $params );
00795     }
00796 
00800     abstract protected function doClean( array $params );
00801 
00809     final protected function getScopedPHPBehaviorForOps() {
00810         if ( php_sapi_name() != 'cli' ) { // http://bugs.php.net/bug.php?id=47540
00811             $old = ignore_user_abort( true ); // avoid half-finished operations
00812             return new ScopedCallback( function() use ( $old ) {
00813                 ignore_user_abort( $old );
00814             } );
00815         }
00816         return null;
00817     }
00818 
00829     abstract public function fileExists( array $params );
00830 
00840     abstract public function getFileTimestamp( array $params );
00841 
00852     final public function getFileContents( array $params ) {
00853         $contents = $this->getFileContentsMulti(
00854             array( 'srcs' => array( $params['src'] ) ) + $params );
00855 
00856         return $contents[$params['src']];
00857     }
00858 
00874     abstract public function getFileContentsMulti( array $params );
00875 
00885     abstract public function getFileSize( array $params );
00886 
00901     abstract public function getFileStat( array $params );
00902 
00912     abstract public function getFileSha1Base36( array $params );
00913 
00924     abstract public function getFileProps( array $params );
00925 
00940     abstract public function streamFile( array $params );
00941 
00961     final public function getLocalReference( array $params ) {
00962         $fsFiles = $this->getLocalReferenceMulti(
00963             array( 'srcs' => array( $params['src'] ) ) + $params );
00964 
00965         return $fsFiles[$params['src']];
00966     }
00967 
00983     abstract public function getLocalReferenceMulti( array $params );
00984 
00996     final public function getLocalCopy( array $params ) {
00997         $tmpFiles = $this->getLocalCopyMulti(
00998             array( 'srcs' => array( $params['src'] ) ) + $params );
00999 
01000         return $tmpFiles[$params['src']];
01001     }
01002 
01018     abstract public function getLocalCopyMulti( array $params );
01019 
01037     abstract public function getFileHttpUrl( array $params );
01038 
01052     abstract public function directoryExists( array $params );
01053 
01073     abstract public function getDirectoryList( array $params );
01074 
01089     final public function getTopDirectoryList( array $params ) {
01090         return $this->getDirectoryList( array( 'topOnly' => true ) + $params );
01091     }
01092 
01112     abstract public function getFileList( array $params );
01113 
01129     final public function getTopFileList( array $params ) {
01130         return $this->getFileList( array( 'topOnly' => true ) + $params );
01131     }
01132 
01140     public function preloadCache( array $paths ) {}
01141 
01149     public function clearCache( array $paths = null ) {}
01150 
01161     final public function lockFiles( array $paths, $type ) {
01162         $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
01163         return $this->lockManager->lock( $paths, $type );
01164     }
01165 
01173     final public function unlockFiles( array $paths, $type ) {
01174         $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
01175         return $this->lockManager->unlock( $paths, $type );
01176     }
01177 
01193     final public function getScopedFileLocks( array $paths, $type, Status $status ) {
01194         if ( $type === 'mixed' ) {
01195             foreach ( $paths as &$typePaths ) {
01196                 $typePaths = array_map( 'FileBackend::normalizeStoragePath', $typePaths );
01197             }
01198         } else {
01199             $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
01200         }
01201         return ScopedLock::factory( $this->lockManager, $paths, $type, $status );
01202     }
01203 
01220     abstract public function getScopedLocksForOps( array $ops, Status $status );
01221 
01229     final public function getRootStoragePath() {
01230         return "mwstore://{$this->name}";
01231     }
01232 
01240     final public function getContainerStoragePath( $container ) {
01241         return $this->getRootStoragePath() . "/{$container}";
01242     }
01243 
01249     final public function getJournal() {
01250         return $this->fileJournal;
01251     }
01252 
01260     final public static function isStoragePath( $path ) {
01261         return ( strpos( $path, 'mwstore://' ) === 0 );
01262     }
01263 
01272     final public static function splitStoragePath( $storagePath ) {
01273         if ( self::isStoragePath( $storagePath ) ) {
01274             // Remove the "mwstore://" prefix and split the path
01275             $parts = explode( '/', substr( $storagePath, 10 ), 3 );
01276             if ( count( $parts ) >= 2 && $parts[0] != '' && $parts[1] != '' ) {
01277                 if ( count( $parts ) == 3 ) {
01278                     return $parts; // e.g. "backend/container/path"
01279                 } else {
01280                     return array( $parts[0], $parts[1], '' ); // e.g. "backend/container"
01281                 }
01282             }
01283         }
01284         return array( null, null, null );
01285     }
01286 
01294     final public static function normalizeStoragePath( $storagePath ) {
01295         list( $backend, $container, $relPath ) = self::splitStoragePath( $storagePath );
01296         if ( $relPath !== null ) { // must be for this backend
01297             $relPath = self::normalizeContainerPath( $relPath );
01298             if ( $relPath !== null ) {
01299                 return ( $relPath != '' )
01300                     ? "mwstore://{$backend}/{$container}/{$relPath}"
01301                     : "mwstore://{$backend}/{$container}";
01302             }
01303         }
01304         return null;
01305     }
01306 
01315     final public static function parentStoragePath( $storagePath ) {
01316         $storagePath = dirname( $storagePath );
01317         list( , , $rel ) = self::splitStoragePath( $storagePath );
01318         return ( $rel === null ) ? null : $storagePath;
01319     }
01320 
01327     final public static function extensionFromPath( $path ) {
01328         $i = strrpos( $path, '.' );
01329         return strtolower( $i ? substr( $path, $i + 1 ) : '' );
01330     }
01331 
01339     final public static function isPathTraversalFree( $path ) {
01340         return ( self::normalizeContainerPath( $path ) !== null );
01341     }
01342 
01352     final public static function makeContentDisposition( $type, $filename = '' ) {
01353         $parts = array();
01354 
01355         $type = strtolower( $type );
01356         if ( !in_array( $type, array( 'inline', 'attachment' ) ) ) {
01357             throw new MWException( "Invalid Content-Disposition type '$type'." );
01358         }
01359         $parts[] = $type;
01360 
01361         if ( strlen( $filename ) ) {
01362             $parts[] = "filename*=UTF-8''" . rawurlencode( basename( $filename ) );
01363         }
01364 
01365         return implode( ';', $parts );
01366     }
01367 
01378     final protected static function normalizeContainerPath( $path ) {
01379         // Normalize directory separators
01380         $path = strtr( $path, '\\', '/' );
01381         // Collapse any consecutive directory separators
01382         $path = preg_replace( '![/]{2,}!', '/', $path );
01383         // Remove any leading directory separator
01384         $path = ltrim( $path, '/' );
01385         // Use the same traversal protection as Title::secureAndSplit()
01386         if ( strpos( $path, '.' ) !== false ) {
01387             if (
01388                 $path === '.' ||
01389                 $path === '..' ||
01390                 strpos( $path, './' ) === 0 ||
01391                 strpos( $path, '../' ) === 0 ||
01392                 strpos( $path, '/./' ) !== false ||
01393                 strpos( $path, '/../' ) !== false
01394             ) {
01395                 return null;
01396             }
01397         }
01398         return $path;
01399     }
01400 }
01401 
01406 class FileBackendError extends MWException {}