MediaWiki
REL1_24
|
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 }