X-Git-Url: https://git.cyclocoop.org/%28%28?a=blobdiff_plain;f=includes%2Flibs%2Ffilebackend%2FFSFileBackend.php;h=14bf616c26a9ecf98b4df15a1862ba80f09d73a5;hb=28a23740d89cf8aa1bcca7cde04366477e543de8;hp=c05dc286c81e60c8896fe7054c6e18c841fc6d45;hpb=cc41773f61c23b922fe4cfc513b105ec4d32eeec;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/libs/filebackend/FSFileBackend.php b/includes/libs/filebackend/FSFileBackend.php index c05dc286c8..14bf616c26 100644 --- a/includes/libs/filebackend/FSFileBackend.php +++ b/includes/libs/filebackend/FSFileBackend.php @@ -40,6 +40,7 @@ * @file * @ingroup FileBackend */ +use Wikimedia\AtEase\AtEase; use Wikimedia\Timestamp\ConvertibleTimestamp; /** @@ -63,7 +64,7 @@ class FSFileBackend extends FileBackendStore { protected $basePath; /** @var array Map of container names to root paths for custom container paths */ - protected $containerPaths = []; + protected $containerPaths; /** @var int File permission mode */ protected $fileMode; @@ -73,7 +74,7 @@ class FSFileBackend extends FileBackendStore { /** @var string Required OS username to own files */ protected $fileOwner; - /** @var bool */ + /** @var bool Whether the OS is Windows (otherwise assumed Unix-like)*/ protected $isWindows; /** @var string OS username running this script */ protected $currentUser; @@ -102,11 +103,9 @@ class FSFileBackend extends FileBackendStore { $this->basePath = null; // none; containers must have explicit paths } - if ( isset( $config['containerPaths'] ) ) { - $this->containerPaths = (array)$config['containerPaths']; - foreach ( $this->containerPaths as &$path ) { - $path = rtrim( $path, '/' ); // remove trailing slash - } + $this->containerPaths = []; + foreach ( ( $config['containerPaths'] ?? [] ) as $container => $path ) { + $this->containerPaths[$container] = rtrim( $path, '/' ); // remove trailing slash } $this->fileMode = $config['fileMode'] ?? 0644; @@ -137,7 +136,7 @@ class FSFileBackend extends FileBackendStore { } } - return null; + return null; // invalid } /** @@ -228,20 +227,12 @@ class FSFileBackend extends FileBackendStore { } if ( !empty( $params['async'] ) ) { // deferred - $tempFile = $this->tmpFileFactory->newTempFSFile( 'create_', 'tmp' ); + $tempFile = $this->stageContentAsTempFile( $params ); if ( !$tempFile ) { $status->fatal( 'backend-fail-create', $params['dst'] ); return $status; } - $this->trapWarnings(); - $bytes = file_put_contents( $tempFile->getPath(), $params['content'] ); - $this->untrapWarnings(); - if ( $bytes === false ) { - $status->fatal( 'backend-fail-create', $params['dst'] ); - - return $status; - } $cmd = implode( ' ', [ $this->isWindows ? 'COPY /B /Y' : 'cp', // (binary, overwrite) escapeshellarg( $this->cleanPathSlashes( $tempFile->getPath() ) ), @@ -457,9 +448,7 @@ class FSFileBackend extends FileBackendStore { }; $status->value = new FSFileOpHandle( $this, $params, $handler, $cmd ); } else { // immediate write - $this->trapWarnings(); - $ok = unlink( $source ); - $this->untrapWarnings(); + $ok = $this->unlink( $source ); if ( !$ok ) { $status->fatal( 'backend-fail-delete', $params['src'] ); @@ -483,7 +472,7 @@ class FSFileBackend extends FileBackendStore { $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot; $existed = is_dir( $dir ); // already there? // Create the directory and its parents as needed... - $this->trapWarnings(); + AtEase::suppressWarnings(); if ( !$existed && !mkdir( $dir, $this->dirMode, true ) && !is_dir( $dir ) ) { $this->logger->error( __METHOD__ . ": cannot create directory $dir" ); $status->fatal( 'directorycreateerror', $params['dir'] ); // fails on races @@ -494,7 +483,7 @@ class FSFileBackend extends FileBackendStore { $this->logger->error( __METHOD__ . ": directory $dir is not readable" ); $status->fatal( 'directorynotreadableerror', $params['dir'] ); } - $this->untrapWarnings(); + AtEase::restoreWarnings(); // Respect any 'noAccess' or 'noListing' flags... if ( is_dir( $dir ) && !$existed ) { $status->merge( $this->doSecureInternal( $fullCont, $dirRel, $params ) ); @@ -519,9 +508,9 @@ class FSFileBackend extends FileBackendStore { } // Add a .htaccess file to the root of the container... if ( !empty( $params['noAccess'] ) && !file_exists( "{$contRoot}/.htaccess" ) ) { - $this->trapWarnings(); + AtEase::suppressWarnings(); $bytes = file_put_contents( "{$contRoot}/.htaccess", $this->htaccessPrivate() ); - $this->untrapWarnings(); + AtEase::restoreWarnings(); if ( $bytes === false ) { $storeDir = "mwstore://{$this->name}/{$shortCont}"; $status->fatal( 'backend-fail-create', "{$storeDir}/.htaccess" ); @@ -539,21 +528,17 @@ class FSFileBackend extends FileBackendStore { // Unseed new directories with a blank index.html, to allow crawling... if ( !empty( $params['listing'] ) && is_file( "{$dir}/index.html" ) ) { $exists = ( file_get_contents( "{$dir}/index.html" ) === $this->indexHtmlPrivate() ); - $this->trapWarnings(); - if ( $exists && !unlink( "{$dir}/index.html" ) ) { // reverse secure() + if ( $exists && !$this->unlink( "{$dir}/index.html" ) ) { // reverse secure() $status->fatal( 'backend-fail-delete', $params['dir'] . '/index.html' ); } - $this->untrapWarnings(); } // Remove the .htaccess file from the root of the container... if ( !empty( $params['access'] ) && is_file( "{$contRoot}/.htaccess" ) ) { $exists = ( file_get_contents( "{$contRoot}/.htaccess" ) === $this->htaccessPrivate() ); - $this->trapWarnings(); - if ( $exists && !unlink( "{$contRoot}/.htaccess" ) ) { // reverse secure() + if ( $exists && !$this->unlink( "{$contRoot}/.htaccess" ) ) { // reverse secure() $storeDir = "mwstore://{$this->name}/{$shortCont}"; $status->fatal( 'backend-fail-delete', "{$storeDir}/.htaccess" ); } - $this->untrapWarnings(); } return $status; @@ -564,11 +549,11 @@ class FSFileBackend extends FileBackendStore { list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] ); $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot; - $this->trapWarnings(); + AtEase::suppressWarnings(); if ( is_dir( $dir ) ) { rmdir( $dir ); // remove directory if empty } - $this->untrapWarnings(); + AtEase::restoreWarnings(); return $status; } @@ -576,25 +561,23 @@ class FSFileBackend extends FileBackendStore { protected function doGetFileStat( array $params ) { $source = $this->resolveToFSPath( $params['src'] ); if ( $source === null ) { - return false; // invalid storage path + return self::$RES_ERROR; // invalid storage path } $this->trapWarnings(); // don't trust 'false' if there were errors $stat = is_file( $source ) ? stat( $source ) : false; // regular files only $hadError = $this->untrapWarnings(); - if ( $stat ) { + if ( is_array( $stat ) ) { $ct = new ConvertibleTimestamp( $stat['mtime'] ); return [ 'mtime' => $ct->getTimestamp( TS_MW ), 'size' => $stat['size'] ]; - } elseif ( !$hadError ) { - return false; // file does not exist - } else { - return null; // failure } + + return $hadError ? self::$RES_ERROR : self::$RES_ABSENT; } protected function doClearCache( array $paths = null ) { @@ -610,7 +593,7 @@ class FSFileBackend extends FileBackendStore { $exists = is_dir( $dir ); $hadError = $this->untrapWarnings(); - return $hadError ? null : $exists; + return $hadError ? self::$RES_ERROR : $exists; } /** @@ -624,18 +607,27 @@ class FSFileBackend extends FileBackendStore { list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] ); $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot; + + $this->trapWarnings(); // don't trust 'false' if there were errors $exists = is_dir( $dir ); - if ( !$exists ) { - $this->logger->warning( __METHOD__ . "() given directory does not exist: '$dir'\n" ); + $isReadable = $exists ? is_readable( $dir ) : false; + $hadError = $this->untrapWarnings(); - return []; // nothing under this dir - } elseif ( !is_readable( $dir ) ) { - $this->logger->warning( __METHOD__ . "() given directory is unreadable: '$dir'\n" ); + if ( $isReadable ) { + return new FSFileBackendDirList( $dir, $params ); + } elseif ( $exists ) { + $this->logger->warning( __METHOD__ . ": given directory is unreadable: '$dir'" ); - return null; // bad permissions? - } + return self::$RES_ERROR; // bad permissions? + } elseif ( $hadError ) { + $this->logger->warning( __METHOD__ . ": given directory was unreachable: '$dir'" ); - return new FSFileBackendDirList( $dir, $params ); + return self::$RES_ERROR; + } else { + $this->logger->info( __METHOD__ . ": given directory does not exist: '$dir'" ); + + return []; // nothing under this dir + } } /** @@ -649,18 +641,27 @@ class FSFileBackend extends FileBackendStore { list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] ); $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot; + + $this->trapWarnings(); // don't trust 'false' if there were errors $exists = is_dir( $dir ); - if ( !$exists ) { - $this->logger->warning( __METHOD__ . "() given directory does not exist: '$dir'\n" ); + $isReadable = $exists ? is_readable( $dir ) : false; + $hadError = $this->untrapWarnings(); - return []; // nothing under this dir - } elseif ( !is_readable( $dir ) ) { - $this->logger->warning( __METHOD__ . "() given directory is unreadable: '$dir'\n" ); + if ( $exists && $isReadable ) { + return new FSFileBackendFileList( $dir, $params ); + } elseif ( $exists ) { + $this->logger->warning( __METHOD__ . ": given directory is unreadable: '$dir'\n" ); - return null; // bad permissions? - } + return self::$RES_ERROR; // bad permissions? + } elseif ( $hadError ) { + $this->logger->warning( __METHOD__ . ": given directory was unreachable: '$dir'\n" ); + + return self::$RES_ERROR; + } else { + $this->logger->info( __METHOD__ . ": given directory does not exist: '$dir'\n" ); - return new FSFileBackendFileList( $dir, $params ); + return []; // nothing under this dir + } } protected function doGetLocalReferenceMulti( array $params ) { @@ -668,10 +669,21 @@ class FSFileBackend extends FileBackendStore { foreach ( $params['srcs'] as $src ) { $source = $this->resolveToFSPath( $src ); - if ( $source === null || !is_file( $source ) ) { - $fsFiles[$src] = null; // invalid path or file does not exist - } else { + if ( $source === null ) { + $fsFiles[$src] = self::$RES_ERROR; // invalid path + continue; + } + + $this->trapWarnings(); // don't trust 'false' if there were errors + $isFile = is_file( $source ); // regular files only + $hadError = $this->untrapWarnings(); + + if ( $isFile ) { $fsFiles[$src] = new FSFile( $source ); + } elseif ( $hadError ) { + $fsFiles[$src] = self::$RES_ERROR; + } else { + $fsFiles[$src] = self::$RES_ABSENT; } } @@ -684,26 +696,31 @@ class FSFileBackend extends FileBackendStore { foreach ( $params['srcs'] as $src ) { $source = $this->resolveToFSPath( $src ); if ( $source === null ) { - $tmpFiles[$src] = null; // invalid path + $tmpFiles[$src] = self::$RES_ERROR; // invalid path + continue; + } + // Create a new temporary file with the same extension... + $ext = FileBackend::extensionFromPath( $src ); + $tmpFile = $this->tmpFileFactory->newTempFSFile( 'localcopy_', $ext ); + if ( !$tmpFile ) { + $tmpFiles[$src] = self::$RES_ERROR; + continue; + } + + $tmpPath = $tmpFile->getPath(); + // Copy the source file over the temp file + $this->trapWarnings(); + $isFile = is_file( $source ); // regular files only + $copySuccess = $isFile ? copy( $source, $tmpPath ) : false; + $hadError = $this->untrapWarnings(); + + if ( $copySuccess ) { + $this->chmod( $tmpPath ); + $tmpFiles[$src] = $tmpFile; + } elseif ( $hadError ) { + $tmpFiles[$src] = self::$RES_ERROR; // copy failed } else { - // Create a new temporary file with the same extension... - $ext = FileBackend::extensionFromPath( $src ); - $tmpFile = $this->tmpFileFactory->newTempFSFile( 'localcopy_', $ext ); - if ( !$tmpFile ) { - $tmpFiles[$src] = null; - } else { - $tmpPath = $tmpFile->getPath(); - // Copy the source file over the temp file - $this->trapWarnings(); - $ok = copy( $source, $tmpPath ); - $this->untrapWarnings(); - if ( !$ok ) { - $tmpFiles[$src] = null; - } else { - $this->chmod( $tmpPath ); - $tmpFiles[$src] = $tmpFile; - } - } + $tmpFiles[$src] = self::$RES_ABSENT; } } @@ -723,8 +740,19 @@ class FSFileBackend extends FileBackendStore { $statuses = []; $pipes = []; + $octalPermissions = '0' . decoct( $this->fileMode ); foreach ( $fileOpHandles as $index => $fileOpHandle ) { - $pipes[$index] = popen( "{$fileOpHandle->cmd} 2>&1", 'r' ); + $cmd = "{$fileOpHandle->cmd} 2>&1"; + // Add a post-operation chmod command for permissions cleanup if applicable + if ( + !$this->isWindows && + $fileOpHandle->chmodPath !== null && + strlen( $octalPermissions ) == 4 + ) { + $encPath = escapeshellarg( $fileOpHandle->chmodPath ); + $cmd .= " && chmod $octalPermissions $encPath 2>/dev/null"; + } + $pipes[$index] = popen( $cmd, 'r' ); } $errs = []; @@ -740,12 +768,10 @@ class FSFileBackend extends FileBackendStore { $function = $fileOpHandle->call; $function( $errs[$index], $status, $fileOpHandle->params, $fileOpHandle->cmd ); $statuses[$index] = $status; - if ( $status->isOK() && $fileOpHandle->chmodPath ) { - $this->chmod( $fileOpHandle->chmodPath ); - } } clearstatcache(); // files changed + return $statuses; } @@ -756,13 +782,52 @@ class FSFileBackend extends FileBackendStore { * @return bool Success */ protected function chmod( $path ) { - $this->trapWarnings(); + if ( $this->isWindows ) { + return true; + } + + AtEase::suppressWarnings(); $ok = chmod( $path, $this->fileMode ); - $this->untrapWarnings(); + AtEase::restoreWarnings(); + + return $ok; + } + + /** + * Unlink a file, suppressing the warnings + * + * @param string $path Absolute file system path + * @return bool Success + */ + protected function unlink( $path ) { + AtEase::suppressWarnings(); + $ok = unlink( $path ); + AtEase::restoreWarnings(); return $ok; } + /** + * @param array $params Operation parameters with 'content' and 'headers' fields + * @return TempFSFile|null + */ + protected function stageContentAsTempFile( array $params ) { + $content = $params['content']; + $tempFile = $this->tmpFileFactory->newTempFSFile( 'create_', 'tmp' ); + if ( !$tempFile ) { + return null; + } + + AtEase::suppressWarnings(); + $tmpPath = $tempFile->getPath(); + if ( file_put_contents( $tmpPath, $content ) === false ) { + $tempFile = null; + } + AtEase::restoreWarnings(); + + return $tempFile; + } + /** * Return the text of an index.html file to hide directory listings *