[ Index ] |
PHP Cross Reference of MediaWiki-1.24.0 |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * File system based backend. 4 * 5 * This program is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation; either version 2 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License along 16 * with this program; if not, write to the Free Software Foundation, Inc., 17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 * http://www.gnu.org/copyleft/gpl.html 19 * 20 * @file 21 * @ingroup FileBackend 22 * @author Aaron Schulz 23 */ 24 25 /** 26 * @brief Class for a file system (FS) based file backend. 27 * 28 * All "containers" each map to a directory under the backend's base directory. 29 * For backwards-compatibility, some container paths can be set to custom paths. 30 * The wiki ID will not be used in any custom paths, so this should be avoided. 31 * 32 * Having directories with thousands of files will diminish performance. 33 * Sharding can be accomplished by using FileRepo-style hash paths. 34 * 35 * Status messages should avoid mentioning the internal FS paths. 36 * PHP warnings are assumed to be logged rather than output. 37 * 38 * @ingroup FileBackend 39 * @since 1.19 40 */ 41 class FSFileBackend extends FileBackendStore { 42 /** @var string Directory holding the container directories */ 43 protected $basePath; 44 45 /** @var array Map of container names to root paths for custom container paths */ 46 protected $containerPaths = array(); 47 48 /** @var int File permission mode */ 49 protected $fileMode; 50 51 /** @var string Required OS username to own files */ 52 protected $fileOwner; 53 54 /** @var string OS username running this script */ 55 protected $currentUser; 56 57 /** @var array */ 58 protected $hadWarningErrors = array(); 59 60 /** 61 * @see FileBackendStore::__construct() 62 * Additional $config params include: 63 * - basePath : File system directory that holds containers. 64 * - containerPaths : Map of container names to custom file system directories. 65 * This should only be used for backwards-compatibility. 66 * - fileMode : Octal UNIX file permissions to use on files stored. 67 */ 68 public function __construct( array $config ) { 69 parent::__construct( $config ); 70 71 // Remove any possible trailing slash from directories 72 if ( isset( $config['basePath'] ) ) { 73 $this->basePath = rtrim( $config['basePath'], '/' ); // remove trailing slash 74 } else { 75 $this->basePath = null; // none; containers must have explicit paths 76 } 77 78 if ( isset( $config['containerPaths'] ) ) { 79 $this->containerPaths = (array)$config['containerPaths']; 80 foreach ( $this->containerPaths as &$path ) { 81 $path = rtrim( $path, '/' ); // remove trailing slash 82 } 83 } 84 85 $this->fileMode = isset( $config['fileMode'] ) ? $config['fileMode'] : 0644; 86 if ( isset( $config['fileOwner'] ) && function_exists( 'posix_getuid' ) ) { 87 $this->fileOwner = $config['fileOwner']; 88 $info = posix_getpwuid( posix_getuid() ); 89 $this->currentUser = $info['name']; // cache this, assuming it doesn't change 90 } 91 } 92 93 public function getFeatures() { 94 return !wfIsWindows() ? FileBackend::ATTR_UNICODE_PATHS : 0; 95 } 96 97 protected function resolveContainerPath( $container, $relStoragePath ) { 98 // Check that container has a root directory 99 if ( isset( $this->containerPaths[$container] ) || isset( $this->basePath ) ) { 100 // Check for sane relative paths (assume the base paths are OK) 101 if ( $this->isLegalRelPath( $relStoragePath ) ) { 102 return $relStoragePath; 103 } 104 } 105 106 return null; 107 } 108 109 /** 110 * Sanity check a relative file system path for validity 111 * 112 * @param string $path Normalized relative path 113 * @return bool 114 */ 115 protected function isLegalRelPath( $path ) { 116 // Check for file names longer than 255 chars 117 if ( preg_match( '![^/]{256}!', $path ) ) { // ext3/NTFS 118 return false; 119 } 120 if ( wfIsWindows() ) { // NTFS 121 return !preg_match( '![:*?"<>|]!', $path ); 122 } else { 123 return true; 124 } 125 } 126 127 /** 128 * Given the short (unresolved) and full (resolved) name of 129 * a container, return the file system path of the container. 130 * 131 * @param string $shortCont 132 * @param string $fullCont 133 * @return string|null 134 */ 135 protected function containerFSRoot( $shortCont, $fullCont ) { 136 if ( isset( $this->containerPaths[$shortCont] ) ) { 137 return $this->containerPaths[$shortCont]; 138 } elseif ( isset( $this->basePath ) ) { 139 return "{$this->basePath}/{$fullCont}"; 140 } 141 142 return null; // no container base path defined 143 } 144 145 /** 146 * Get the absolute file system path for a storage path 147 * 148 * @param string $storagePath Storage path 149 * @return string|null 150 */ 151 protected function resolveToFSPath( $storagePath ) { 152 list( $fullCont, $relPath ) = $this->resolveStoragePathReal( $storagePath ); 153 if ( $relPath === null ) { 154 return null; // invalid 155 } 156 list( , $shortCont, ) = FileBackend::splitStoragePath( $storagePath ); 157 $fsPath = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid 158 if ( $relPath != '' ) { 159 $fsPath .= "/{$relPath}"; 160 } 161 162 return $fsPath; 163 } 164 165 public function isPathUsableInternal( $storagePath ) { 166 $fsPath = $this->resolveToFSPath( $storagePath ); 167 if ( $fsPath === null ) { 168 return false; // invalid 169 } 170 $parentDir = dirname( $fsPath ); 171 172 if ( file_exists( $fsPath ) ) { 173 $ok = is_file( $fsPath ) && is_writable( $fsPath ); 174 } else { 175 $ok = is_dir( $parentDir ) && is_writable( $parentDir ); 176 } 177 178 if ( $this->fileOwner !== null && $this->currentUser !== $this->fileOwner ) { 179 $ok = false; 180 trigger_error( __METHOD__ . ": PHP process owner is not '{$this->fileOwner}'." ); 181 } 182 183 return $ok; 184 } 185 186 protected function doCreateInternal( array $params ) { 187 $status = Status::newGood(); 188 189 $dest = $this->resolveToFSPath( $params['dst'] ); 190 if ( $dest === null ) { 191 $status->fatal( 'backend-fail-invalidpath', $params['dst'] ); 192 193 return $status; 194 } 195 196 if ( !empty( $params['async'] ) ) { // deferred 197 $tempFile = TempFSFile::factory( 'create_', 'tmp' ); 198 if ( !$tempFile ) { 199 $status->fatal( 'backend-fail-create', $params['dst'] ); 200 201 return $status; 202 } 203 $this->trapWarnings(); 204 $bytes = file_put_contents( $tempFile->getPath(), $params['content'] ); 205 $this->untrapWarnings(); 206 if ( $bytes === false ) { 207 $status->fatal( 'backend-fail-create', $params['dst'] ); 208 209 return $status; 210 } 211 $cmd = implode( ' ', array( 212 wfIsWindows() ? 'COPY /B /Y' : 'cp', // (binary, overwrite) 213 wfEscapeShellArg( $this->cleanPathSlashes( $tempFile->getPath() ) ), 214 wfEscapeShellArg( $this->cleanPathSlashes( $dest ) ) 215 ) ); 216 $handler = function ( $errors, Status $status, array $params, $cmd ) { 217 if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) { 218 $status->fatal( 'backend-fail-create', $params['dst'] ); 219 trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output 220 } 221 }; 222 $status->value = new FSFileOpHandle( $this, $params, $handler, $cmd, $dest ); 223 $tempFile->bind( $status->value ); 224 } else { // immediate write 225 $this->trapWarnings(); 226 $bytes = file_put_contents( $dest, $params['content'] ); 227 $this->untrapWarnings(); 228 if ( $bytes === false ) { 229 $status->fatal( 'backend-fail-create', $params['dst'] ); 230 231 return $status; 232 } 233 $this->chmod( $dest ); 234 } 235 236 return $status; 237 } 238 239 protected function doStoreInternal( array $params ) { 240 $status = Status::newGood(); 241 242 $dest = $this->resolveToFSPath( $params['dst'] ); 243 if ( $dest === null ) { 244 $status->fatal( 'backend-fail-invalidpath', $params['dst'] ); 245 246 return $status; 247 } 248 249 if ( !empty( $params['async'] ) ) { // deferred 250 $cmd = implode( ' ', array( 251 wfIsWindows() ? 'COPY /B /Y' : 'cp', // (binary, overwrite) 252 wfEscapeShellArg( $this->cleanPathSlashes( $params['src'] ) ), 253 wfEscapeShellArg( $this->cleanPathSlashes( $dest ) ) 254 ) ); 255 $handler = function ( $errors, Status $status, array $params, $cmd ) { 256 if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) { 257 $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] ); 258 trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output 259 } 260 }; 261 $status->value = new FSFileOpHandle( $this, $params, $handler, $cmd, $dest ); 262 } else { // immediate write 263 $this->trapWarnings(); 264 $ok = copy( $params['src'], $dest ); 265 $this->untrapWarnings(); 266 // In some cases (at least over NFS), copy() returns true when it fails 267 if ( !$ok || ( filesize( $params['src'] ) !== filesize( $dest ) ) ) { 268 if ( $ok ) { // PHP bug 269 unlink( $dest ); // remove broken file 270 trigger_error( __METHOD__ . ": copy() failed but returned true." ); 271 } 272 $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] ); 273 274 return $status; 275 } 276 $this->chmod( $dest ); 277 } 278 279 return $status; 280 } 281 282 protected function doCopyInternal( array $params ) { 283 $status = Status::newGood(); 284 285 $source = $this->resolveToFSPath( $params['src'] ); 286 if ( $source === null ) { 287 $status->fatal( 'backend-fail-invalidpath', $params['src'] ); 288 289 return $status; 290 } 291 292 $dest = $this->resolveToFSPath( $params['dst'] ); 293 if ( $dest === null ) { 294 $status->fatal( 'backend-fail-invalidpath', $params['dst'] ); 295 296 return $status; 297 } 298 299 if ( !is_file( $source ) ) { 300 if ( empty( $params['ignoreMissingSource'] ) ) { 301 $status->fatal( 'backend-fail-copy', $params['src'] ); 302 } 303 304 return $status; // do nothing; either OK or bad status 305 } 306 307 if ( !empty( $params['async'] ) ) { // deferred 308 $cmd = implode( ' ', array( 309 wfIsWindows() ? 'COPY /B /Y' : 'cp', // (binary, overwrite) 310 wfEscapeShellArg( $this->cleanPathSlashes( $source ) ), 311 wfEscapeShellArg( $this->cleanPathSlashes( $dest ) ) 312 ) ); 313 $handler = function ( $errors, Status $status, array $params, $cmd ) { 314 if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) { 315 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] ); 316 trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output 317 } 318 }; 319 $status->value = new FSFileOpHandle( $this, $params, $handler, $cmd, $dest ); 320 } else { // immediate write 321 $this->trapWarnings(); 322 $ok = ( $source === $dest ) ? true : copy( $source, $dest ); 323 $this->untrapWarnings(); 324 // In some cases (at least over NFS), copy() returns true when it fails 325 if ( !$ok || ( filesize( $source ) !== filesize( $dest ) ) ) { 326 if ( $ok ) { // PHP bug 327 $this->trapWarnings(); 328 unlink( $dest ); // remove broken file 329 $this->untrapWarnings(); 330 trigger_error( __METHOD__ . ": copy() failed but returned true." ); 331 } 332 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] ); 333 334 return $status; 335 } 336 $this->chmod( $dest ); 337 } 338 339 return $status; 340 } 341 342 protected function doMoveInternal( array $params ) { 343 $status = Status::newGood(); 344 345 $source = $this->resolveToFSPath( $params['src'] ); 346 if ( $source === null ) { 347 $status->fatal( 'backend-fail-invalidpath', $params['src'] ); 348 349 return $status; 350 } 351 352 $dest = $this->resolveToFSPath( $params['dst'] ); 353 if ( $dest === null ) { 354 $status->fatal( 'backend-fail-invalidpath', $params['dst'] ); 355 356 return $status; 357 } 358 359 if ( !is_file( $source ) ) { 360 if ( empty( $params['ignoreMissingSource'] ) ) { 361 $status->fatal( 'backend-fail-move', $params['src'] ); 362 } 363 364 return $status; // do nothing; either OK or bad status 365 } 366 367 if ( !empty( $params['async'] ) ) { // deferred 368 $cmd = implode( ' ', array( 369 wfIsWindows() ? 'MOVE /Y' : 'mv', // (overwrite) 370 wfEscapeShellArg( $this->cleanPathSlashes( $source ) ), 371 wfEscapeShellArg( $this->cleanPathSlashes( $dest ) ) 372 ) ); 373 $handler = function ( $errors, Status $status, array $params, $cmd ) { 374 if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) { 375 $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] ); 376 trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output 377 } 378 }; 379 $status->value = new FSFileOpHandle( $this, $params, $handler, $cmd ); 380 } else { // immediate write 381 $this->trapWarnings(); 382 $ok = ( $source === $dest ) ? true : rename( $source, $dest ); 383 $this->untrapWarnings(); 384 clearstatcache(); // file no longer at source 385 if ( !$ok ) { 386 $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] ); 387 388 return $status; 389 } 390 } 391 392 return $status; 393 } 394 395 protected function doDeleteInternal( array $params ) { 396 $status = Status::newGood(); 397 398 $source = $this->resolveToFSPath( $params['src'] ); 399 if ( $source === null ) { 400 $status->fatal( 'backend-fail-invalidpath', $params['src'] ); 401 402 return $status; 403 } 404 405 if ( !is_file( $source ) ) { 406 if ( empty( $params['ignoreMissingSource'] ) ) { 407 $status->fatal( 'backend-fail-delete', $params['src'] ); 408 } 409 410 return $status; // do nothing; either OK or bad status 411 } 412 413 if ( !empty( $params['async'] ) ) { // deferred 414 $cmd = implode( ' ', array( 415 wfIsWindows() ? 'DEL' : 'unlink', 416 wfEscapeShellArg( $this->cleanPathSlashes( $source ) ) 417 ) ); 418 $handler = function ( $errors, Status $status, array $params, $cmd ) { 419 if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) { 420 $status->fatal( 'backend-fail-delete', $params['src'] ); 421 trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output 422 } 423 }; 424 $status->value = new FSFileOpHandle( $this, $params, $handler, $cmd ); 425 } else { // immediate write 426 $this->trapWarnings(); 427 $ok = unlink( $source ); 428 $this->untrapWarnings(); 429 if ( !$ok ) { 430 $status->fatal( 'backend-fail-delete', $params['src'] ); 431 432 return $status; 433 } 434 } 435 436 return $status; 437 } 438 439 /** 440 * @param string $fullCont 441 * @param string $dirRel 442 * @param array $params 443 * @return Status 444 */ 445 protected function doPrepareInternal( $fullCont, $dirRel, array $params ) { 446 $status = Status::newGood(); 447 list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] ); 448 $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid 449 $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot; 450 $existed = is_dir( $dir ); // already there? 451 // Create the directory and its parents as needed... 452 $this->trapWarnings(); 453 if ( !wfMkdirParents( $dir ) ) { 454 $status->fatal( 'directorycreateerror', $params['dir'] ); // fails on races 455 } elseif ( !is_writable( $dir ) ) { 456 $status->fatal( 'directoryreadonlyerror', $params['dir'] ); 457 } elseif ( !is_readable( $dir ) ) { 458 $status->fatal( 'directorynotreadableerror', $params['dir'] ); 459 } 460 $this->untrapWarnings(); 461 // Respect any 'noAccess' or 'noListing' flags... 462 if ( is_dir( $dir ) && !$existed ) { 463 $status->merge( $this->doSecureInternal( $fullCont, $dirRel, $params ) ); 464 } 465 466 return $status; 467 } 468 469 protected function doSecureInternal( $fullCont, $dirRel, array $params ) { 470 $status = Status::newGood(); 471 list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] ); 472 $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid 473 $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot; 474 // Seed new directories with a blank index.html, to prevent crawling... 475 if ( !empty( $params['noListing'] ) && !file_exists( "{$dir}/index.html" ) ) { 476 $this->trapWarnings(); 477 $bytes = file_put_contents( "{$dir}/index.html", $this->indexHtmlPrivate() ); 478 $this->untrapWarnings(); 479 if ( $bytes === false ) { 480 $status->fatal( 'backend-fail-create', $params['dir'] . '/index.html' ); 481 } 482 } 483 // Add a .htaccess file to the root of the container... 484 if ( !empty( $params['noAccess'] ) && !file_exists( "{$contRoot}/.htaccess" ) ) { 485 $this->trapWarnings(); 486 $bytes = file_put_contents( "{$contRoot}/.htaccess", $this->htaccessPrivate() ); 487 $this->untrapWarnings(); 488 if ( $bytes === false ) { 489 $storeDir = "mwstore://{$this->name}/{$shortCont}"; 490 $status->fatal( 'backend-fail-create', "{$storeDir}/.htaccess" ); 491 } 492 } 493 494 return $status; 495 } 496 497 protected function doPublishInternal( $fullCont, $dirRel, array $params ) { 498 $status = Status::newGood(); 499 list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] ); 500 $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid 501 $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot; 502 // Unseed new directories with a blank index.html, to allow crawling... 503 if ( !empty( $params['listing'] ) && is_file( "{$dir}/index.html" ) ) { 504 $exists = ( file_get_contents( "{$dir}/index.html" ) === $this->indexHtmlPrivate() ); 505 $this->trapWarnings(); 506 if ( $exists && !unlink( "{$dir}/index.html" ) ) { // reverse secure() 507 $status->fatal( 'backend-fail-delete', $params['dir'] . '/index.html' ); 508 } 509 $this->untrapWarnings(); 510 } 511 // Remove the .htaccess file from the root of the container... 512 if ( !empty( $params['access'] ) && is_file( "{$contRoot}/.htaccess" ) ) { 513 $exists = ( file_get_contents( "{$contRoot}/.htaccess" ) === $this->htaccessPrivate() ); 514 $this->trapWarnings(); 515 if ( $exists && !unlink( "{$contRoot}/.htaccess" ) ) { // reverse secure() 516 $storeDir = "mwstore://{$this->name}/{$shortCont}"; 517 $status->fatal( 'backend-fail-delete', "{$storeDir}/.htaccess" ); 518 } 519 $this->untrapWarnings(); 520 } 521 522 return $status; 523 } 524 525 protected function doCleanInternal( $fullCont, $dirRel, array $params ) { 526 $status = Status::newGood(); 527 list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] ); 528 $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid 529 $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot; 530 $this->trapWarnings(); 531 if ( is_dir( $dir ) ) { 532 rmdir( $dir ); // remove directory if empty 533 } 534 $this->untrapWarnings(); 535 536 return $status; 537 } 538 539 protected function doGetFileStat( array $params ) { 540 $source = $this->resolveToFSPath( $params['src'] ); 541 if ( $source === null ) { 542 return false; // invalid storage path 543 } 544 545 $this->trapWarnings(); // don't trust 'false' if there were errors 546 $stat = is_file( $source ) ? stat( $source ) : false; // regular files only 547 $hadError = $this->untrapWarnings(); 548 549 if ( $stat ) { 550 return array( 551 'mtime' => wfTimestamp( TS_MW, $stat['mtime'] ), 552 'size' => $stat['size'] 553 ); 554 } elseif ( !$hadError ) { 555 return false; // file does not exist 556 } else { 557 return null; // failure 558 } 559 } 560 561 /** 562 * @see FileBackendStore::doClearCache() 563 */ 564 protected function doClearCache( array $paths = null ) { 565 clearstatcache(); // clear the PHP file stat cache 566 } 567 568 protected function doDirectoryExists( $fullCont, $dirRel, array $params ) { 569 list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] ); 570 $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid 571 $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot; 572 573 $this->trapWarnings(); // don't trust 'false' if there were errors 574 $exists = is_dir( $dir ); 575 $hadError = $this->untrapWarnings(); 576 577 return $hadError ? null : $exists; 578 } 579 580 /** 581 * @see FileBackendStore::getDirectoryListInternal() 582 * @param string $fullCont 583 * @param string $dirRel 584 * @param array $params 585 * @return array|null 586 */ 587 public function getDirectoryListInternal( $fullCont, $dirRel, array $params ) { 588 list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] ); 589 $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid 590 $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot; 591 $exists = is_dir( $dir ); 592 if ( !$exists ) { 593 wfDebug( __METHOD__ . "() given directory does not exist: '$dir'\n" ); 594 595 return array(); // nothing under this dir 596 } elseif ( !is_readable( $dir ) ) { 597 wfDebug( __METHOD__ . "() given directory is unreadable: '$dir'\n" ); 598 599 return null; // bad permissions? 600 } 601 602 return new FSFileBackendDirList( $dir, $params ); 603 } 604 605 /** 606 * @see FileBackendStore::getFileListInternal() 607 * @param string $fullCont 608 * @param string $dirRel 609 * @param array $params 610 * @return array|FSFileBackendFileList|null 611 */ 612 public function getFileListInternal( $fullCont, $dirRel, array $params ) { 613 list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] ); 614 $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid 615 $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot; 616 $exists = is_dir( $dir ); 617 if ( !$exists ) { 618 wfDebug( __METHOD__ . "() given directory does not exist: '$dir'\n" ); 619 620 return array(); // nothing under this dir 621 } elseif ( !is_readable( $dir ) ) { 622 wfDebug( __METHOD__ . "() given directory is unreadable: '$dir'\n" ); 623 624 return null; // bad permissions? 625 } 626 627 return new FSFileBackendFileList( $dir, $params ); 628 } 629 630 protected function doGetLocalReferenceMulti( array $params ) { 631 $fsFiles = array(); // (path => FSFile) 632 633 foreach ( $params['srcs'] as $src ) { 634 $source = $this->resolveToFSPath( $src ); 635 if ( $source === null || !is_file( $source ) ) { 636 $fsFiles[$src] = null; // invalid path or file does not exist 637 } else { 638 $fsFiles[$src] = new FSFile( $source ); 639 } 640 } 641 642 return $fsFiles; 643 } 644 645 protected function doGetLocalCopyMulti( array $params ) { 646 $tmpFiles = array(); // (path => TempFSFile) 647 648 foreach ( $params['srcs'] as $src ) { 649 $source = $this->resolveToFSPath( $src ); 650 if ( $source === null ) { 651 $tmpFiles[$src] = null; // invalid path 652 } else { 653 // Create a new temporary file with the same extension... 654 $ext = FileBackend::extensionFromPath( $src ); 655 $tmpFile = TempFSFile::factory( 'localcopy_', $ext ); 656 if ( !$tmpFile ) { 657 $tmpFiles[$src] = null; 658 } else { 659 $tmpPath = $tmpFile->getPath(); 660 // Copy the source file over the temp file 661 $this->trapWarnings(); 662 $ok = copy( $source, $tmpPath ); 663 $this->untrapWarnings(); 664 if ( !$ok ) { 665 $tmpFiles[$src] = null; 666 } else { 667 $this->chmod( $tmpPath ); 668 $tmpFiles[$src] = $tmpFile; 669 } 670 } 671 } 672 } 673 674 return $tmpFiles; 675 } 676 677 protected function directoriesAreVirtual() { 678 return false; 679 } 680 681 protected function doExecuteOpHandlesInternal( array $fileOpHandles ) { 682 $statuses = array(); 683 684 $pipes = array(); 685 foreach ( $fileOpHandles as $index => $fileOpHandle ) { 686 $pipes[$index] = popen( "{$fileOpHandle->cmd} 2>&1", 'r' ); 687 } 688 689 $errs = array(); 690 foreach ( $pipes as $index => $pipe ) { 691 // Result will be empty on success in *NIX. On Windows, 692 // it may be something like " 1 file(s) [copied|moved].". 693 $errs[$index] = stream_get_contents( $pipe ); 694 fclose( $pipe ); 695 } 696 697 foreach ( $fileOpHandles as $index => $fileOpHandle ) { 698 $status = Status::newGood(); 699 $function = $fileOpHandle->call; 700 $function( $errs[$index], $status, $fileOpHandle->params, $fileOpHandle->cmd ); 701 $statuses[$index] = $status; 702 if ( $status->isOK() && $fileOpHandle->chmodPath ) { 703 $this->chmod( $fileOpHandle->chmodPath ); 704 } 705 } 706 707 clearstatcache(); // files changed 708 return $statuses; 709 } 710 711 /** 712 * Chmod a file, suppressing the warnings 713 * 714 * @param string $path Absolute file system path 715 * @return bool Success 716 */ 717 protected function chmod( $path ) { 718 $this->trapWarnings(); 719 $ok = chmod( $path, $this->fileMode ); 720 $this->untrapWarnings(); 721 722 return $ok; 723 } 724 725 /** 726 * Return the text of an index.html file to hide directory listings 727 * 728 * @return string 729 */ 730 protected function indexHtmlPrivate() { 731 return ''; 732 } 733 734 /** 735 * Return the text of a .htaccess file to make a directory private 736 * 737 * @return string 738 */ 739 protected function htaccessPrivate() { 740 return "Deny from all\n"; 741 } 742 743 /** 744 * Clean up directory separators for the given OS 745 * 746 * @param string $path FS path 747 * @return string 748 */ 749 protected function cleanPathSlashes( $path ) { 750 return wfIsWindows() ? strtr( $path, '/', '\\' ) : $path; 751 } 752 753 /** 754 * Listen for E_WARNING errors and track whether any happen 755 */ 756 protected function trapWarnings() { 757 $this->hadWarningErrors[] = false; // push to stack 758 set_error_handler( array( $this, 'handleWarning' ), E_WARNING ); 759 } 760 761 /** 762 * Stop listening for E_WARNING errors and return true if any happened 763 * 764 * @return bool 765 */ 766 protected function untrapWarnings() { 767 restore_error_handler(); // restore previous handler 768 return array_pop( $this->hadWarningErrors ); // pop from stack 769 } 770 771 /** 772 * @param int $errno 773 * @param string $errstr 774 * @return bool 775 * @access private 776 */ 777 public function handleWarning( $errno, $errstr ) { 778 wfDebugLog( 'FSFileBackend', $errstr ); // more detailed error logging 779 $this->hadWarningErrors[count( $this->hadWarningErrors ) - 1] = true; 780 781 return true; // suppress from PHP handler 782 } 783 } 784 785 /** 786 * @see FileBackendStoreOpHandle 787 */ 788 class FSFileOpHandle extends FileBackendStoreOpHandle { 789 public $cmd; // string; shell command 790 public $chmodPath; // string; file to chmod 791 792 /** 793 * @param FSFileBackend $backend 794 * @param array $params 795 * @param callable $call 796 * @param string $cmd 797 * @param int|null $chmodPath 798 */ 799 public function __construct( 800 FSFileBackend $backend, array $params, $call, $cmd, $chmodPath = null 801 ) { 802 $this->backend = $backend; 803 $this->params = $params; 804 $this->call = $call; 805 $this->cmd = $cmd; 806 $this->chmodPath = $chmodPath; 807 } 808 } 809 810 /** 811 * Wrapper around RecursiveDirectoryIterator/DirectoryIterator that 812 * catches exception or does any custom behavoir that we may want. 813 * Do not use this class from places outside FSFileBackend. 814 * 815 * @ingroup FileBackend 816 */ 817 abstract class FSFileBackendList implements Iterator { 818 /** @var Iterator */ 819 protected $iter; 820 821 /** @var int */ 822 protected $suffixStart; 823 824 /** @var int */ 825 protected $pos = 0; 826 827 /** @var array */ 828 protected $params = array(); 829 830 /** 831 * @param string $dir File system directory 832 * @param array $params 833 */ 834 public function __construct( $dir, array $params ) { 835 $path = realpath( $dir ); // normalize 836 if ( $path === false ) { 837 $path = $dir; 838 } 839 $this->suffixStart = strlen( $path ) + 1; // size of "path/to/dir/" 840 $this->params = $params; 841 842 try { 843 $this->iter = $this->initIterator( $path ); 844 } catch ( UnexpectedValueException $e ) { 845 $this->iter = null; // bad permissions? deleted? 846 } 847 } 848 849 /** 850 * Return an appropriate iterator object to wrap 851 * 852 * @param string $dir File system directory 853 * @return Iterator 854 */ 855 protected function initIterator( $dir ) { 856 if ( !empty( $this->params['topOnly'] ) ) { // non-recursive 857 # Get an iterator that will get direct sub-nodes 858 return new DirectoryIterator( $dir ); 859 } else { // recursive 860 # Get an iterator that will return leaf nodes (non-directories) 861 # RecursiveDirectoryIterator extends FilesystemIterator. 862 # FilesystemIterator::SKIP_DOTS default is inconsistent in PHP 5.3.x. 863 $flags = FilesystemIterator::CURRENT_AS_SELF | FilesystemIterator::SKIP_DOTS; 864 865 return new RecursiveIteratorIterator( 866 new RecursiveDirectoryIterator( $dir, $flags ), 867 RecursiveIteratorIterator::CHILD_FIRST // include dirs 868 ); 869 } 870 } 871 872 /** 873 * @see Iterator::key() 874 * @return int 875 */ 876 public function key() { 877 return $this->pos; 878 } 879 880 /** 881 * @see Iterator::current() 882 * @return string|bool String or false 883 */ 884 public function current() { 885 return $this->getRelPath( $this->iter->current()->getPathname() ); 886 } 887 888 /** 889 * @see Iterator::next() 890 * @throws FileBackendError 891 */ 892 public function next() { 893 try { 894 $this->iter->next(); 895 $this->filterViaNext(); 896 } catch ( UnexpectedValueException $e ) { // bad permissions? deleted? 897 throw new FileBackendError( "File iterator gave UnexpectedValueException." ); 898 } 899 ++$this->pos; 900 } 901 902 /** 903 * @see Iterator::rewind() 904 * @throws FileBackendError 905 */ 906 public function rewind() { 907 $this->pos = 0; 908 try { 909 $this->iter->rewind(); 910 $this->filterViaNext(); 911 } catch ( UnexpectedValueException $e ) { // bad permissions? deleted? 912 throw new FileBackendError( "File iterator gave UnexpectedValueException." ); 913 } 914 } 915 916 /** 917 * @see Iterator::valid() 918 * @return bool 919 */ 920 public function valid() { 921 return $this->iter && $this->iter->valid(); 922 } 923 924 /** 925 * Filter out items by advancing to the next ones 926 */ 927 protected function filterViaNext() { 928 } 929 930 /** 931 * Return only the relative path and normalize slashes to FileBackend-style. 932 * Uses the "real path" since the suffix is based upon that. 933 * 934 * @param string $dir 935 * @return string 936 */ 937 protected function getRelPath( $dir ) { 938 $path = realpath( $dir ); 939 if ( $path === false ) { 940 $path = $dir; 941 } 942 943 return strtr( substr( $path, $this->suffixStart ), '\\', '/' ); 944 } 945 } 946 947 class FSFileBackendDirList extends FSFileBackendList { 948 protected function filterViaNext() { 949 while ( $this->iter->valid() ) { 950 if ( $this->iter->current()->isDot() || !$this->iter->current()->isDir() ) { 951 $this->iter->next(); // skip non-directories and dot files 952 } else { 953 break; 954 } 955 } 956 } 957 } 958 959 class FSFileBackendFileList extends FSFileBackendList { 960 protected function filterViaNext() { 961 while ( $this->iter->valid() ) { 962 if ( !$this->iter->current()->isFile() ) { 963 $this->iter->next(); // skip non-files and dot files 964 } else { 965 break; 966 } 967 } 968 } 969 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 14:03:12 2014 | Cross-referenced by PHPXref 0.7.1 |