MediaWiki  REL1_23
FSFileBackend.php
Go to the documentation of this file.
00001 <?php
00041 class FSFileBackend extends FileBackendStore {
00043     protected $basePath;
00044 
00046     protected $containerPaths = array();
00047 
00049     protected $fileMode;
00050 
00052     protected $fileOwner;
00053 
00055     protected $currentUser;
00056 
00058     protected $hadWarningErrors = array();
00059 
00068     public function __construct( array $config ) {
00069         parent::__construct( $config );
00070 
00071         // Remove any possible trailing slash from directories
00072         if ( isset( $config['basePath'] ) ) {
00073             $this->basePath = rtrim( $config['basePath'], '/' ); // remove trailing slash
00074         } else {
00075             $this->basePath = null; // none; containers must have explicit paths
00076         }
00077 
00078         if ( isset( $config['containerPaths'] ) ) {
00079             $this->containerPaths = (array)$config['containerPaths'];
00080             foreach ( $this->containerPaths as &$path ) {
00081                 $path = rtrim( $path, '/' ); // remove trailing slash
00082             }
00083         }
00084 
00085         $this->fileMode = isset( $config['fileMode'] ) ? $config['fileMode'] : 0644;
00086         if ( isset( $config['fileOwner'] ) && function_exists( 'posix_getuid' ) ) {
00087             $this->fileOwner = $config['fileOwner'];
00088             $info = posix_getpwuid( posix_getuid() );
00089             $this->currentUser = $info['name']; // cache this, assuming it doesn't change
00090         }
00091     }
00092 
00093     protected function resolveContainerPath( $container, $relStoragePath ) {
00094         // Check that container has a root directory
00095         if ( isset( $this->containerPaths[$container] ) || isset( $this->basePath ) ) {
00096             // Check for sane relative paths (assume the base paths are OK)
00097             if ( $this->isLegalRelPath( $relStoragePath ) ) {
00098                 return $relStoragePath;
00099             }
00100         }
00101 
00102         return null;
00103     }
00104 
00111     protected function isLegalRelPath( $path ) {
00112         // Check for file names longer than 255 chars
00113         if ( preg_match( '![^/]{256}!', $path ) ) { // ext3/NTFS
00114             return false;
00115         }
00116         if ( wfIsWindows() ) { // NTFS
00117             return !preg_match( '![:*?"<>|]!', $path );
00118         } else {
00119             return true;
00120         }
00121     }
00122 
00131     protected function containerFSRoot( $shortCont, $fullCont ) {
00132         if ( isset( $this->containerPaths[$shortCont] ) ) {
00133             return $this->containerPaths[$shortCont];
00134         } elseif ( isset( $this->basePath ) ) {
00135             return "{$this->basePath}/{$fullCont}";
00136         }
00137 
00138         return null; // no container base path defined
00139     }
00140 
00147     protected function resolveToFSPath( $storagePath ) {
00148         list( $fullCont, $relPath ) = $this->resolveStoragePathReal( $storagePath );
00149         if ( $relPath === null ) {
00150             return null; // invalid
00151         }
00152         list( , $shortCont, ) = FileBackend::splitStoragePath( $storagePath );
00153         $fsPath = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
00154         if ( $relPath != '' ) {
00155             $fsPath .= "/{$relPath}";
00156         }
00157 
00158         return $fsPath;
00159     }
00160 
00161     public function isPathUsableInternal( $storagePath ) {
00162         $fsPath = $this->resolveToFSPath( $storagePath );
00163         if ( $fsPath === null ) {
00164             return false; // invalid
00165         }
00166         $parentDir = dirname( $fsPath );
00167 
00168         if ( file_exists( $fsPath ) ) {
00169             $ok = is_file( $fsPath ) && is_writable( $fsPath );
00170         } else {
00171             $ok = is_dir( $parentDir ) && is_writable( $parentDir );
00172         }
00173 
00174         if ( $this->fileOwner !== null && $this->currentUser !== $this->fileOwner ) {
00175             $ok = false;
00176             trigger_error( __METHOD__ . ": PHP process owner is not '{$this->fileOwner}'." );
00177         }
00178 
00179         return $ok;
00180     }
00181 
00182     protected function doCreateInternal( array $params ) {
00183         $status = Status::newGood();
00184 
00185         $dest = $this->resolveToFSPath( $params['dst'] );
00186         if ( $dest === null ) {
00187             $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
00188 
00189             return $status;
00190         }
00191 
00192         if ( !empty( $params['async'] ) ) { // deferred
00193             $tempFile = TempFSFile::factory( 'create_', 'tmp' );
00194             if ( !$tempFile ) {
00195                 $status->fatal( 'backend-fail-create', $params['dst'] );
00196 
00197                 return $status;
00198             }
00199             $this->trapWarnings();
00200             $bytes = file_put_contents( $tempFile->getPath(), $params['content'] );
00201             $this->untrapWarnings();
00202             if ( $bytes === false ) {
00203                 $status->fatal( 'backend-fail-create', $params['dst'] );
00204 
00205                 return $status;
00206             }
00207             $cmd = implode( ' ', array(
00208                 wfIsWindows() ? 'COPY /B /Y' : 'cp', // (binary, overwrite)
00209                 wfEscapeShellArg( $this->cleanPathSlashes( $tempFile->getPath() ) ),
00210                 wfEscapeShellArg( $this->cleanPathSlashes( $dest ) )
00211             ) );
00212             $status->value = new FSFileOpHandle( $this, $params, 'Create', $cmd, $dest );
00213             $tempFile->bind( $status->value );
00214         } else { // immediate write
00215             $this->trapWarnings();
00216             $bytes = file_put_contents( $dest, $params['content'] );
00217             $this->untrapWarnings();
00218             if ( $bytes === false ) {
00219                 $status->fatal( 'backend-fail-create', $params['dst'] );
00220 
00221                 return $status;
00222             }
00223             $this->chmod( $dest );
00224         }
00225 
00226         return $status;
00227     }
00228 
00232     protected function getResponseCreate( $errors, Status $status, array $params, $cmd ) {
00233         if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
00234             $status->fatal( 'backend-fail-create', $params['dst'] );
00235             trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
00236         }
00237     }
00238 
00239     protected function doStoreInternal( array $params ) {
00240         $status = Status::newGood();
00241 
00242         $dest = $this->resolveToFSPath( $params['dst'] );
00243         if ( $dest === null ) {
00244             $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
00245 
00246             return $status;
00247         }
00248 
00249         if ( !empty( $params['async'] ) ) { // deferred
00250             $cmd = implode( ' ', array(
00251                 wfIsWindows() ? 'COPY /B /Y' : 'cp', // (binary, overwrite)
00252                 wfEscapeShellArg( $this->cleanPathSlashes( $params['src'] ) ),
00253                 wfEscapeShellArg( $this->cleanPathSlashes( $dest ) )
00254             ) );
00255             $status->value = new FSFileOpHandle( $this, $params, 'Store', $cmd, $dest );
00256         } else { // immediate write
00257             $this->trapWarnings();
00258             $ok = copy( $params['src'], $dest );
00259             $this->untrapWarnings();
00260             // In some cases (at least over NFS), copy() returns true when it fails
00261             if ( !$ok || ( filesize( $params['src'] ) !== filesize( $dest ) ) ) {
00262                 if ( $ok ) { // PHP bug
00263                     unlink( $dest ); // remove broken file
00264                     trigger_error( __METHOD__ . ": copy() failed but returned true." );
00265                 }
00266                 $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
00267 
00268                 return $status;
00269             }
00270             $this->chmod( $dest );
00271         }
00272 
00273         return $status;
00274     }
00275 
00279     protected function getResponseStore( $errors, Status $status, array $params, $cmd ) {
00280         if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
00281             $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
00282             trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
00283         }
00284     }
00285 
00286     protected function doCopyInternal( array $params ) {
00287         $status = Status::newGood();
00288 
00289         $source = $this->resolveToFSPath( $params['src'] );
00290         if ( $source === null ) {
00291             $status->fatal( 'backend-fail-invalidpath', $params['src'] );
00292 
00293             return $status;
00294         }
00295 
00296         $dest = $this->resolveToFSPath( $params['dst'] );
00297         if ( $dest === null ) {
00298             $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
00299 
00300             return $status;
00301         }
00302 
00303         if ( !is_file( $source ) ) {
00304             if ( empty( $params['ignoreMissingSource'] ) ) {
00305                 $status->fatal( 'backend-fail-copy', $params['src'] );
00306             }
00307 
00308             return $status; // do nothing; either OK or bad status
00309         }
00310 
00311         if ( !empty( $params['async'] ) ) { // deferred
00312             $cmd = implode( ' ', array(
00313                 wfIsWindows() ? 'COPY /B /Y' : 'cp', // (binary, overwrite)
00314                 wfEscapeShellArg( $this->cleanPathSlashes( $source ) ),
00315                 wfEscapeShellArg( $this->cleanPathSlashes( $dest ) )
00316             ) );
00317             $status->value = new FSFileOpHandle( $this, $params, 'Copy', $cmd, $dest );
00318         } else { // immediate write
00319             $this->trapWarnings();
00320             $ok = ( $source === $dest ) ? true : copy( $source, $dest );
00321             $this->untrapWarnings();
00322             // In some cases (at least over NFS), copy() returns true when it fails
00323             if ( !$ok || ( filesize( $source ) !== filesize( $dest ) ) ) {
00324                 if ( $ok ) { // PHP bug
00325                     $this->trapWarnings();
00326                     unlink( $dest ); // remove broken file
00327                     $this->untrapWarnings();
00328                     trigger_error( __METHOD__ . ": copy() failed but returned true." );
00329                 }
00330                 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
00331 
00332                 return $status;
00333             }
00334             $this->chmod( $dest );
00335         }
00336 
00337         return $status;
00338     }
00339 
00343     protected function getResponseCopy( $errors, Status $status, array $params, $cmd ) {
00344         if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
00345             $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
00346             trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
00347         }
00348     }
00349 
00350     protected function doMoveInternal( array $params ) {
00351         $status = Status::newGood();
00352 
00353         $source = $this->resolveToFSPath( $params['src'] );
00354         if ( $source === null ) {
00355             $status->fatal( 'backend-fail-invalidpath', $params['src'] );
00356 
00357             return $status;
00358         }
00359 
00360         $dest = $this->resolveToFSPath( $params['dst'] );
00361         if ( $dest === null ) {
00362             $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
00363 
00364             return $status;
00365         }
00366 
00367         if ( !is_file( $source ) ) {
00368             if ( empty( $params['ignoreMissingSource'] ) ) {
00369                 $status->fatal( 'backend-fail-move', $params['src'] );
00370             }
00371 
00372             return $status; // do nothing; either OK or bad status
00373         }
00374 
00375         if ( !empty( $params['async'] ) ) { // deferred
00376             $cmd = implode( ' ', array(
00377                 wfIsWindows() ? 'MOVE /Y' : 'mv', // (overwrite)
00378                 wfEscapeShellArg( $this->cleanPathSlashes( $source ) ),
00379                 wfEscapeShellArg( $this->cleanPathSlashes( $dest ) )
00380             ) );
00381             $status->value = new FSFileOpHandle( $this, $params, 'Move', $cmd );
00382         } else { // immediate write
00383             $this->trapWarnings();
00384             $ok = ( $source === $dest ) ? true : rename( $source, $dest );
00385             $this->untrapWarnings();
00386             clearstatcache(); // file no longer at source
00387             if ( !$ok ) {
00388                 $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
00389 
00390                 return $status;
00391             }
00392         }
00393 
00394         return $status;
00395     }
00396 
00400     protected function getResponseMove( $errors, Status $status, array $params, $cmd ) {
00401         if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
00402             $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
00403             trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
00404         }
00405     }
00406 
00407     protected function doDeleteInternal( array $params ) {
00408         $status = Status::newGood();
00409 
00410         $source = $this->resolveToFSPath( $params['src'] );
00411         if ( $source === null ) {
00412             $status->fatal( 'backend-fail-invalidpath', $params['src'] );
00413 
00414             return $status;
00415         }
00416 
00417         if ( !is_file( $source ) ) {
00418             if ( empty( $params['ignoreMissingSource'] ) ) {
00419                 $status->fatal( 'backend-fail-delete', $params['src'] );
00420             }
00421 
00422             return $status; // do nothing; either OK or bad status
00423         }
00424 
00425         if ( !empty( $params['async'] ) ) { // deferred
00426             $cmd = implode( ' ', array(
00427                 wfIsWindows() ? 'DEL' : 'unlink',
00428                 wfEscapeShellArg( $this->cleanPathSlashes( $source ) )
00429             ) );
00430             $status->value = new FSFileOpHandle( $this, $params, 'Copy', $cmd );
00431         } else { // immediate write
00432             $this->trapWarnings();
00433             $ok = unlink( $source );
00434             $this->untrapWarnings();
00435             if ( !$ok ) {
00436                 $status->fatal( 'backend-fail-delete', $params['src'] );
00437 
00438                 return $status;
00439             }
00440         }
00441 
00442         return $status;
00443     }
00444 
00448     protected function getResponseDelete( $errors, Status $status, array $params, $cmd ) {
00449         if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
00450             $status->fatal( 'backend-fail-delete', $params['src'] );
00451             trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
00452         }
00453     }
00454 
00461     protected function doPrepareInternal( $fullCont, $dirRel, array $params ) {
00462         $status = Status::newGood();
00463         list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
00464         $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
00465         $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
00466         $existed = is_dir( $dir ); // already there?
00467         // Create the directory and its parents as needed...
00468         $this->trapWarnings();
00469         if ( !wfMkdirParents( $dir ) ) {
00470             $status->fatal( 'directorycreateerror', $params['dir'] ); // fails on races
00471         } elseif ( !is_writable( $dir ) ) {
00472             $status->fatal( 'directoryreadonlyerror', $params['dir'] );
00473         } elseif ( !is_readable( $dir ) ) {
00474             $status->fatal( 'directorynotreadableerror', $params['dir'] );
00475         }
00476         $this->untrapWarnings();
00477         // Respect any 'noAccess' or 'noListing' flags...
00478         if ( is_dir( $dir ) && !$existed ) {
00479             $status->merge( $this->doSecureInternal( $fullCont, $dirRel, $params ) );
00480         }
00481 
00482         return $status;
00483     }
00484 
00485     protected function doSecureInternal( $fullCont, $dirRel, array $params ) {
00486         $status = Status::newGood();
00487         list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
00488         $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
00489         $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
00490         // Seed new directories with a blank index.html, to prevent crawling...
00491         if ( !empty( $params['noListing'] ) && !file_exists( "{$dir}/index.html" ) ) {
00492             $this->trapWarnings();
00493             $bytes = file_put_contents( "{$dir}/index.html", $this->indexHtmlPrivate() );
00494             $this->untrapWarnings();
00495             if ( $bytes === false ) {
00496                 $status->fatal( 'backend-fail-create', $params['dir'] . '/index.html' );
00497             }
00498         }
00499         // Add a .htaccess file to the root of the container...
00500         if ( !empty( $params['noAccess'] ) && !file_exists( "{$contRoot}/.htaccess" ) ) {
00501             $this->trapWarnings();
00502             $bytes = file_put_contents( "{$contRoot}/.htaccess", $this->htaccessPrivate() );
00503             $this->untrapWarnings();
00504             if ( $bytes === false ) {
00505                 $storeDir = "mwstore://{$this->name}/{$shortCont}";
00506                 $status->fatal( 'backend-fail-create', "{$storeDir}/.htaccess" );
00507             }
00508         }
00509 
00510         return $status;
00511     }
00512 
00513     protected function doPublishInternal( $fullCont, $dirRel, array $params ) {
00514         $status = Status::newGood();
00515         list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
00516         $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
00517         $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
00518         // Unseed new directories with a blank index.html, to allow crawling...
00519         if ( !empty( $params['listing'] ) && is_file( "{$dir}/index.html" ) ) {
00520             $exists = ( file_get_contents( "{$dir}/index.html" ) === $this->indexHtmlPrivate() );
00521             $this->trapWarnings();
00522             if ( $exists && !unlink( "{$dir}/index.html" ) ) { // reverse secure()
00523                 $status->fatal( 'backend-fail-delete', $params['dir'] . '/index.html' );
00524             }
00525             $this->untrapWarnings();
00526         }
00527         // Remove the .htaccess file from the root of the container...
00528         if ( !empty( $params['access'] ) && is_file( "{$contRoot}/.htaccess" ) ) {
00529             $exists = ( file_get_contents( "{$contRoot}/.htaccess" ) === $this->htaccessPrivate() );
00530             $this->trapWarnings();
00531             if ( $exists && !unlink( "{$contRoot}/.htaccess" ) ) { // reverse secure()
00532                 $storeDir = "mwstore://{$this->name}/{$shortCont}";
00533                 $status->fatal( 'backend-fail-delete', "{$storeDir}/.htaccess" );
00534             }
00535             $this->untrapWarnings();
00536         }
00537 
00538         return $status;
00539     }
00540 
00541     protected function doCleanInternal( $fullCont, $dirRel, array $params ) {
00542         $status = Status::newGood();
00543         list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
00544         $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
00545         $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
00546         $this->trapWarnings();
00547         if ( is_dir( $dir ) ) {
00548             rmdir( $dir ); // remove directory if empty
00549         }
00550         $this->untrapWarnings();
00551 
00552         return $status;
00553     }
00554 
00555     protected function doGetFileStat( array $params ) {
00556         $source = $this->resolveToFSPath( $params['src'] );
00557         if ( $source === null ) {
00558             return false; // invalid storage path
00559         }
00560 
00561         $this->trapWarnings(); // don't trust 'false' if there were errors
00562         $stat = is_file( $source ) ? stat( $source ) : false; // regular files only
00563         $hadError = $this->untrapWarnings();
00564 
00565         if ( $stat ) {
00566             return array(
00567                 'mtime' => wfTimestamp( TS_MW, $stat['mtime'] ),
00568                 'size' => $stat['size']
00569             );
00570         } elseif ( !$hadError ) {
00571             return false; // file does not exist
00572         } else {
00573             return null; // failure
00574         }
00575     }
00576 
00580     protected function doClearCache( array $paths = null ) {
00581         clearstatcache(); // clear the PHP file stat cache
00582     }
00583 
00584     protected function doDirectoryExists( $fullCont, $dirRel, array $params ) {
00585         list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
00586         $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
00587         $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
00588 
00589         $this->trapWarnings(); // don't trust 'false' if there were errors
00590         $exists = is_dir( $dir );
00591         $hadError = $this->untrapWarnings();
00592 
00593         return $hadError ? null : $exists;
00594     }
00595 
00603     public function getDirectoryListInternal( $fullCont, $dirRel, array $params ) {
00604         list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
00605         $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
00606         $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
00607         $exists = is_dir( $dir );
00608         if ( !$exists ) {
00609             wfDebug( __METHOD__ . "() given directory does not exist: '$dir'\n" );
00610 
00611             return array(); // nothing under this dir
00612         } elseif ( !is_readable( $dir ) ) {
00613             wfDebug( __METHOD__ . "() given directory is unreadable: '$dir'\n" );
00614 
00615             return null; // bad permissions?
00616         }
00617 
00618         return new FSFileBackendDirList( $dir, $params );
00619     }
00620 
00628     public function getFileListInternal( $fullCont, $dirRel, array $params ) {
00629         list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
00630         $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
00631         $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
00632         $exists = is_dir( $dir );
00633         if ( !$exists ) {
00634             wfDebug( __METHOD__ . "() given directory does not exist: '$dir'\n" );
00635 
00636             return array(); // nothing under this dir
00637         } elseif ( !is_readable( $dir ) ) {
00638             wfDebug( __METHOD__ . "() given directory is unreadable: '$dir'\n" );
00639 
00640             return null; // bad permissions?
00641         }
00642 
00643         return new FSFileBackendFileList( $dir, $params );
00644     }
00645 
00646     protected function doGetLocalReferenceMulti( array $params ) {
00647         $fsFiles = array(); // (path => FSFile)
00648 
00649         foreach ( $params['srcs'] as $src ) {
00650             $source = $this->resolveToFSPath( $src );
00651             if ( $source === null || !is_file( $source ) ) {
00652                 $fsFiles[$src] = null; // invalid path or file does not exist
00653             } else {
00654                 $fsFiles[$src] = new FSFile( $source );
00655             }
00656         }
00657 
00658         return $fsFiles;
00659     }
00660 
00661     protected function doGetLocalCopyMulti( array $params ) {
00662         $tmpFiles = array(); // (path => TempFSFile)
00663 
00664         foreach ( $params['srcs'] as $src ) {
00665             $source = $this->resolveToFSPath( $src );
00666             if ( $source === null ) {
00667                 $tmpFiles[$src] = null; // invalid path
00668             } else {
00669                 // Create a new temporary file with the same extension...
00670                 $ext = FileBackend::extensionFromPath( $src );
00671                 $tmpFile = TempFSFile::factory( 'localcopy_', $ext );
00672                 if ( !$tmpFile ) {
00673                     $tmpFiles[$src] = null;
00674                 } else {
00675                     $tmpPath = $tmpFile->getPath();
00676                     // Copy the source file over the temp file
00677                     $this->trapWarnings();
00678                     $ok = copy( $source, $tmpPath );
00679                     $this->untrapWarnings();
00680                     if ( !$ok ) {
00681                         $tmpFiles[$src] = null;
00682                     } else {
00683                         $this->chmod( $tmpPath );
00684                         $tmpFiles[$src] = $tmpFile;
00685                     }
00686                 }
00687             }
00688         }
00689 
00690         return $tmpFiles;
00691     }
00692 
00693     protected function directoriesAreVirtual() {
00694         return false;
00695     }
00696 
00697     protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
00698         $statuses = array();
00699 
00700         $pipes = array();
00701         foreach ( $fileOpHandles as $index => $fileOpHandle ) {
00702             $pipes[$index] = popen( "{$fileOpHandle->cmd} 2>&1", 'r' );
00703         }
00704 
00705         $errs = array();
00706         foreach ( $pipes as $index => $pipe ) {
00707             // Result will be empty on success in *NIX. On Windows,
00708             // it may be something like "        1 file(s) [copied|moved].".
00709             $errs[$index] = stream_get_contents( $pipe );
00710             fclose( $pipe );
00711         }
00712 
00713         foreach ( $fileOpHandles as $index => $fileOpHandle ) {
00714             $status = Status::newGood();
00715             $function = 'getResponse' . $fileOpHandle->call;
00716             $this->$function( $errs[$index], $status, $fileOpHandle->params, $fileOpHandle->cmd );
00717             $statuses[$index] = $status;
00718             if ( $status->isOK() && $fileOpHandle->chmodPath ) {
00719                 $this->chmod( $fileOpHandle->chmodPath );
00720             }
00721         }
00722 
00723         clearstatcache(); // files changed
00724         return $statuses;
00725     }
00726 
00733     protected function chmod( $path ) {
00734         $this->trapWarnings();
00735         $ok = chmod( $path, $this->fileMode );
00736         $this->untrapWarnings();
00737 
00738         return $ok;
00739     }
00740 
00746     protected function indexHtmlPrivate() {
00747         return '';
00748     }
00749 
00755     protected function htaccessPrivate() {
00756         return "Deny from all\n";
00757     }
00758 
00765     protected function cleanPathSlashes( $path ) {
00766         return wfIsWindows() ? strtr( $path, '/', '\\' ) : $path;
00767     }
00768 
00772     protected function trapWarnings() {
00773         $this->hadWarningErrors[] = false; // push to stack
00774         set_error_handler( array( $this, 'handleWarning' ), E_WARNING );
00775     }
00776 
00782     protected function untrapWarnings() {
00783         restore_error_handler(); // restore previous handler
00784         return array_pop( $this->hadWarningErrors ); // pop from stack
00785     }
00786 
00793     public function handleWarning( $errno, $errstr ) {
00794         wfDebugLog( 'FSFileBackend', $errstr ); // more detailed error logging
00795         $this->hadWarningErrors[count( $this->hadWarningErrors ) - 1] = true;
00796 
00797         return true; // suppress from PHP handler
00798     }
00799 }
00800 
00804 class FSFileOpHandle extends FileBackendStoreOpHandle {
00805     public $cmd; // string; shell command
00806     public $chmodPath; // string; file to chmod
00807 
00815     public function __construct(
00816         FSFileBackend $backend, array $params, $call, $cmd, $chmodPath = null
00817     ) {
00818         $this->backend = $backend;
00819         $this->params = $params;
00820         $this->call = $call;
00821         $this->cmd = $cmd;
00822         $this->chmodPath = $chmodPath;
00823     }
00824 }
00825 
00833 abstract class FSFileBackendList implements Iterator {
00835     protected $iter;
00836 
00838     protected $suffixStart;
00839 
00841     protected $pos = 0;
00842 
00844     protected $params = array();
00845 
00850     public function __construct( $dir, array $params ) {
00851         $path = realpath( $dir ); // normalize
00852         if ( $path === false ) {
00853             $path = $dir;
00854         }
00855         $this->suffixStart = strlen( $path ) + 1; // size of "path/to/dir/"
00856         $this->params = $params;
00857 
00858         try {
00859             $this->iter = $this->initIterator( $path );
00860         } catch ( UnexpectedValueException $e ) {
00861             $this->iter = null; // bad permissions? deleted?
00862         }
00863     }
00864 
00871     protected function initIterator( $dir ) {
00872         if ( !empty( $this->params['topOnly'] ) ) { // non-recursive
00873             # Get an iterator that will get direct sub-nodes
00874             return new DirectoryIterator( $dir );
00875         } else { // recursive
00876             # Get an iterator that will return leaf nodes (non-directories)
00877             # RecursiveDirectoryIterator extends FilesystemIterator.
00878             # FilesystemIterator::SKIP_DOTS default is inconsistent in PHP 5.3.x.
00879             $flags = FilesystemIterator::CURRENT_AS_SELF | FilesystemIterator::SKIP_DOTS;
00880 
00881             return new RecursiveIteratorIterator(
00882                 new RecursiveDirectoryIterator( $dir, $flags ),
00883                 RecursiveIteratorIterator::CHILD_FIRST // include dirs
00884             );
00885         }
00886     }
00887 
00892     public function key() {
00893         return $this->pos;
00894     }
00895 
00900     public function current() {
00901         return $this->getRelPath( $this->iter->current()->getPathname() );
00902     }
00903 
00908     public function next() {
00909         try {
00910             $this->iter->next();
00911             $this->filterViaNext();
00912         } catch ( UnexpectedValueException $e ) { // bad permissions? deleted?
00913             throw new FileBackendError( "File iterator gave UnexpectedValueException." );
00914         }
00915         ++$this->pos;
00916     }
00917 
00922     public function rewind() {
00923         $this->pos = 0;
00924         try {
00925             $this->iter->rewind();
00926             $this->filterViaNext();
00927         } catch ( UnexpectedValueException $e ) { // bad permissions? deleted?
00928             throw new FileBackendError( "File iterator gave UnexpectedValueException." );
00929         }
00930     }
00931 
00936     public function valid() {
00937         return $this->iter && $this->iter->valid();
00938     }
00939 
00943     protected function filterViaNext() {
00944     }
00945 
00953     protected function getRelPath( $dir ) {
00954         $path = realpath( $dir );
00955         if ( $path === false ) {
00956             $path = $dir;
00957         }
00958 
00959         return strtr( substr( $path, $this->suffixStart ), '\\', '/' );
00960     }
00961 }
00962 
00963 class FSFileBackendDirList extends FSFileBackendList {
00964     protected function filterViaNext() {
00965         while ( $this->iter->valid() ) {
00966             if ( $this->iter->current()->isDot() || !$this->iter->current()->isDir() ) {
00967                 $this->iter->next(); // skip non-directories and dot files
00968             } else {
00969                 break;
00970             }
00971         }
00972     }
00973 }
00974 
00975 class FSFileBackendFileList extends FSFileBackendList {
00976     protected function filterViaNext() {
00977         while ( $this->iter->valid() ) {
00978             if ( !$this->iter->current()->isFile() ) {
00979                 $this->iter->next(); // skip non-files and dot files
00980             } else {
00981                 break;
00982             }
00983         }
00984     }
00985 }