diff --git a/src/Composer/Autoload/AutoloadGenerator.php b/src/Composer/Autoload/AutoloadGenerator.php index 6f4191443..8d7bd73a4 100644 --- a/src/Composer/Autoload/AutoloadGenerator.php +++ b/src/Composer/Autoload/AutoloadGenerator.php @@ -346,7 +346,7 @@ EOF; $classmapFile .= ");\n"; if (!$suffix) { - if (!$config->get('autoloader-suffix') && is_readable($vendorPath.'/autoload.php')) { + if (!$config->get('autoloader-suffix') && Filesystem::isReadable($vendorPath.'/autoload.php')) { $content = file_get_contents($vendorPath.'/autoload.php'); if (preg_match('{ComposerAutoloaderInit([^:\s]+)::}', $content, $match)) { $suffix = $match[1]; @@ -1131,7 +1131,7 @@ INITIALIZER; foreach ($autoload[$type] as $namespace => $paths) { foreach ((array) $paths as $path) { - if (($type === 'files' || $type === 'classmap' || $type === 'exclude-from-classmap') && $package->getTargetDir() && !is_readable($installPath.'/'.$path)) { + if (($type === 'files' || $type === 'classmap' || $type === 'exclude-from-classmap') && $package->getTargetDir() && !Filesystem::isReadable($installPath.'/'.$path)) { // remove target-dir from file paths of the root package if ($package === $rootPackage) { $targetDir = str_replace('\\', '[\\\\/]', preg_quote(str_replace(array('/', '\\'), '', $package->getTargetDir()))); diff --git a/src/Composer/Autoload/ClassMapGenerator.php b/src/Composer/Autoload/ClassMapGenerator.php index ec8c3de5b..abb0d5e9a 100644 --- a/src/Composer/Autoload/ClassMapGenerator.php +++ b/src/Composer/Autoload/ClassMapGenerator.php @@ -225,7 +225,7 @@ class ClassMapGenerator if (!$contents) { if (!file_exists($path)) { $message = 'File at "%s" does not exist, check your classmap definitions'; - } elseif (!is_readable($path)) { + } elseif (!Filesystem::isReadable($path)) { $message = 'File at "%s" is not readable, check its permissions'; } elseif ('' === trim(file_get_contents($path))) { // The input file was really empty and thus contains no classes diff --git a/src/Composer/Command/ConfigCommand.php b/src/Composer/Command/ConfigCommand.php index b0f26deb2..232bb3742 100644 --- a/src/Composer/Command/ConfigCommand.php +++ b/src/Composer/Command/ConfigCommand.php @@ -12,6 +12,7 @@ namespace Composer\Command; +use Composer\Util\Filesystem; use Composer\Util\Platform; use Composer\Util\Silencer; use Symfony\Component\Console\Input\InputInterface; @@ -411,7 +412,7 @@ EOT 'secure-http' => array($booleanValidator, $booleanNormalizer), 'cafile' => array( function ($val) { - return file_exists($val) && is_readable($val); + return file_exists($val) && Filesystem::isReadable($val); }, function ($val) { return $val === 'null' ? null : $val; @@ -419,7 +420,7 @@ EOT ), 'capath' => array( function ($val) { - return is_dir($val) && is_readable($val); + return is_dir($val) && Filesystem::isReadable($val); }, function ($val) { return $val === 'null' ? null : $val; diff --git a/src/Composer/Command/InitCommand.php b/src/Composer/Command/InitCommand.php index 6515c94db..6b514ebf8 100644 --- a/src/Composer/Command/InitCommand.php +++ b/src/Composer/Command/InitCommand.php @@ -796,7 +796,7 @@ EOT } $file = Factory::getComposerFile(); - if (is_file($file) && is_readable($file) && is_array($composer = json_decode(file_get_contents($file), true))) { + if (is_file($file) && Filesystem::isReadable($file) && is_array($composer = json_decode(file_get_contents($file), true))) { if (!empty($composer['minimum-stability'])) { return VersionParser::normalizeStability($composer['minimum-stability']); } diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index ff98c484e..5dd9cde18 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -13,6 +13,7 @@ namespace Composer\Command; use Composer\DependencyResolver\Request; +use Composer\Util\Filesystem; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; @@ -119,9 +120,7 @@ EOT return 1; } - // check for readability by reading the file as is_readable can not be trusted on network-mounts - // see https://github.com/composer/composer/issues/8231 and https://bugs.php.net/bug.php?id=68926 - if (!is_readable($this->file) && false === Silencer::call('file_get_contents', $this->file)) { + if (!Filesystem::isReadable($this->file)) { $io->writeError(''.$this->file.' is not readable.'); return 1; diff --git a/src/Composer/Command/SelfUpdateCommand.php b/src/Composer/Command/SelfUpdateCommand.php index 54750d4ad..70e65d252 100644 --- a/src/Composer/Command/SelfUpdateCommand.php +++ b/src/Composer/Command/SelfUpdateCommand.php @@ -381,7 +381,7 @@ TAGSPUBKEY if (!is_file($oldFile)) { throw new FilesystemException('Composer rollback failed: "'.$oldFile.'" could not be found'); } - if (!is_readable($oldFile)) { + if (!Filesystem::isReadable($oldFile)) { throw new FilesystemException('Composer rollback failed: "'.$oldFile.'" could not be read'); } @@ -582,7 +582,7 @@ EOT; @unlink($script); // see if the file was moved and is still accessible - if ($result = is_readable($localFilename) && (hash_file('sha256', $localFilename) === $checksum)) { + if ($result = Filesystem::isReadable($localFilename) && (hash_file('sha256', $localFilename) === $checksum)) { $io->writeError('Operation succeeded.'); @unlink($newFilename); } else { diff --git a/src/Composer/Command/ValidateCommand.php b/src/Composer/Command/ValidateCommand.php index 128064f8e..0818c8c2f 100644 --- a/src/Composer/Command/ValidateCommand.php +++ b/src/Composer/Command/ValidateCommand.php @@ -17,6 +17,7 @@ use Composer\Package\Loader\ValidatingArrayLoader; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; use Composer\Util\ConfigValidator; +use Composer\Util\Filesystem; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -77,7 +78,7 @@ EOT return 3; } - if (!is_readable($file)) { + if (!Filesystem::isReadable($file)) { $io->writeError('' . $file . ' is not readable.'); return 3; diff --git a/src/Composer/Config/JsonConfigSource.php b/src/Composer/Config/JsonConfigSource.php index 80cf51050..4be155782 100644 --- a/src/Composer/Config/JsonConfigSource.php +++ b/src/Composer/Config/JsonConfigSource.php @@ -15,6 +15,7 @@ namespace Composer\Config; use Composer\Json\JsonFile; use Composer\Json\JsonManipulator; use Composer\Json\JsonValidationException; +use Composer\Util\Filesystem; use Composer\Util\Silencer; /** @@ -217,7 +218,7 @@ class JsonConfigSource implements ConfigSourceInterface throw new \RuntimeException(sprintf('The file "%s" is not writable.', $this->file->getPath())); } - if (!is_readable($this->file->getPath())) { + if (!Filesystem::isReadable($this->file->getPath())) { throw new \RuntimeException(sprintf('The file "%s" is not readable.', $this->file->getPath())); } diff --git a/src/Composer/Console/Application.php b/src/Composer/Console/Application.php index 3dea8abe9..8b72eb31a 100644 --- a/src/Composer/Console/Application.php +++ b/src/Composer/Console/Application.php @@ -13,6 +13,7 @@ namespace Composer\Console; use Composer\IO\NullIO; +use Composer\Util\Filesystem; use Composer\Util\Platform; use Composer\Util\Silencer; use Symfony\Component\Console\Application as BaseApplication; @@ -282,7 +283,7 @@ class Application extends BaseApplication // add non-standard scripts as own commands $file = Factory::getComposerFile(); - if (is_file($file) && is_readable($file) && is_array($composer = json_decode(file_get_contents($file), true))) { + if (is_file($file) && Filesystem::isReadable($file) && is_array($composer = json_decode(file_get_contents($file), true))) { if (isset($composer['scripts']) && is_array($composer['scripts'])) { foreach ($composer['scripts'] as $script => $dummy) { if (!defined('Composer\Script\ScriptEvents::'.str_replace('-', '_', strtoupper($script)))) { diff --git a/src/Composer/Installer/LibraryInstaller.php b/src/Composer/Installer/LibraryInstaller.php index fd1848f68..82a32520a 100644 --- a/src/Composer/Installer/LibraryInstaller.php +++ b/src/Composer/Installer/LibraryInstaller.php @@ -79,7 +79,7 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface $installPath = $this->getInstallPath($package); - if (is_readable($installPath)) { + if (Filesystem::isReadable($installPath)) { return true; } @@ -128,7 +128,7 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface $downloadPath = $this->getInstallPath($package); // remove the binaries if it appears the package files are missing - if (!is_readable($downloadPath) && $repo->hasPackage($package)) { + if (!Filesystem::isReadable($downloadPath) && $repo->hasPackage($package)) { $this->binaryInstaller->removeBinaries($package); } diff --git a/src/Composer/Util/Filesystem.php b/src/Composer/Util/Filesystem.php index c1a122b11..8335052c7 100644 --- a/src/Composer/Util/Filesystem.php +++ b/src/Composer/Util/Filesystem.php @@ -600,6 +600,33 @@ class Filesystem return preg_replace('{^file://}i', '', $path); } + /** + * Cross-platform safe version of is_readable() + * + * This will also check for readability by reading the file as is_readable can not be trusted on network-mounts + * and \\wsl$ paths. See https://github.com/composer/composer/issues/8231 and https://bugs.php.net/bug.php?id=68926 + * + * @param string $path + * @return bool + */ + public static function isReadable($path) + { + if (is_readable($path)) { + return true; + } + + if (is_file($path)) { + return false !== Silencer::call('file_get_contents', $path, false, null, 0, 1); + } + + if (is_dir($path)) { + return false !== Silencer::call('opendir', $path); + } + + // assume false otherwise + return false; + } + protected function directorySize($directory) { $it = new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS); diff --git a/src/Composer/Util/StreamContextFactory.php b/src/Composer/Util/StreamContextFactory.php index e189c20ca..3545db105 100644 --- a/src/Composer/Util/StreamContextFactory.php +++ b/src/Composer/Util/StreamContextFactory.php @@ -211,11 +211,11 @@ final class StreamContextFactory } } - if (isset($defaults['ssl']['cafile']) && (!is_readable($defaults['ssl']['cafile']) || !CaBundle::validateCaFile($defaults['ssl']['cafile'], $logger))) { + if (isset($defaults['ssl']['cafile']) && (!Filesystem::isReadable($defaults['ssl']['cafile']) || !CaBundle::validateCaFile($defaults['ssl']['cafile'], $logger))) { throw new TransportException('The configured cafile was not valid or could not be read.'); } - if (isset($defaults['ssl']['capath']) && (!is_dir($defaults['ssl']['capath']) || !is_readable($defaults['ssl']['capath']))) { + if (isset($defaults['ssl']['capath']) && (!is_dir($defaults['ssl']['capath']) || !Filesystem::isReadable($defaults['ssl']['capath']))) { throw new TransportException('The configured capath was not valid or could not be read.'); }