[ Index ]

PHP Cross Reference of MediaWiki-1.24.0

title

Body

[close]

/includes/filebackend/ -> FSFileBackend.php (source)

   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  }


Generated: Fri Nov 28 14:03:12 2014 Cross-referenced by PHPXref 0.7.1