MediaWiki  REL1_22
FSFileBackend.php
Go to the documentation of this file.
00001 <?php
00041 class FSFileBackend extends FileBackendStore {
00042     protected $basePath; // string; directory holding the container directories
00044     protected $containerPaths = array(); // for custom container paths
00045     protected $fileMode; // integer; file permission mode
00046     protected $fileOwner; // string; required OS username to own files
00047     protected $currentUser; // string; OS username running this script
00048 
00050     protected $hadWarningErrors = array();
00051 
00060     public function __construct( array $config ) {
00061         parent::__construct( $config );
00062 
00063         // Remove any possible trailing slash from directories
00064         if ( isset( $config['basePath'] ) ) {
00065             $this->basePath = rtrim( $config['basePath'], '/' ); // remove trailing slash
00066         } else {
00067             $this->basePath = null; // none; containers must have explicit paths
00068         }
00069 
00070         if ( isset( $config['containerPaths'] ) ) {
00071             $this->containerPaths = (array)$config['containerPaths'];
00072             foreach ( $this->containerPaths as &$path ) {
00073                 $path = rtrim( $path, '/' ); // remove trailing slash
00074             }
00075         }
00076 
00077         $this->fileMode = isset( $config['fileMode'] ) ? $config['fileMode'] : 0644;
00078         if ( isset( $config['fileOwner'] ) && function_exists( 'posix_getuid' ) ) {
00079             $this->fileOwner = $config['fileOwner'];
00080             $info = posix_getpwuid( posix_getuid() );
00081             $this->currentUser = $info['name']; // cache this, assuming it doesn't change
00082         }
00083     }
00084 
00085     protected function resolveContainerPath( $container, $relStoragePath ) {
00086         // Check that container has a root directory
00087         if ( isset( $this->containerPaths[$container] ) || isset( $this->basePath ) ) {
00088             // Check for sane relative paths (assume the base paths are OK)
00089             if ( $this->isLegalRelPath( $relStoragePath ) ) {
00090                 return $relStoragePath;
00091             }
00092         }
00093         return null;
00094     }
00095 
00102     protected function isLegalRelPath( $path ) {
00103         // Check for file names longer than 255 chars
00104         if ( preg_match( '![^/]{256}!', $path ) ) { // ext3/NTFS
00105             return false;
00106         }
00107         if ( wfIsWindows() ) { // NTFS
00108             return !preg_match( '![:*?"<>|]!', $path );
00109         } else {
00110             return true;
00111         }
00112     }
00113 
00122     protected function containerFSRoot( $shortCont, $fullCont ) {
00123         if ( isset( $this->containerPaths[$shortCont] ) ) {
00124             return $this->containerPaths[$shortCont];
00125         } elseif ( isset( $this->basePath ) ) {
00126             return "{$this->basePath}/{$fullCont}";
00127         }
00128         return null; // no container base path defined
00129     }
00130 
00137     protected function resolveToFSPath( $storagePath ) {
00138         list( $fullCont, $relPath ) = $this->resolveStoragePathReal( $storagePath );
00139         if ( $relPath === null ) {
00140             return null; // invalid
00141         }
00142         list( , $shortCont, ) = FileBackend::splitStoragePath( $storagePath );
00143         $fsPath = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
00144         if ( $relPath != '' ) {
00145             $fsPath .= "/{$relPath}";
00146         }
00147         return $fsPath;
00148     }
00149 
00150     public function isPathUsableInternal( $storagePath ) {
00151         $fsPath = $this->resolveToFSPath( $storagePath );
00152         if ( $fsPath === null ) {
00153             return false; // invalid
00154         }
00155         $parentDir = dirname( $fsPath );
00156 
00157         if ( file_exists( $fsPath ) ) {
00158             $ok = is_file( $fsPath ) && is_writable( $fsPath );
00159         } else {
00160             $ok = is_dir( $parentDir ) && is_writable( $parentDir );
00161         }
00162 
00163         if ( $this->fileOwner !== null && $this->currentUser !== $this->fileOwner ) {
00164             $ok = false;
00165             trigger_error( __METHOD__ . ": PHP process owner is not '{$this->fileOwner}'." );
00166         }
00167 
00168         return $ok;
00169     }
00170 
00171     protected function doCreateInternal( array $params ) {
00172         $status = Status::newGood();
00173 
00174         $dest = $this->resolveToFSPath( $params['dst'] );
00175         if ( $dest === null ) {
00176             $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
00177             return $status;
00178         }
00179 
00180         if ( !empty( $params['async'] ) ) { // deferred
00181             $tempFile = TempFSFile::factory( 'create_', 'tmp' );
00182             if ( !$tempFile ) {
00183                 $status->fatal( 'backend-fail-create', $params['dst'] );
00184                 return $status;
00185             }
00186             $this->trapWarnings();
00187             $bytes = file_put_contents( $tempFile->getPath(), $params['content'] );
00188             $this->untrapWarnings();
00189             if ( $bytes === false ) {
00190                 $status->fatal( 'backend-fail-create', $params['dst'] );
00191                 return $status;
00192             }
00193             $cmd = implode( ' ', array(
00194                 wfIsWindows() ? 'COPY /B /Y' : 'cp', // (binary, overwrite)
00195                 wfEscapeShellArg( $this->cleanPathSlashes( $tempFile->getPath() ) ),
00196                 wfEscapeShellArg( $this->cleanPathSlashes( $dest ) )
00197             ) );
00198             $status->value = new FSFileOpHandle( $this, $params, 'Create', $cmd, $dest );
00199             $tempFile->bind( $status->value );
00200         } else { // immediate write
00201             $this->trapWarnings();
00202             $bytes = file_put_contents( $dest, $params['content'] );
00203             $this->untrapWarnings();
00204             if ( $bytes === false ) {
00205                 $status->fatal( 'backend-fail-create', $params['dst'] );
00206                 return $status;
00207             }
00208             $this->chmod( $dest );
00209         }
00210 
00211         return $status;
00212     }
00213 
00217     protected function _getResponseCreate( $errors, Status $status, array $params, $cmd ) {
00218         if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
00219             $status->fatal( 'backend-fail-create', $params['dst'] );
00220             trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
00221         }
00222     }
00223 
00224     protected function doStoreInternal( array $params ) {
00225         $status = Status::newGood();
00226 
00227         $dest = $this->resolveToFSPath( $params['dst'] );
00228         if ( $dest === null ) {
00229             $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
00230             return $status;
00231         }
00232 
00233         if ( !empty( $params['async'] ) ) { // deferred
00234             $cmd = implode( ' ', array(
00235                 wfIsWindows() ? 'COPY /B /Y' : 'cp', // (binary, overwrite)
00236                 wfEscapeShellArg( $this->cleanPathSlashes( $params['src'] ) ),
00237                 wfEscapeShellArg( $this->cleanPathSlashes( $dest ) )
00238             ) );
00239             $status->value = new FSFileOpHandle( $this, $params, 'Store', $cmd, $dest );
00240         } else { // immediate write
00241             $this->trapWarnings();
00242             $ok = copy( $params['src'], $dest );
00243             $this->untrapWarnings();
00244             // In some cases (at least over NFS), copy() returns true when it fails
00245             if ( !$ok || ( filesize( $params['src'] ) !== filesize( $dest ) ) ) {
00246                 if ( $ok ) { // PHP bug
00247                     unlink( $dest ); // remove broken file
00248                     trigger_error( __METHOD__ . ": copy() failed but returned true." );
00249                 }
00250                 $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
00251                 return $status;
00252             }
00253             $this->chmod( $dest );
00254         }
00255 
00256         return $status;
00257     }
00258 
00262     protected function _getResponseStore( $errors, Status $status, array $params, $cmd ) {
00263         if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
00264             $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
00265             trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
00266         }
00267     }
00268 
00269     protected function doCopyInternal( array $params ) {
00270         $status = Status::newGood();
00271 
00272         $source = $this->resolveToFSPath( $params['src'] );
00273         if ( $source === null ) {
00274             $status->fatal( 'backend-fail-invalidpath', $params['src'] );
00275             return $status;
00276         }
00277 
00278         $dest = $this->resolveToFSPath( $params['dst'] );
00279         if ( $dest === null ) {
00280             $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
00281             return $status;
00282         }
00283 
00284         if ( !is_file( $source ) ) {
00285             if ( empty( $params['ignoreMissingSource'] ) ) {
00286                 $status->fatal( 'backend-fail-copy', $params['src'] );
00287             }
00288             return $status; // do nothing; either OK or bad status
00289         }
00290 
00291         if ( !empty( $params['async'] ) ) { // deferred
00292             $cmd = implode( ' ', array(
00293                 wfIsWindows() ? 'COPY /B /Y' : 'cp', // (binary, overwrite)
00294                 wfEscapeShellArg( $this->cleanPathSlashes( $source ) ),
00295                 wfEscapeShellArg( $this->cleanPathSlashes( $dest ) )
00296             ) );
00297             $status->value = new FSFileOpHandle( $this, $params, 'Copy', $cmd, $dest );
00298         } else { // immediate write
00299             $this->trapWarnings();
00300             $ok = ( $source === $dest ) ? true : copy( $source, $dest );
00301             $this->untrapWarnings();
00302             // In some cases (at least over NFS), copy() returns true when it fails
00303             if ( !$ok || ( filesize( $source ) !== filesize( $dest ) ) ) {
00304                 if ( $ok ) { // PHP bug
00305                     $this->trapWarnings();
00306                     unlink( $dest ); // remove broken file
00307                     $this->untrapWarnings();
00308                     trigger_error( __METHOD__ . ": copy() failed but returned true." );
00309                 }
00310                 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
00311                 return $status;
00312             }
00313             $this->chmod( $dest );
00314         }
00315 
00316         return $status;
00317     }
00318 
00322     protected function _getResponseCopy( $errors, Status $status, array $params, $cmd ) {
00323         if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
00324             $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
00325             trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
00326         }
00327     }
00328 
00329     protected function doMoveInternal( array $params ) {
00330         $status = Status::newGood();
00331 
00332         $source = $this->resolveToFSPath( $params['src'] );
00333         if ( $source === null ) {
00334             $status->fatal( 'backend-fail-invalidpath', $params['src'] );
00335             return $status;
00336         }
00337 
00338         $dest = $this->resolveToFSPath( $params['dst'] );
00339         if ( $dest === null ) {
00340             $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
00341             return $status;
00342         }
00343 
00344         if ( !is_file( $source ) ) {
00345             if ( empty( $params['ignoreMissingSource'] ) ) {
00346                 $status->fatal( 'backend-fail-move', $params['src'] );
00347             }
00348             return $status; // do nothing; either OK or bad status
00349         }
00350 
00351         if ( !empty( $params['async'] ) ) { // deferred
00352             $cmd = implode( ' ', array(
00353                 wfIsWindows() ? 'MOVE /Y' : 'mv', // (overwrite)
00354                 wfEscapeShellArg( $this->cleanPathSlashes( $source ) ),
00355                 wfEscapeShellArg( $this->cleanPathSlashes( $dest ) )
00356             ) );
00357             $status->value = new FSFileOpHandle( $this, $params, 'Move', $cmd );
00358         } else { // immediate write
00359             $this->trapWarnings();
00360             $ok = ( $source === $dest ) ? true : rename( $source, $dest );
00361             $this->untrapWarnings();
00362             clearstatcache(); // file no longer at source
00363             if ( !$ok ) {
00364                 $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
00365                 return $status;
00366             }
00367         }
00368 
00369         return $status;
00370     }
00371 
00375     protected function _getResponseMove( $errors, Status $status, array $params, $cmd ) {
00376         if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
00377             $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
00378             trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
00379         }
00380     }
00381 
00382     protected function doDeleteInternal( array $params ) {
00383         $status = Status::newGood();
00384 
00385         $source = $this->resolveToFSPath( $params['src'] );
00386         if ( $source === null ) {
00387             $status->fatal( 'backend-fail-invalidpath', $params['src'] );
00388             return $status;
00389         }
00390 
00391         if ( !is_file( $source ) ) {
00392             if ( empty( $params['ignoreMissingSource'] ) ) {
00393                 $status->fatal( 'backend-fail-delete', $params['src'] );
00394             }
00395             return $status; // do nothing; either OK or bad status
00396         }
00397 
00398         if ( !empty( $params['async'] ) ) { // deferred
00399             $cmd = implode( ' ', array(
00400                 wfIsWindows() ? 'DEL' : 'unlink',
00401                 wfEscapeShellArg( $this->cleanPathSlashes( $source ) )
00402             ) );
00403             $status->value = new FSFileOpHandle( $this, $params, 'Copy', $cmd );
00404         } else { // immediate write
00405             $this->trapWarnings();
00406             $ok = unlink( $source );
00407             $this->untrapWarnings();
00408             if ( !$ok ) {
00409                 $status->fatal( 'backend-fail-delete', $params['src'] );
00410                 return $status;
00411             }
00412         }
00413 
00414         return $status;
00415     }
00416 
00420     protected function _getResponseDelete( $errors, Status $status, array $params, $cmd ) {
00421         if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
00422             $status->fatal( 'backend-fail-delete', $params['src'] );
00423             trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
00424         }
00425     }
00426 
00427     protected function doPrepareInternal( $fullCont, $dirRel, array $params ) {
00428         $status = Status::newGood();
00429         list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
00430         $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
00431         $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
00432         $existed = is_dir( $dir ); // already there?
00433         // Create the directory and its parents as needed...
00434         $this->trapWarnings();
00435         if ( !wfMkdirParents( $dir ) ) {
00436             $status->fatal( 'directorycreateerror', $params['dir'] ); // fails on races
00437         } elseif ( !is_writable( $dir ) ) {
00438             $status->fatal( 'directoryreadonlyerror', $params['dir'] );
00439         } elseif ( !is_readable( $dir ) ) {
00440             $status->fatal( 'directorynotreadableerror', $params['dir'] );
00441         }
00442         $this->untrapWarnings();
00443         // Respect any 'noAccess' or 'noListing' flags...
00444         if ( is_dir( $dir ) && !$existed ) {
00445             $status->merge( $this->doSecureInternal( $fullCont, $dirRel, $params ) );
00446         }
00447         return $status;
00448     }
00449 
00450     protected function doSecureInternal( $fullCont, $dirRel, array $params ) {
00451         $status = Status::newGood();
00452         list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
00453         $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
00454         $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
00455         // Seed new directories with a blank index.html, to prevent crawling...
00456         if ( !empty( $params['noListing'] ) && !file_exists( "{$dir}/index.html" ) ) {
00457             $this->trapWarnings();
00458             $bytes = file_put_contents( "{$dir}/index.html", $this->indexHtmlPrivate() );
00459             $this->untrapWarnings();
00460             if ( $bytes === false ) {
00461                 $status->fatal( 'backend-fail-create', $params['dir'] . '/index.html' );
00462             }
00463         }
00464         // Add a .htaccess file to the root of the container...
00465         if ( !empty( $params['noAccess'] ) && !file_exists( "{$contRoot}/.htaccess" ) ) {
00466             $this->trapWarnings();
00467             $bytes = file_put_contents( "{$contRoot}/.htaccess", $this->htaccessPrivate() );
00468             $this->untrapWarnings();
00469             if ( $bytes === false ) {
00470                 $storeDir = "mwstore://{$this->name}/{$shortCont}";
00471                 $status->fatal( 'backend-fail-create', "{$storeDir}/.htaccess" );
00472             }
00473         }
00474         return $status;
00475     }
00476 
00477     protected function doPublishInternal( $fullCont, $dirRel, array $params ) {
00478         $status = Status::newGood();
00479         list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
00480         $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
00481         $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
00482         // Unseed new directories with a blank index.html, to allow crawling...
00483         if ( !empty( $params['listing'] ) && is_file( "{$dir}/index.html" ) ) {
00484             $exists = ( file_get_contents( "{$dir}/index.html" ) === $this->indexHtmlPrivate() );
00485             $this->trapWarnings();
00486             if ( $exists && !unlink( "{$dir}/index.html" ) ) { // reverse secure()
00487                 $status->fatal( 'backend-fail-delete', $params['dir'] . '/index.html' );
00488             }
00489             $this->untrapWarnings();
00490         }
00491         // Remove the .htaccess file from the root of the container...
00492         if ( !empty( $params['access'] ) && is_file( "{$contRoot}/.htaccess" ) ) {
00493             $exists = ( file_get_contents( "{$contRoot}/.htaccess" ) === $this->htaccessPrivate() );
00494             $this->trapWarnings();
00495             if ( $exists && !unlink( "{$contRoot}/.htaccess" ) ) { // reverse secure()
00496                 $storeDir = "mwstore://{$this->name}/{$shortCont}";
00497                 $status->fatal( 'backend-fail-delete', "{$storeDir}/.htaccess" );
00498             }
00499             $this->untrapWarnings();
00500         }
00501         return $status;
00502     }
00503 
00504     protected function doCleanInternal( $fullCont, $dirRel, array $params ) {
00505         $status = Status::newGood();
00506         list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
00507         $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
00508         $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
00509         $this->trapWarnings();
00510         if ( is_dir( $dir ) ) {
00511             rmdir( $dir ); // remove directory if empty
00512         }
00513         $this->untrapWarnings();
00514         return $status;
00515     }
00516 
00517     protected function doGetFileStat( array $params ) {
00518         $source = $this->resolveToFSPath( $params['src'] );
00519         if ( $source === null ) {
00520             return false; // invalid storage path
00521         }
00522 
00523         $this->trapWarnings(); // don't trust 'false' if there were errors
00524         $stat = is_file( $source ) ? stat( $source ) : false; // regular files only
00525         $hadError = $this->untrapWarnings();
00526 
00527         if ( $stat ) {
00528             return array(
00529                 'mtime' => wfTimestamp( TS_MW, $stat['mtime'] ),
00530                 'size' => $stat['size']
00531             );
00532         } elseif ( !$hadError ) {
00533             return false; // file does not exist
00534         } else {
00535             return null; // failure
00536         }
00537     }
00538 
00542     protected function doClearCache( array $paths = null ) {
00543         clearstatcache(); // clear the PHP file stat cache
00544     }
00545 
00546     protected function doDirectoryExists( $fullCont, $dirRel, array $params ) {
00547         list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
00548         $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
00549         $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
00550 
00551         $this->trapWarnings(); // don't trust 'false' if there were errors
00552         $exists = is_dir( $dir );
00553         $hadError = $this->untrapWarnings();
00554 
00555         return $hadError ? null : $exists;
00556     }
00557 
00562     public function getDirectoryListInternal( $fullCont, $dirRel, array $params ) {
00563         list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
00564         $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
00565         $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
00566         $exists = is_dir( $dir );
00567         if ( !$exists ) {
00568             wfDebug( __METHOD__ . "() given directory does not exist: '$dir'\n" );
00569             return array(); // nothing under this dir
00570         } elseif ( !is_readable( $dir ) ) {
00571             wfDebug( __METHOD__ . "() given directory is unreadable: '$dir'\n" );
00572             return null; // bad permissions?
00573         }
00574         return new FSFileBackendDirList( $dir, $params );
00575     }
00576 
00581     public function getFileListInternal( $fullCont, $dirRel, array $params ) {
00582         list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
00583         $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
00584         $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
00585         $exists = is_dir( $dir );
00586         if ( !$exists ) {
00587             wfDebug( __METHOD__ . "() given directory does not exist: '$dir'\n" );
00588             return array(); // nothing under this dir
00589         } elseif ( !is_readable( $dir ) ) {
00590             wfDebug( __METHOD__ . "() given directory is unreadable: '$dir'\n" );
00591             return null; // bad permissions?
00592         }
00593         return new FSFileBackendFileList( $dir, $params );
00594     }
00595 
00596     protected function doGetLocalReferenceMulti( array $params ) {
00597         $fsFiles = array(); // (path => FSFile)
00598 
00599         foreach ( $params['srcs'] as $src ) {
00600             $source = $this->resolveToFSPath( $src );
00601             if ( $source === null || !is_file( $source ) ) {
00602                 $fsFiles[$src] = null; // invalid path or file does not exist
00603             } else {
00604                 $fsFiles[$src] = new FSFile( $source );
00605             }
00606         }
00607 
00608         return $fsFiles;
00609     }
00610 
00611     protected function doGetLocalCopyMulti( array $params ) {
00612         $tmpFiles = array(); // (path => TempFSFile)
00613 
00614         foreach ( $params['srcs'] as $src ) {
00615             $source = $this->resolveToFSPath( $src );
00616             if ( $source === null ) {
00617                 $tmpFiles[$src] = null; // invalid path
00618             } else {
00619                 // Create a new temporary file with the same extension...
00620                 $ext = FileBackend::extensionFromPath( $src );
00621                 $tmpFile = TempFSFile::factory( 'localcopy_', $ext );
00622                 if ( !$tmpFile ) {
00623                     $tmpFiles[$src] = null;
00624                 } else {
00625                     $tmpPath = $tmpFile->getPath();
00626                     // Copy the source file over the temp file
00627                     $this->trapWarnings();
00628                     $ok = copy( $source, $tmpPath );
00629                     $this->untrapWarnings();
00630                     if ( !$ok ) {
00631                         $tmpFiles[$src] = null;
00632                     } else {
00633                         $this->chmod( $tmpPath );
00634                         $tmpFiles[$src] = $tmpFile;
00635                     }
00636                 }
00637             }
00638         }
00639 
00640         return $tmpFiles;
00641     }
00642 
00643     protected function directoriesAreVirtual() {
00644         return false;
00645     }
00646 
00647     protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
00648         $statuses = array();
00649 
00650         $pipes = array();
00651         foreach ( $fileOpHandles as $index => $fileOpHandle ) {
00652             $pipes[$index] = popen( "{$fileOpHandle->cmd} 2>&1", 'r' );
00653         }
00654 
00655         $errs = array();
00656         foreach ( $pipes as $index => $pipe ) {
00657             // Result will be empty on success in *NIX. On Windows,
00658             // it may be something like "        1 file(s) [copied|moved].".
00659             $errs[$index] = stream_get_contents( $pipe );
00660             fclose( $pipe );
00661         }
00662 
00663         foreach ( $fileOpHandles as $index => $fileOpHandle ) {
00664             $status = Status::newGood();
00665             $function = '_getResponse' . $fileOpHandle->call;
00666             $this->$function( $errs[$index], $status, $fileOpHandle->params, $fileOpHandle->cmd );
00667             $statuses[$index] = $status;
00668             if ( $status->isOK() && $fileOpHandle->chmodPath ) {
00669                 $this->chmod( $fileOpHandle->chmodPath );
00670             }
00671         }
00672 
00673         clearstatcache(); // files changed
00674         return $statuses;
00675     }
00676 
00683     protected function chmod( $path ) {
00684         $this->trapWarnings();
00685         $ok = chmod( $path, $this->fileMode );
00686         $this->untrapWarnings();
00687 
00688         return $ok;
00689     }
00690 
00696     protected function indexHtmlPrivate() {
00697         return '';
00698     }
00699 
00705     protected function htaccessPrivate() {
00706         return "Deny from all\n";
00707     }
00708 
00715     protected function cleanPathSlashes( $path ) {
00716         return wfIsWindows() ? strtr( $path, '/', '\\' ) : $path;
00717     }
00718 
00724     protected function trapWarnings() {
00725         $this->hadWarningErrors[] = false; // push to stack
00726         set_error_handler( array( $this, 'handleWarning' ), E_WARNING );
00727     }
00728 
00734     protected function untrapWarnings() {
00735         restore_error_handler(); // restore previous handler
00736         return array_pop( $this->hadWarningErrors ); // pop from stack
00737     }
00738 
00745     public function handleWarning( $errno, $errstr ) {
00746         wfDebugLog( 'FSFileBackend', $errstr ); // more detailed error logging
00747         $this->hadWarningErrors[count( $this->hadWarningErrors ) - 1] = true;
00748         return true; // suppress from PHP handler
00749     }
00750 }
00751 
00755 class FSFileOpHandle extends FileBackendStoreOpHandle {
00756     public $cmd; // string; shell command
00757     public $chmodPath; // string; file to chmod
00758 
00766     public function __construct(
00767         FSFileBackend $backend, array $params, $call, $cmd, $chmodPath = null
00768     ) {
00769         $this->backend = $backend;
00770         $this->params = $params;
00771         $this->call = $call;
00772         $this->cmd = $cmd;
00773         $this->chmodPath = $chmodPath;
00774     }
00775 }
00776 
00784 abstract class FSFileBackendList implements Iterator {
00786     protected $iter;
00787     protected $suffixStart; // integer
00788     protected $pos = 0; // integer
00790     protected $params = array();
00791 
00796     public function __construct( $dir, array $params ) {
00797         $path = realpath( $dir ); // normalize
00798         if ( $path === false ) {
00799             $path = $dir;
00800         }
00801         $this->suffixStart = strlen( $path ) + 1; // size of "path/to/dir/"
00802         $this->params = $params;
00803 
00804         try {
00805             $this->iter = $this->initIterator( $path );
00806         } catch ( UnexpectedValueException $e ) {
00807             $this->iter = null; // bad permissions? deleted?
00808         }
00809     }
00810 
00817     protected function initIterator( $dir ) {
00818         if ( !empty( $this->params['topOnly'] ) ) { // non-recursive
00819             # Get an iterator that will get direct sub-nodes
00820             return new DirectoryIterator( $dir );
00821         } else { // recursive
00822             # Get an iterator that will return leaf nodes (non-directories)
00823             # RecursiveDirectoryIterator extends FilesystemIterator.
00824             # FilesystemIterator::SKIP_DOTS default is inconsistent in PHP 5.3.x.
00825             $flags = FilesystemIterator::CURRENT_AS_SELF | FilesystemIterator::SKIP_DOTS;
00826             return new RecursiveIteratorIterator(
00827                 new RecursiveDirectoryIterator( $dir, $flags ),
00828                 RecursiveIteratorIterator::CHILD_FIRST // include dirs
00829             );
00830         }
00831     }
00832 
00837     public function key() {
00838         return $this->pos;
00839     }
00840 
00845     public function current() {
00846         return $this->getRelPath( $this->iter->current()->getPathname() );
00847     }
00848 
00853     public function next() {
00854         try {
00855             $this->iter->next();
00856             $this->filterViaNext();
00857         } catch ( UnexpectedValueException $e ) { // bad permissions? deleted?
00858             throw new FileBackendError( "File iterator gave UnexpectedValueException." );
00859         }
00860         ++$this->pos;
00861     }
00862 
00867     public function rewind() {
00868         $this->pos = 0;
00869         try {
00870             $this->iter->rewind();
00871             $this->filterViaNext();
00872         } catch ( UnexpectedValueException $e ) { // bad permissions? deleted?
00873             throw new FileBackendError( "File iterator gave UnexpectedValueException." );
00874         }
00875     }
00876 
00881     public function valid() {
00882         return $this->iter && $this->iter->valid();
00883     }
00884 
00888     protected function filterViaNext() {}
00889 
00897     protected function getRelPath( $dir ) {
00898         $path = realpath( $dir );
00899         if ( $path === false ) {
00900             $path = $dir;
00901         }
00902         return strtr( substr( $path, $this->suffixStart ), '\\', '/' );
00903     }
00904 }
00905 
00906 class FSFileBackendDirList extends FSFileBackendList {
00907     protected function filterViaNext() {
00908         while ( $this->iter->valid() ) {
00909             if ( $this->iter->current()->isDot() || !$this->iter->current()->isDir() ) {
00910                 $this->iter->next(); // skip non-directories and dot files
00911             } else {
00912                 break;
00913             }
00914         }
00915     }
00916 }
00917 
00918 class FSFileBackendFileList extends FSFileBackendList {
00919     protected function filterViaNext() {
00920         while ( $this->iter->valid() ) {
00921             if ( !$this->iter->current()->isFile() ) {
00922                 $this->iter->next(); // skip non-files and dot files
00923             } else {
00924                 break;
00925             }
00926         }
00927     }
00928 }