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