MediaWiki  REL1_19
FSFileBackend.php
Go to the documentation of this file.
00001 <?php
00024 class FSFileBackend extends FileBackendStore {
00025         protected $basePath; // string; directory holding the container directories
00027         protected $containerPaths = array(); // for custom container paths
00028         protected $fileMode; // integer; file permission mode
00029 
00030         protected $hadWarningErrors = array();
00031 
00040         public function __construct( array $config ) {
00041                 parent::__construct( $config );
00042 
00043                 // Remove any possible trailing slash from directories
00044                 if ( isset( $config['basePath'] ) ) {
00045                         $this->basePath = rtrim( $config['basePath'], '/' ); // remove trailing slash
00046                 } else {
00047                         $this->basePath = null; // none; containers must have explicit paths
00048                 }
00049 
00050                 if ( isset( $config['containerPaths'] ) ) {
00051                         $this->containerPaths = (array)$config['containerPaths'];
00052                         foreach ( $this->containerPaths as &$path ) {
00053                                 $path = rtrim( $path, '/' );  // remove trailing slash
00054                         }
00055                 }
00056 
00057                 $this->fileMode = isset( $config['fileMode'] )
00058                         ? $config['fileMode']
00059                         : 0644;
00060         }
00061 
00065         protected function resolveContainerPath( $container, $relStoragePath ) {
00066                 // Check that container has a root directory
00067                 if ( isset( $this->containerPaths[$container] ) || isset( $this->basePath ) ) {
00068                         // Check for sane relative paths (assume the base paths are OK)
00069                         if ( $this->isLegalRelPath( $relStoragePath ) ) {
00070                                 return $relStoragePath;
00071                         }
00072                 }
00073                 return null;
00074         }
00075 
00082         protected function isLegalRelPath( $path ) {
00083                 // Check for file names longer than 255 chars
00084                 if ( preg_match( '![^/]{256}!', $path ) ) { // ext3/NTFS
00085                         return false;
00086                 }
00087                 if ( wfIsWindows() ) { // NTFS
00088                         return !preg_match( '![:*?"<>|]!', $path );
00089                 } else {
00090                         return true;
00091                 }
00092         }
00093 
00102         protected function containerFSRoot( $shortCont, $fullCont ) {
00103                 if ( isset( $this->containerPaths[$shortCont] ) ) {
00104                         return $this->containerPaths[$shortCont]; 
00105                 } elseif ( isset( $this->basePath ) ) {
00106                         return "{$this->basePath}/{$fullCont}";
00107                 }
00108                 return null; // no container base path defined
00109         }
00110 
00117         protected function resolveToFSPath( $storagePath ) {
00118                 list( $fullCont, $relPath ) = $this->resolveStoragePathReal( $storagePath );
00119                 if ( $relPath === null ) {
00120                         return null; // invalid
00121                 }
00122                 list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $storagePath );
00123                 $fsPath = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
00124                 if ( $relPath != '' ) {
00125                         $fsPath .= "/{$relPath}";
00126                 }
00127                 return $fsPath;
00128         }
00129 
00133         public function isPathUsableInternal( $storagePath ) {
00134                 $fsPath = $this->resolveToFSPath( $storagePath );
00135                 if ( $fsPath === null ) {
00136                         return false; // invalid
00137                 }
00138                 $parentDir = dirname( $fsPath );
00139 
00140                 if ( file_exists( $fsPath ) ) {
00141                         $ok = is_file( $fsPath ) && is_writable( $fsPath );
00142                 } else {
00143                         $ok = is_dir( $parentDir ) && is_writable( $parentDir );
00144                 }
00145 
00146                 return $ok;
00147         }
00148 
00152         protected function doStoreInternal( array $params ) {
00153                 $status = Status::newGood();
00154 
00155                 $dest = $this->resolveToFSPath( $params['dst'] );
00156                 if ( $dest === null ) {
00157                         $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
00158                         return $status;
00159                 }
00160 
00161                 if ( file_exists( $dest ) ) {
00162                         if ( !empty( $params['overwrite'] ) ) {
00163                                 $ok = unlink( $dest );
00164                                 if ( !$ok ) {
00165                                         $status->fatal( 'backend-fail-delete', $params['dst'] );
00166                                         return $status;
00167                                 }
00168                         } else {
00169                                 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
00170                                 return $status;
00171                         }
00172                 }
00173 
00174                 $ok = copy( $params['src'], $dest );
00175                 if ( !$ok ) {
00176                         $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
00177                         return $status;
00178                 }
00179 
00180                 $this->chmod( $dest );
00181 
00182                 return $status;
00183         }
00184 
00188         protected function doCopyInternal( array $params ) {
00189                 $status = Status::newGood();
00190 
00191                 $source = $this->resolveToFSPath( $params['src'] );
00192                 if ( $source === null ) {
00193                         $status->fatal( 'backend-fail-invalidpath', $params['src'] );
00194                         return $status;
00195                 }
00196 
00197                 $dest = $this->resolveToFSPath( $params['dst'] );
00198                 if ( $dest === null ) {
00199                         $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
00200                         return $status;
00201                 }
00202 
00203                 if ( file_exists( $dest ) ) {
00204                         if ( !empty( $params['overwrite'] ) ) {
00205                                 $ok = unlink( $dest );
00206                                 if ( !$ok ) {
00207                                         $status->fatal( 'backend-fail-delete', $params['dst'] );
00208                                         return $status;
00209                                 }
00210                         } else {
00211                                 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
00212                                 return $status;
00213                         }
00214                 }
00215 
00216                 $ok = copy( $source, $dest );
00217                 if ( !$ok ) {
00218                         $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
00219                         return $status;
00220                 }
00221 
00222                 $this->chmod( $dest );
00223 
00224                 return $status;
00225         }
00226 
00230         protected function doMoveInternal( array $params ) {
00231                 $status = Status::newGood();
00232 
00233                 $source = $this->resolveToFSPath( $params['src'] );
00234                 if ( $source === null ) {
00235                         $status->fatal( 'backend-fail-invalidpath', $params['src'] );
00236                         return $status;
00237                 }
00238 
00239                 $dest = $this->resolveToFSPath( $params['dst'] );
00240                 if ( $dest === null ) {
00241                         $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
00242                         return $status;
00243                 }
00244 
00245                 if ( file_exists( $dest ) ) {
00246                         if ( !empty( $params['overwrite'] ) ) {
00247                                 // Windows does not support moving over existing files
00248                                 if ( wfIsWindows() ) {
00249                                         $ok = unlink( $dest );
00250                                         if ( !$ok ) {
00251                                                 $status->fatal( 'backend-fail-delete', $params['dst'] );
00252                                                 return $status;
00253                                         }
00254                                 }
00255                         } else {
00256                                 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
00257                                 return $status;
00258                         }
00259                 }
00260 
00261                 $ok = rename( $source, $dest );
00262                 clearstatcache(); // file no longer at source
00263                 if ( !$ok ) {
00264                         $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
00265                         return $status;
00266                 }
00267 
00268                 return $status;
00269         }
00270 
00274         protected function doDeleteInternal( array $params ) {
00275                 $status = Status::newGood();
00276 
00277                 $source = $this->resolveToFSPath( $params['src'] );
00278                 if ( $source === null ) {
00279                         $status->fatal( 'backend-fail-invalidpath', $params['src'] );
00280                         return $status;
00281                 }
00282 
00283                 if ( !is_file( $source ) ) {
00284                         if ( empty( $params['ignoreMissingSource'] ) ) {
00285                                 $status->fatal( 'backend-fail-delete', $params['src'] );
00286                         }
00287                         return $status; // do nothing; either OK or bad status
00288                 }
00289 
00290                 $ok = unlink( $source );
00291                 if ( !$ok ) {
00292                         $status->fatal( 'backend-fail-delete', $params['src'] );
00293                         return $status;
00294                 }
00295 
00296                 return $status;
00297         }
00298 
00302         protected function doCreateInternal( array $params ) {
00303                 $status = Status::newGood();
00304 
00305                 $dest = $this->resolveToFSPath( $params['dst'] );
00306                 if ( $dest === null ) {
00307                         $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
00308                         return $status;
00309                 }
00310 
00311                 if ( file_exists( $dest ) ) {
00312                         if ( !empty( $params['overwrite'] ) ) {
00313                                 $ok = unlink( $dest );
00314                                 if ( !$ok ) {
00315                                         $status->fatal( 'backend-fail-delete', $params['dst'] );
00316                                         return $status;
00317                                 }
00318                         } else {
00319                                 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
00320                                 return $status;
00321                         }
00322                 }
00323 
00324                 $bytes = file_put_contents( $dest, $params['content'] );
00325                 if ( $bytes === false ) {
00326                         $status->fatal( 'backend-fail-create', $params['dst'] );
00327                         return $status;
00328                 }
00329 
00330                 $this->chmod( $dest );
00331 
00332                 return $status;
00333         }
00334 
00338         protected function doPrepareInternal( $fullCont, $dirRel, array $params ) {
00339                 $status = Status::newGood();
00340                 list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] );
00341                 $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
00342                 $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
00343                 if ( !wfMkdirParents( $dir ) ) { // make directory and its parents
00344                         $status->fatal( 'directorycreateerror', $params['dir'] );
00345                 } elseif ( !is_writable( $dir ) ) {
00346                         $status->fatal( 'directoryreadonlyerror', $params['dir'] );
00347                 } elseif ( !is_readable( $dir ) ) {
00348                         $status->fatal( 'directorynotreadableerror', $params['dir'] );
00349                 }
00350                 return $status;
00351         }
00352 
00356         protected function doSecureInternal( $fullCont, $dirRel, array $params ) {
00357                 $status = Status::newGood();
00358                 list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] );
00359                 $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
00360                 $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
00361                 // Seed new directories with a blank index.html, to prevent crawling...
00362                 if ( !empty( $params['noListing'] ) && !file_exists( "{$dir}/index.html" ) ) {
00363                         $bytes = file_put_contents( "{$dir}/index.html", '' );
00364                         if ( !$bytes ) {
00365                                 $status->fatal( 'backend-fail-create', $params['dir'] . '/index.html' );
00366                                 return $status;
00367                         }
00368                 }
00369                 // Add a .htaccess file to the root of the container...
00370                 if ( !empty( $params['noAccess'] ) ) {
00371                         if ( !file_exists( "{$contRoot}/.htaccess" ) ) {
00372                                 $bytes = file_put_contents( "{$contRoot}/.htaccess", "Deny from all\n" );
00373                                 if ( !$bytes ) {
00374                                         $storeDir = "mwstore://{$this->name}/{$shortCont}";
00375                                         $status->fatal( 'backend-fail-create', "{$storeDir}/.htaccess" );
00376                                         return $status;
00377                                 }
00378                         }
00379                 }
00380                 return $status;
00381         }
00382 
00386         protected function doCleanInternal( $fullCont, $dirRel, array $params ) {
00387                 $status = Status::newGood();
00388                 list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] );
00389                 $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
00390                 $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
00391                 wfSuppressWarnings();
00392                 if ( is_dir( $dir ) ) {
00393                         rmdir( $dir ); // remove directory if empty
00394                 }
00395                 wfRestoreWarnings();
00396                 return $status;
00397         }
00398 
00402         protected function doGetFileStat( array $params ) {
00403                 $source = $this->resolveToFSPath( $params['src'] );
00404                 if ( $source === null ) {
00405                         return false; // invalid storage path
00406                 }
00407 
00408                 $this->trapWarnings(); // don't trust 'false' if there were errors
00409                 $stat = is_file( $source ) ? stat( $source ) : false; // regular files only
00410                 $hadError = $this->untrapWarnings();
00411 
00412                 if ( $stat ) {
00413                         return array(
00414                                 'mtime' => wfTimestamp( TS_MW, $stat['mtime'] ),
00415                                 'size'  => $stat['size']
00416                         );
00417                 } elseif ( !$hadError ) {
00418                         return false; // file does not exist
00419                 } else {
00420                         return null; // failure
00421                 }
00422         }
00423 
00427         protected function doClearCache( array $paths = null ) {
00428                 clearstatcache(); // clear the PHP file stat cache
00429         }
00430 
00434         public function getFileListInternal( $fullCont, $dirRel, array $params ) {
00435                 list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] );
00436                 $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
00437                 $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
00438                 $exists = is_dir( $dir );
00439                 if ( !$exists ) {
00440                         wfDebug( __METHOD__ . "() given directory does not exist: '$dir'\n" );
00441                         return array(); // nothing under this dir
00442                 }
00443                 $readable = is_readable( $dir );
00444                 if ( !$readable ) {
00445                         wfDebug( __METHOD__ . "() given directory is unreadable: '$dir'\n" );
00446                         return null; // bad permissions?
00447                 }
00448                 return new FSFileBackendFileList( $dir );
00449         }
00450 
00454         public function getLocalReference( array $params ) {
00455                 $source = $this->resolveToFSPath( $params['src'] );
00456                 if ( $source === null ) {
00457                         return null;
00458                 }
00459                 return new FSFile( $source );
00460         }
00461 
00465         public function getLocalCopy( array $params ) {
00466                 $source = $this->resolveToFSPath( $params['src'] );
00467                 if ( $source === null ) {
00468                         return null;
00469                 }
00470 
00471                 // Create a new temporary file with the same extension...
00472                 $ext = FileBackend::extensionFromPath( $params['src'] );
00473                 $tmpFile = TempFSFile::factory( wfBaseName( $source ) . '_', $ext );
00474                 if ( !$tmpFile ) {
00475                         return null;
00476                 }
00477                 $tmpPath = $tmpFile->getPath();
00478 
00479                 // Copy the source file over the temp file
00480                 $ok = copy( $source, $tmpPath );
00481                 if ( !$ok ) {
00482                         return null;
00483                 }
00484 
00485                 $this->chmod( $tmpPath );
00486 
00487                 return $tmpFile;
00488         }
00489 
00496         protected function chmod( $path ) {
00497                 wfSuppressWarnings();
00498                 $ok = chmod( $path, $this->fileMode );
00499                 wfRestoreWarnings();
00500 
00501                 return $ok;
00502         }
00503 
00509         protected function trapWarnings() {
00510                 $this->hadWarningErrors[] = false; // push to stack
00511                 set_error_handler( array( $this, 'handleWarning' ), E_WARNING );
00512                 return false; // invoke normal PHP error handler
00513         }
00514 
00520         protected function untrapWarnings() {
00521                 restore_error_handler(); // restore previous handler
00522                 return array_pop( $this->hadWarningErrors ); // pop from stack
00523         }
00524 
00525         private function handleWarning() {
00526                 $this->hadWarningErrors[count( $this->hadWarningErrors ) - 1] = true;
00527                 return true; // suppress from PHP handler
00528         }
00529 }
00530 
00538 class FSFileBackendFileList implements Iterator {
00540         protected $iter;
00541         protected $suffixStart; // integer
00542         protected $pos = 0; // integer
00543 
00547         public function __construct( $dir ) {
00548                 $dir = realpath( $dir ); // normalize
00549                 $this->suffixStart = strlen( $dir ) + 1; // size of "path/to/dir/"
00550                 try {
00551                         # Get an iterator that will return leaf nodes (non-directories)
00552                         if ( MWInit::classExists( 'FilesystemIterator' ) ) { // PHP >= 5.3
00553                                 # RecursiveDirectoryIterator extends FilesystemIterator.
00554                                 # FilesystemIterator::SKIP_DOTS default is inconsistent in PHP 5.3.x.
00555                                 $flags = FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS;
00556                                 $this->iter = new RecursiveIteratorIterator( 
00557                                         new RecursiveDirectoryIterator( $dir, $flags ) );
00558                         } else { // PHP < 5.3
00559                                 # RecursiveDirectoryIterator extends DirectoryIterator
00560                                 $this->iter = new RecursiveIteratorIterator( 
00561                                         new RecursiveDirectoryIterator( $dir ) );
00562                         }
00563                 } catch ( UnexpectedValueException $e ) {
00564                         $this->iter = null; // bad permissions? deleted?
00565                 }
00566         }
00567 
00568         public function current() {
00569                 // Return only the relative path and normalize slashes to FileBackend-style
00570                 // Make sure to use the realpath since the suffix is based upon that
00571                 return str_replace( '\\', '/',
00572                         substr( realpath( $this->iter->current() ), $this->suffixStart ) );
00573         }
00574 
00575         public function key() {
00576                 return $this->pos;
00577         }
00578 
00579         public function next() {
00580                 try {
00581                         $this->iter->next();
00582                 } catch ( UnexpectedValueException $e ) {
00583                         $this->iter = null;
00584                 }
00585                 ++$this->pos;
00586         }
00587 
00588         public function rewind() {
00589                 $this->pos = 0;
00590                 try {
00591                         $this->iter->rewind();
00592                 } catch ( UnexpectedValueException $e ) {
00593                         $this->iter = null;
00594                 }
00595         }
00596 
00597         public function valid() {
00598                 return $this->iter && $this->iter->valid();
00599         }
00600 }