MediaWiki
REL1_22
|
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 {}