MediaWiki  REL1_24
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     public function getFeatures() {
00094         return !wfIsWindows() ? FileBackend::ATTR_UNICODE_PATHS : 0;
00095     }
00096 
00097     protected function resolveContainerPath( $container, $relStoragePath ) {
00098         // Check that container has a root directory
00099         if ( isset( $this->containerPaths[$container] ) || isset( $this->basePath ) ) {
00100             // Check for sane relative paths (assume the base paths are OK)
00101             if ( $this->isLegalRelPath( $relStoragePath ) ) {
00102                 return $relStoragePath;
00103             }
00104         }
00105 
00106         return null;
00107     }
00108 
00115     protected function isLegalRelPath( $path ) {
00116         // Check for file names longer than 255 chars
00117         if ( preg_match( '![^/]{256}!', $path ) ) { // ext3/NTFS
00118             return false;
00119         }
00120         if ( wfIsWindows() ) { // NTFS
00121             return !preg_match( '![:*?"<>|]!', $path );
00122         } else {
00123             return true;
00124         }
00125     }
00126 
00135     protected function containerFSRoot( $shortCont, $fullCont ) {
00136         if ( isset( $this->containerPaths[$shortCont] ) ) {
00137             return $this->containerPaths[$shortCont];
00138         } elseif ( isset( $this->basePath ) ) {
00139             return "{$this->basePath}/{$fullCont}";
00140         }
00141 
00142         return null; // no container base path defined
00143     }
00144 
00151     protected function resolveToFSPath( $storagePath ) {
00152         list( $fullCont, $relPath ) = $this->resolveStoragePathReal( $storagePath );
00153         if ( $relPath === null ) {
00154             return null; // invalid
00155         }
00156         list( , $shortCont, ) = FileBackend::splitStoragePath( $storagePath );
00157         $fsPath = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
00158         if ( $relPath != '' ) {
00159             $fsPath .= "/{$relPath}";
00160         }
00161 
00162         return $fsPath;
00163     }
00164 
00165     public function isPathUsableInternal( $storagePath ) {
00166         $fsPath = $this->resolveToFSPath( $storagePath );
00167         if ( $fsPath === null ) {
00168             return false; // invalid
00169         }
00170         $parentDir = dirname( $fsPath );
00171 
00172         if ( file_exists( $fsPath ) ) {
00173             $ok = is_file( $fsPath ) && is_writable( $fsPath );
00174         } else {
00175             $ok = is_dir( $parentDir ) && is_writable( $parentDir );
00176         }
00177 
00178         if ( $this->fileOwner !== null && $this->currentUser !== $this->fileOwner ) {
00179             $ok = false;
00180             trigger_error( __METHOD__ . ": PHP process owner is not '{$this->fileOwner}'." );
00181         }
00182 
00183         return $ok;
00184     }
00185 
00186     protected function doCreateInternal( array $params ) {
00187         $status = Status::newGood();
00188 
00189         $dest = $this->resolveToFSPath( $params['dst'] );
00190         if ( $dest === null ) {
00191             $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
00192 
00193             return $status;
00194         }
00195 
00196         if ( !empty( $params['async'] ) ) { // deferred
00197             $tempFile = TempFSFile::factory( 'create_', 'tmp' );
00198             if ( !$tempFile ) {
00199                 $status->fatal( 'backend-fail-create', $params['dst'] );
00200 
00201                 return $status;
00202             }
00203             $this->trapWarnings();
00204             $bytes = file_put_contents( $tempFile->getPath(), $params['content'] );
00205             $this->untrapWarnings();
00206             if ( $bytes === false ) {
00207                 $status->fatal( 'backend-fail-create', $params['dst'] );
00208 
00209                 return $status;
00210             }
00211             $cmd = implode( ' ', array(
00212                 wfIsWindows() ? 'COPY /B /Y' : 'cp', // (binary, overwrite)
00213                 wfEscapeShellArg( $this->cleanPathSlashes( $tempFile->getPath() ) ),
00214                 wfEscapeShellArg( $this->cleanPathSlashes( $dest ) )
00215             ) );
00216             $handler = function ( $errors, Status $status, array $params, $cmd ) {
00217                 if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
00218                     $status->fatal( 'backend-fail-create', $params['dst'] );
00219                     trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
00220                 }
00221             };
00222             $status->value = new FSFileOpHandle( $this, $params, $handler, $cmd, $dest );
00223             $tempFile->bind( $status->value );
00224         } else { // immediate write
00225             $this->trapWarnings();
00226             $bytes = file_put_contents( $dest, $params['content'] );
00227             $this->untrapWarnings();
00228             if ( $bytes === false ) {
00229                 $status->fatal( 'backend-fail-create', $params['dst'] );
00230 
00231                 return $status;
00232             }
00233             $this->chmod( $dest );
00234         }
00235 
00236         return $status;
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             $handler = function ( $errors, Status $status, array $params, $cmd ) {
00256                 if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
00257                     $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
00258                     trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
00259                 }
00260             };
00261             $status->value = new FSFileOpHandle( $this, $params, $handler, $cmd, $dest );
00262         } else { // immediate write
00263             $this->trapWarnings();
00264             $ok = copy( $params['src'], $dest );
00265             $this->untrapWarnings();
00266             // In some cases (at least over NFS), copy() returns true when it fails
00267             if ( !$ok || ( filesize( $params['src'] ) !== filesize( $dest ) ) ) {
00268                 if ( $ok ) { // PHP bug
00269                     unlink( $dest ); // remove broken file
00270                     trigger_error( __METHOD__ . ": copy() failed but returned true." );
00271                 }
00272                 $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
00273 
00274                 return $status;
00275             }
00276             $this->chmod( $dest );
00277         }
00278 
00279         return $status;
00280     }
00281 
00282     protected function doCopyInternal( array $params ) {
00283         $status = Status::newGood();
00284 
00285         $source = $this->resolveToFSPath( $params['src'] );
00286         if ( $source === null ) {
00287             $status->fatal( 'backend-fail-invalidpath', $params['src'] );
00288 
00289             return $status;
00290         }
00291 
00292         $dest = $this->resolveToFSPath( $params['dst'] );
00293         if ( $dest === null ) {
00294             $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
00295 
00296             return $status;
00297         }
00298 
00299         if ( !is_file( $source ) ) {
00300             if ( empty( $params['ignoreMissingSource'] ) ) {
00301                 $status->fatal( 'backend-fail-copy', $params['src'] );
00302             }
00303 
00304             return $status; // do nothing; either OK or bad status
00305         }
00306 
00307         if ( !empty( $params['async'] ) ) { // deferred
00308             $cmd = implode( ' ', array(
00309                 wfIsWindows() ? 'COPY /B /Y' : 'cp', // (binary, overwrite)
00310                 wfEscapeShellArg( $this->cleanPathSlashes( $source ) ),
00311                 wfEscapeShellArg( $this->cleanPathSlashes( $dest ) )
00312             ) );
00313             $handler = function ( $errors, Status $status, array $params, $cmd ) {
00314                 if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
00315                     $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
00316                     trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
00317                 }
00318             };
00319             $status->value = new FSFileOpHandle( $this, $params, $handler, $cmd, $dest );
00320         } else { // immediate write
00321             $this->trapWarnings();
00322             $ok = ( $source === $dest ) ? true : copy( $source, $dest );
00323             $this->untrapWarnings();
00324             // In some cases (at least over NFS), copy() returns true when it fails
00325             if ( !$ok || ( filesize( $source ) !== filesize( $dest ) ) ) {
00326                 if ( $ok ) { // PHP bug
00327                     $this->trapWarnings();
00328                     unlink( $dest ); // remove broken file
00329                     $this->untrapWarnings();
00330                     trigger_error( __METHOD__ . ": copy() failed but returned true." );
00331                 }
00332                 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
00333 
00334                 return $status;
00335             }
00336             $this->chmod( $dest );
00337         }
00338 
00339         return $status;
00340     }
00341 
00342     protected function doMoveInternal( array $params ) {
00343         $status = Status::newGood();
00344 
00345         $source = $this->resolveToFSPath( $params['src'] );
00346         if ( $source === null ) {
00347             $status->fatal( 'backend-fail-invalidpath', $params['src'] );
00348 
00349             return $status;
00350         }
00351 
00352         $dest = $this->resolveToFSPath( $params['dst'] );
00353         if ( $dest === null ) {
00354             $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
00355 
00356             return $status;
00357         }
00358 
00359         if ( !is_file( $source ) ) {
00360             if ( empty( $params['ignoreMissingSource'] ) ) {
00361                 $status->fatal( 'backend-fail-move', $params['src'] );
00362             }
00363 
00364             return $status; // do nothing; either OK or bad status
00365         }
00366 
00367         if ( !empty( $params['async'] ) ) { // deferred
00368             $cmd = implode( ' ', array(
00369                 wfIsWindows() ? 'MOVE /Y' : 'mv', // (overwrite)
00370                 wfEscapeShellArg( $this->cleanPathSlashes( $source ) ),
00371                 wfEscapeShellArg( $this->cleanPathSlashes( $dest ) )
00372             ) );
00373             $handler = function ( $errors, Status $status, array $params, $cmd ) {
00374                 if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
00375                     $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
00376                     trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
00377                 }
00378             };
00379             $status->value = new FSFileOpHandle( $this, $params, $handler, $cmd );
00380         } else { // immediate write
00381             $this->trapWarnings();
00382             $ok = ( $source === $dest ) ? true : rename( $source, $dest );
00383             $this->untrapWarnings();
00384             clearstatcache(); // file no longer at source
00385             if ( !$ok ) {
00386                 $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
00387 
00388                 return $status;
00389             }
00390         }
00391 
00392         return $status;
00393     }
00394 
00395     protected function doDeleteInternal( array $params ) {
00396         $status = Status::newGood();
00397 
00398         $source = $this->resolveToFSPath( $params['src'] );
00399         if ( $source === null ) {
00400             $status->fatal( 'backend-fail-invalidpath', $params['src'] );
00401 
00402             return $status;
00403         }
00404 
00405         if ( !is_file( $source ) ) {
00406             if ( empty( $params['ignoreMissingSource'] ) ) {
00407                 $status->fatal( 'backend-fail-delete', $params['src'] );
00408             }
00409 
00410             return $status; // do nothing; either OK or bad status
00411         }
00412 
00413         if ( !empty( $params['async'] ) ) { // deferred
00414             $cmd = implode( ' ', array(
00415                 wfIsWindows() ? 'DEL' : 'unlink',
00416                 wfEscapeShellArg( $this->cleanPathSlashes( $source ) )
00417             ) );
00418             $handler = function ( $errors, Status $status, array $params, $cmd ) {
00419                 if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
00420                     $status->fatal( 'backend-fail-delete', $params['src'] );
00421                     trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
00422                 }
00423             };
00424             $status->value = new FSFileOpHandle( $this, $params, $handler, $cmd );
00425         } else { // immediate write
00426             $this->trapWarnings();
00427             $ok = unlink( $source );
00428             $this->untrapWarnings();
00429             if ( !$ok ) {
00430                 $status->fatal( 'backend-fail-delete', $params['src'] );
00431 
00432                 return $status;
00433             }
00434         }
00435 
00436         return $status;
00437     }
00438 
00445     protected function doPrepareInternal( $fullCont, $dirRel, array $params ) {
00446         $status = Status::newGood();
00447         list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
00448         $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
00449         $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
00450         $existed = is_dir( $dir ); // already there?
00451         // Create the directory and its parents as needed...
00452         $this->trapWarnings();
00453         if ( !wfMkdirParents( $dir ) ) {
00454             $status->fatal( 'directorycreateerror', $params['dir'] ); // fails on races
00455         } elseif ( !is_writable( $dir ) ) {
00456             $status->fatal( 'directoryreadonlyerror', $params['dir'] );
00457         } elseif ( !is_readable( $dir ) ) {
00458             $status->fatal( 'directorynotreadableerror', $params['dir'] );
00459         }
00460         $this->untrapWarnings();
00461         // Respect any 'noAccess' or 'noListing' flags...
00462         if ( is_dir( $dir ) && !$existed ) {
00463             $status->merge( $this->doSecureInternal( $fullCont, $dirRel, $params ) );
00464         }
00465 
00466         return $status;
00467     }
00468 
00469     protected function doSecureInternal( $fullCont, $dirRel, array $params ) {
00470         $status = Status::newGood();
00471         list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
00472         $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
00473         $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
00474         // Seed new directories with a blank index.html, to prevent crawling...
00475         if ( !empty( $params['noListing'] ) && !file_exists( "{$dir}/index.html" ) ) {
00476             $this->trapWarnings();
00477             $bytes = file_put_contents( "{$dir}/index.html", $this->indexHtmlPrivate() );
00478             $this->untrapWarnings();
00479             if ( $bytes === false ) {
00480                 $status->fatal( 'backend-fail-create', $params['dir'] . '/index.html' );
00481             }
00482         }
00483         // Add a .htaccess file to the root of the container...
00484         if ( !empty( $params['noAccess'] ) && !file_exists( "{$contRoot}/.htaccess" ) ) {
00485             $this->trapWarnings();
00486             $bytes = file_put_contents( "{$contRoot}/.htaccess", $this->htaccessPrivate() );
00487             $this->untrapWarnings();
00488             if ( $bytes === false ) {
00489                 $storeDir = "mwstore://{$this->name}/{$shortCont}";
00490                 $status->fatal( 'backend-fail-create', "{$storeDir}/.htaccess" );
00491             }
00492         }
00493 
00494         return $status;
00495     }
00496 
00497     protected function doPublishInternal( $fullCont, $dirRel, array $params ) {
00498         $status = Status::newGood();
00499         list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
00500         $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
00501         $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
00502         // Unseed new directories with a blank index.html, to allow crawling...
00503         if ( !empty( $params['listing'] ) && is_file( "{$dir}/index.html" ) ) {
00504             $exists = ( file_get_contents( "{$dir}/index.html" ) === $this->indexHtmlPrivate() );
00505             $this->trapWarnings();
00506             if ( $exists && !unlink( "{$dir}/index.html" ) ) { // reverse secure()
00507                 $status->fatal( 'backend-fail-delete', $params['dir'] . '/index.html' );
00508             }
00509             $this->untrapWarnings();
00510         }
00511         // Remove the .htaccess file from the root of the container...
00512         if ( !empty( $params['access'] ) && is_file( "{$contRoot}/.htaccess" ) ) {
00513             $exists = ( file_get_contents( "{$contRoot}/.htaccess" ) === $this->htaccessPrivate() );
00514             $this->trapWarnings();
00515             if ( $exists && !unlink( "{$contRoot}/.htaccess" ) ) { // reverse secure()
00516                 $storeDir = "mwstore://{$this->name}/{$shortCont}";
00517                 $status->fatal( 'backend-fail-delete', "{$storeDir}/.htaccess" );
00518             }
00519             $this->untrapWarnings();
00520         }
00521 
00522         return $status;
00523     }
00524 
00525     protected function doCleanInternal( $fullCont, $dirRel, array $params ) {
00526         $status = Status::newGood();
00527         list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
00528         $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
00529         $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
00530         $this->trapWarnings();
00531         if ( is_dir( $dir ) ) {
00532             rmdir( $dir ); // remove directory if empty
00533         }
00534         $this->untrapWarnings();
00535 
00536         return $status;
00537     }
00538 
00539     protected function doGetFileStat( array $params ) {
00540         $source = $this->resolveToFSPath( $params['src'] );
00541         if ( $source === null ) {
00542             return false; // invalid storage path
00543         }
00544 
00545         $this->trapWarnings(); // don't trust 'false' if there were errors
00546         $stat = is_file( $source ) ? stat( $source ) : false; // regular files only
00547         $hadError = $this->untrapWarnings();
00548 
00549         if ( $stat ) {
00550             return array(
00551                 'mtime' => wfTimestamp( TS_MW, $stat['mtime'] ),
00552                 'size' => $stat['size']
00553             );
00554         } elseif ( !$hadError ) {
00555             return false; // file does not exist
00556         } else {
00557             return null; // failure
00558         }
00559     }
00560 
00564     protected function doClearCache( array $paths = null ) {
00565         clearstatcache(); // clear the PHP file stat cache
00566     }
00567 
00568     protected function doDirectoryExists( $fullCont, $dirRel, array $params ) {
00569         list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
00570         $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
00571         $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
00572 
00573         $this->trapWarnings(); // don't trust 'false' if there were errors
00574         $exists = is_dir( $dir );
00575         $hadError = $this->untrapWarnings();
00576 
00577         return $hadError ? null : $exists;
00578     }
00579 
00587     public function getDirectoryListInternal( $fullCont, $dirRel, array $params ) {
00588         list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
00589         $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
00590         $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
00591         $exists = is_dir( $dir );
00592         if ( !$exists ) {
00593             wfDebug( __METHOD__ . "() given directory does not exist: '$dir'\n" );
00594 
00595             return array(); // nothing under this dir
00596         } elseif ( !is_readable( $dir ) ) {
00597             wfDebug( __METHOD__ . "() given directory is unreadable: '$dir'\n" );
00598 
00599             return null; // bad permissions?
00600         }
00601 
00602         return new FSFileBackendDirList( $dir, $params );
00603     }
00604 
00612     public function getFileListInternal( $fullCont, $dirRel, array $params ) {
00613         list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
00614         $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
00615         $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
00616         $exists = is_dir( $dir );
00617         if ( !$exists ) {
00618             wfDebug( __METHOD__ . "() given directory does not exist: '$dir'\n" );
00619 
00620             return array(); // nothing under this dir
00621         } elseif ( !is_readable( $dir ) ) {
00622             wfDebug( __METHOD__ . "() given directory is unreadable: '$dir'\n" );
00623 
00624             return null; // bad permissions?
00625         }
00626 
00627         return new FSFileBackendFileList( $dir, $params );
00628     }
00629 
00630     protected function doGetLocalReferenceMulti( array $params ) {
00631         $fsFiles = array(); // (path => FSFile)
00632 
00633         foreach ( $params['srcs'] as $src ) {
00634             $source = $this->resolveToFSPath( $src );
00635             if ( $source === null || !is_file( $source ) ) {
00636                 $fsFiles[$src] = null; // invalid path or file does not exist
00637             } else {
00638                 $fsFiles[$src] = new FSFile( $source );
00639             }
00640         }
00641 
00642         return $fsFiles;
00643     }
00644 
00645     protected function doGetLocalCopyMulti( array $params ) {
00646         $tmpFiles = array(); // (path => TempFSFile)
00647 
00648         foreach ( $params['srcs'] as $src ) {
00649             $source = $this->resolveToFSPath( $src );
00650             if ( $source === null ) {
00651                 $tmpFiles[$src] = null; // invalid path
00652             } else {
00653                 // Create a new temporary file with the same extension...
00654                 $ext = FileBackend::extensionFromPath( $src );
00655                 $tmpFile = TempFSFile::factory( 'localcopy_', $ext );
00656                 if ( !$tmpFile ) {
00657                     $tmpFiles[$src] = null;
00658                 } else {
00659                     $tmpPath = $tmpFile->getPath();
00660                     // Copy the source file over the temp file
00661                     $this->trapWarnings();
00662                     $ok = copy( $source, $tmpPath );
00663                     $this->untrapWarnings();
00664                     if ( !$ok ) {
00665                         $tmpFiles[$src] = null;
00666                     } else {
00667                         $this->chmod( $tmpPath );
00668                         $tmpFiles[$src] = $tmpFile;
00669                     }
00670                 }
00671             }
00672         }
00673 
00674         return $tmpFiles;
00675     }
00676 
00677     protected function directoriesAreVirtual() {
00678         return false;
00679     }
00680 
00681     protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
00682         $statuses = array();
00683 
00684         $pipes = array();
00685         foreach ( $fileOpHandles as $index => $fileOpHandle ) {
00686             $pipes[$index] = popen( "{$fileOpHandle->cmd} 2>&1", 'r' );
00687         }
00688 
00689         $errs = array();
00690         foreach ( $pipes as $index => $pipe ) {
00691             // Result will be empty on success in *NIX. On Windows,
00692             // it may be something like "        1 file(s) [copied|moved].".
00693             $errs[$index] = stream_get_contents( $pipe );
00694             fclose( $pipe );
00695         }
00696 
00697         foreach ( $fileOpHandles as $index => $fileOpHandle ) {
00698             $status = Status::newGood();
00699             $function = $fileOpHandle->call;
00700             $function( $errs[$index], $status, $fileOpHandle->params, $fileOpHandle->cmd );
00701             $statuses[$index] = $status;
00702             if ( $status->isOK() && $fileOpHandle->chmodPath ) {
00703                 $this->chmod( $fileOpHandle->chmodPath );
00704             }
00705         }
00706 
00707         clearstatcache(); // files changed
00708         return $statuses;
00709     }
00710 
00717     protected function chmod( $path ) {
00718         $this->trapWarnings();
00719         $ok = chmod( $path, $this->fileMode );
00720         $this->untrapWarnings();
00721 
00722         return $ok;
00723     }
00724 
00730     protected function indexHtmlPrivate() {
00731         return '';
00732     }
00733 
00739     protected function htaccessPrivate() {
00740         return "Deny from all\n";
00741     }
00742 
00749     protected function cleanPathSlashes( $path ) {
00750         return wfIsWindows() ? strtr( $path, '/', '\\' ) : $path;
00751     }
00752 
00756     protected function trapWarnings() {
00757         $this->hadWarningErrors[] = false; // push to stack
00758         set_error_handler( array( $this, 'handleWarning' ), E_WARNING );
00759     }
00760 
00766     protected function untrapWarnings() {
00767         restore_error_handler(); // restore previous handler
00768         return array_pop( $this->hadWarningErrors ); // pop from stack
00769     }
00770 
00777     public function handleWarning( $errno, $errstr ) {
00778         wfDebugLog( 'FSFileBackend', $errstr ); // more detailed error logging
00779         $this->hadWarningErrors[count( $this->hadWarningErrors ) - 1] = true;
00780 
00781         return true; // suppress from PHP handler
00782     }
00783 }
00784 
00788 class FSFileOpHandle extends FileBackendStoreOpHandle {
00789     public $cmd; // string; shell command
00790     public $chmodPath; // string; file to chmod
00791 
00799     public function __construct(
00800         FSFileBackend $backend, array $params, $call, $cmd, $chmodPath = null
00801     ) {
00802         $this->backend = $backend;
00803         $this->params = $params;
00804         $this->call = $call;
00805         $this->cmd = $cmd;
00806         $this->chmodPath = $chmodPath;
00807     }
00808 }
00809 
00817 abstract class FSFileBackendList implements Iterator {
00819     protected $iter;
00820 
00822     protected $suffixStart;
00823 
00825     protected $pos = 0;
00826 
00828     protected $params = array();
00829 
00834     public function __construct( $dir, array $params ) {
00835         $path = realpath( $dir ); // normalize
00836         if ( $path === false ) {
00837             $path = $dir;
00838         }
00839         $this->suffixStart = strlen( $path ) + 1; // size of "path/to/dir/"
00840         $this->params = $params;
00841 
00842         try {
00843             $this->iter = $this->initIterator( $path );
00844         } catch ( UnexpectedValueException $e ) {
00845             $this->iter = null; // bad permissions? deleted?
00846         }
00847     }
00848 
00855     protected function initIterator( $dir ) {
00856         if ( !empty( $this->params['topOnly'] ) ) { // non-recursive
00857             # Get an iterator that will get direct sub-nodes
00858             return new DirectoryIterator( $dir );
00859         } else { // recursive
00860             # Get an iterator that will return leaf nodes (non-directories)
00861             # RecursiveDirectoryIterator extends FilesystemIterator.
00862             # FilesystemIterator::SKIP_DOTS default is inconsistent in PHP 5.3.x.
00863             $flags = FilesystemIterator::CURRENT_AS_SELF | FilesystemIterator::SKIP_DOTS;
00864 
00865             return new RecursiveIteratorIterator(
00866                 new RecursiveDirectoryIterator( $dir, $flags ),
00867                 RecursiveIteratorIterator::CHILD_FIRST // include dirs
00868             );
00869         }
00870     }
00871 
00876     public function key() {
00877         return $this->pos;
00878     }
00879 
00884     public function current() {
00885         return $this->getRelPath( $this->iter->current()->getPathname() );
00886     }
00887 
00892     public function next() {
00893         try {
00894             $this->iter->next();
00895             $this->filterViaNext();
00896         } catch ( UnexpectedValueException $e ) { // bad permissions? deleted?
00897             throw new FileBackendError( "File iterator gave UnexpectedValueException." );
00898         }
00899         ++$this->pos;
00900     }
00901 
00906     public function rewind() {
00907         $this->pos = 0;
00908         try {
00909             $this->iter->rewind();
00910             $this->filterViaNext();
00911         } catch ( UnexpectedValueException $e ) { // bad permissions? deleted?
00912             throw new FileBackendError( "File iterator gave UnexpectedValueException." );
00913         }
00914     }
00915 
00920     public function valid() {
00921         return $this->iter && $this->iter->valid();
00922     }
00923 
00927     protected function filterViaNext() {
00928     }
00929 
00937     protected function getRelPath( $dir ) {
00938         $path = realpath( $dir );
00939         if ( $path === false ) {
00940             $path = $dir;
00941         }
00942 
00943         return strtr( substr( $path, $this->suffixStart ), '\\', '/' );
00944     }
00945 }
00946 
00947 class FSFileBackendDirList extends FSFileBackendList {
00948     protected function filterViaNext() {
00949         while ( $this->iter->valid() ) {
00950             if ( $this->iter->current()->isDot() || !$this->iter->current()->isDir() ) {
00951                 $this->iter->next(); // skip non-directories and dot files
00952             } else {
00953                 break;
00954             }
00955         }
00956     }
00957 }
00958 
00959 class FSFileBackendFileList extends FSFileBackendList {
00960     protected function filterViaNext() {
00961         while ( $this->iter->valid() ) {
00962             if ( !$this->iter->current()->isFile() ) {
00963                 $this->iter->next(); // skip non-files and dot files
00964             } else {
00965                 break;
00966             }
00967         }
00968     }
00969 }