From 90332f1dbd2b0d22f6e0272cfe4bc0d759e5dc84 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 25 Aug 2020 13:55:32 +0200 Subject: [PATCH] Add a readonly mode to the cache, fixes #9150 --- doc/06-config.md | 10 +++++--- res/composer-schema.json | 4 +++ src/Composer/Cache.php | 25 ++++++++++++++++--- src/Composer/Command/ClearCacheCommand.php | 1 + src/Composer/Config.php | 2 ++ src/Composer/Downloader/FileDownloader.php | 2 +- src/Composer/Factory.php | 1 + .../Repository/ComposerRepository.php | 11 +++++--- .../Repository/Vcs/BitbucketDriver.php | 1 + src/Composer/Repository/Vcs/GitDriver.php | 1 + src/Composer/Repository/Vcs/GitHubDriver.php | 1 + src/Composer/Repository/Vcs/GitLabDriver.php | 1 + src/Composer/Repository/Vcs/SvnDriver.php | 1 + 13 files changed, 51 insertions(+), 10 deletions(-) diff --git a/doc/06-config.md b/doc/06-config.md index b7e5e8e45..ccbdb3b07 100644 --- a/doc/06-config.md +++ b/doc/06-config.md @@ -85,11 +85,11 @@ gitlab.com the domain names must be also specified with the ## gitlab-token -A list of domain names and private tokens. Private token can be either simple -string, or array with username and token. For example using `{"gitlab.com": +A list of domain names and private tokens. Private token can be either simple +string, or array with username and token. For example using `{"gitlab.com": "privatetoken"}` as the value of this option will use `privatetoken` to access private repositories on gitlab. Using `{"gitlab.com": {"username": "gitlabuser", - "token": "privatetoken"}}` will use both username and token for gitlab deploy + "token": "privatetoken"}}` will use both username and token for gitlab deploy token functionality (https://docs.gitlab.com/ee/user/project/deploy_tokens/) Please note: If the package is not hosted at gitlab.com the domain names must be also specified with the @@ -204,6 +204,10 @@ downloads. When the garbage collection is periodically ran, this is the maximum size the cache will be able to use. Older (less used) files will be removed first until the cache fits. +## cache-read-only + +Defaults to `false`. Whether to use the Composer cache in read-only mode. + ## bin-compat Defaults to `auto`. Determines the compatibility of the binaries to be installed. diff --git a/res/composer-schema.json b/res/composer-schema.json index 04db6d3a3..8aa401580 100644 --- a/res/composer-schema.json +++ b/res/composer-schema.json @@ -237,6 +237,10 @@ "type": ["string", "integer"], "description": "The cache max size for the files cache, defaults to \"300MiB\"." }, + "cache-read-only": { + "type": ["boolean"], + "description": "Whether to use the Composer cache in read-only mode." + }, "bin-compat": { "enum": ["auto", "full"], "description": "The compatibility of the binaries, defaults to \"auto\" (automatically guessed) and can be \"full\" (compatible with both Windows and Unix-based systems)." diff --git a/src/Composer/Cache.php b/src/Composer/Cache.php index 2a4e7756f..97be1e1a2 100644 --- a/src/Composer/Cache.php +++ b/src/Composer/Cache.php @@ -30,19 +30,22 @@ class Cache private $enabled = true; private $allowlist; private $filesystem; + private $readOnly; /** * @param IOInterface $io * @param string $cacheDir location of the cache * @param string $allowlist List of characters that are allowed in path names (used in a regex character class) * @param Filesystem $filesystem optional filesystem instance + * @param bool $readOnly whether the cache is in readOnly mode */ - public function __construct(IOInterface $io, $cacheDir, $allowlist = 'a-z0-9.', Filesystem $filesystem = null) + public function __construct(IOInterface $io, $cacheDir, $allowlist = 'a-z0-9.', Filesystem $filesystem = null, $readOnly = false) { $this->io = $io; $this->root = rtrim($cacheDir, '/\\') . '/'; $this->allowlist = $allowlist; $this->filesystem = $filesystem ?: new Filesystem(); + $this->readOnly = (bool) $readOnly; if (!self::isUsable($cacheDir)) { $this->enabled = false; @@ -59,6 +62,22 @@ class Cache } } + /** + * @param bool $readOnly + */ + public function setReadOnly($readOnly) + { + $this->readOnly = (bool) $readOnly; + } + + /** + * @return bool + */ + public function isReadOnly() + { + return $this->readOnly; + } + public static function isUsable($path) { return !preg_match('{(^|[\\\\/])(\$null|nul|NUL|/dev/null)([\\\\/]|$)}', $path); @@ -90,7 +109,7 @@ class Cache public function write($file, $contents) { - if ($this->enabled) { + if ($this->enabled && !$this->readOnly) { $file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file); $this->io->writeError('Writing '.$this->root . $file.' into cache', true, IOInterface::DEBUG); @@ -128,7 +147,7 @@ class Cache */ public function copyFrom($file, $source) { - if ($this->enabled) { + if ($this->enabled && !$this->readOnly) { $file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file); $this->filesystem->ensureDirectoryExists(dirname($this->root . $file)); diff --git a/src/Composer/Command/ClearCacheCommand.php b/src/Composer/Command/ClearCacheCommand.php index 2f511641e..bdbdd80cf 100644 --- a/src/Composer/Command/ClearCacheCommand.php +++ b/src/Composer/Command/ClearCacheCommand.php @@ -59,6 +59,7 @@ EOT continue; } $cache = new Cache($io, $cachePath); + $cache->setReadOnly($config->get('cache-read-only')); if (!$cache->isEnabled()) { $io->writeError("Cache is not enabled ($key): $cachePath"); diff --git a/src/Composer/Config.php b/src/Composer/Config.php index 4d03a4971..54d2e360c 100644 --- a/src/Composer/Config.php +++ b/src/Composer/Config.php @@ -41,6 +41,7 @@ class Config 'cache-ttl' => 15552000, // 6 months 'cache-files-ttl' => null, // fallback to cache-ttl 'cache-files-maxsize' => '300MiB', + 'cache-read-only' => false, 'bin-compat' => 'auto', 'discard-changes' => false, 'autoloader-suffix' => null, @@ -236,6 +237,7 @@ class Config return (($flags & self::RELATIVE_PATHS) == self::RELATIVE_PATHS) ? $val : $this->realpath($val); // booleans with env var support + case 'cache-read-only': case 'htaccess-protect': // convert foo-bar to COMPOSER_FOO_BAR and check if it exists since it overrides the local config $env = 'COMPOSER_' . strtoupper(strtr($key, '-', '_')); diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index 8fbb48e9c..9fa498af8 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -187,7 +187,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface $url = reset($urls); $cacheKey = $url['cacheKey']; - if ($cache) { + if ($cache && !$cache->isReadOnly()) { $self->lastCacheWrites[$package->getName()] = $cacheKey; $cache->copyFrom($cacheKey, $fileName); } diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index 7fd9af344..218a53457 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -477,6 +477,7 @@ class Factory $cache = null; if ($config->get('cache-files-ttl') > 0) { $cache = new Cache($io, $config->get('cache-files-dir'), 'a-z0-9_./'); + $cache->setReadOnly($config->get('cache-read-only')); } $fs = new Filesystem($process); diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index d3db3069c..e5706e04d 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -130,6 +130,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $this->baseUrl = rtrim(preg_replace('{(?:/[^/\\\\]+\.json)?(?:[?#].*)?$}', '', $this->url), '/'); $this->io = $io; $this->cache = new Cache($io, $config->get('cache-repo-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $this->url), 'a-z0-9.$~'); + $this->cache->setReadOnly($config->get('cache-read-only')); $this->versionParser = new VersionParser(); $this->loader = new ArrayLoader($this->versionParser); $this->httpDownloader = $httpDownloader; @@ -1071,7 +1072,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $data = $response->decodeJson(); HttpDownloader::outputWarnings($this->io, $this->url, $data); - if ($cacheKey) { + if ($cacheKey && !$this->cache->isReadOnly()) { if ($storeLastModifiedTime) { $lastModifiedDate = $response->getHeader('last-modified'); if ($lastModifiedDate) { @@ -1155,7 +1156,9 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $data['last-modified'] = $lastModifiedDate; $json = json_encode($data); } - $this->cache->write($cacheKey, $json); + if (!$this->cache->isReadOnly()) { + $this->cache->write($cacheKey, $json); + } return $data; } catch (\Exception $e) { @@ -1238,7 +1241,9 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $data['last-modified'] = $lastModifiedDate; $json = JsonFile::encode($data, JsonFile::JSON_UNESCAPED_SLASHES | JsonFile::JSON_UNESCAPED_UNICODE); } - $cache->write($cacheKey, $json); + if (!$cache->isReadOnly()) { + $cache->write($cacheKey, $json); + } $repo->freshMetadataUrls[$filename] = true; return $data; diff --git a/src/Composer/Repository/Vcs/BitbucketDriver.php b/src/Composer/Repository/Vcs/BitbucketDriver.php index d23b32dd2..c1781342f 100644 --- a/src/Composer/Repository/Vcs/BitbucketDriver.php +++ b/src/Composer/Repository/Vcs/BitbucketDriver.php @@ -60,6 +60,7 @@ abstract class BitbucketDriver extends VcsDriver $this->repository, )) ); + $this->cache->setReadOnly($this->config->get('cache-read-only')); } /** diff --git a/src/Composer/Repository/Vcs/GitDriver.php b/src/Composer/Repository/Vcs/GitDriver.php index c7672c619..499a5a9df 100644 --- a/src/Composer/Repository/Vcs/GitDriver.php +++ b/src/Composer/Repository/Vcs/GitDriver.php @@ -75,6 +75,7 @@ class GitDriver extends VcsDriver $this->getBranches(); $this->cache = new Cache($this->io, $this->config->get('cache-repo-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $cacheUrl)); + $this->cache->setReadOnly($this->config->get('cache-read-only')); } /** diff --git a/src/Composer/Repository/Vcs/GitHubDriver.php b/src/Composer/Repository/Vcs/GitHubDriver.php index 83e353321..bbbbd6ecb 100644 --- a/src/Composer/Repository/Vcs/GitHubDriver.php +++ b/src/Composer/Repository/Vcs/GitHubDriver.php @@ -58,6 +58,7 @@ class GitHubDriver extends VcsDriver $this->originUrl = 'github.com'; } $this->cache = new Cache($this->io, $this->config->get('cache-repo-dir').'/'.$this->originUrl.'/'.$this->owner.'/'.$this->repository); + $this->cache->setReadOnly($this->config->get('cache-read-only')); if ( $this->config->get('use-github-api') === false || (isset($this->repoConfig['no-api']) && $this->repoConfig['no-api'] ) ){ $this->setupGitDriver($this->url); diff --git a/src/Composer/Repository/Vcs/GitLabDriver.php b/src/Composer/Repository/Vcs/GitLabDriver.php index 22f884d7f..2987b3b94 100644 --- a/src/Composer/Repository/Vcs/GitLabDriver.php +++ b/src/Composer/Repository/Vcs/GitLabDriver.php @@ -105,6 +105,7 @@ class GitLabDriver extends VcsDriver $this->repository = preg_replace('#(\.git)$#', '', $match['repo']); $this->cache = new Cache($this->io, $this->config->get('cache-repo-dir').'/'.$this->originUrl.'/'.$this->namespace.'/'.$this->repository); + $this->cache->setReadOnly($this->config->get('cache-read-only')); $this->fetchProject(); } diff --git a/src/Composer/Repository/Vcs/SvnDriver.php b/src/Composer/Repository/Vcs/SvnDriver.php index 097103487..87c9781b6 100644 --- a/src/Composer/Repository/Vcs/SvnDriver.php +++ b/src/Composer/Repository/Vcs/SvnDriver.php @@ -78,6 +78,7 @@ class SvnDriver extends VcsDriver } $this->cache = new Cache($this->io, $this->config->get('cache-repo-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $this->baseUrl)); + $this->cache->setReadOnly($this->config->get('cache-read-only')); $this->getBranches(); $this->getTags();