Add allow-plugins config value (#10314)
Fixes #5659 - Automatically switch off plugins by default in July 2022 - reword hash into object in schema Co-authored-by: Nils Adermann <naderman@naderman.de>pull/10347/head
parent
04dbed27a9
commit
a3e91b5be6
|
@ -24,6 +24,37 @@ helper is available:
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## allow-plugins
|
||||||
|
|
||||||
|
Defaults to `null` (allow all plugins implicitly) for backwards compatibility until July 2022.
|
||||||
|
At that point the default will become `{}` and plugins will not load anymore unless allowed.
|
||||||
|
|
||||||
|
As of Composer 2.2.0, the `allow-plugins` option adds a layer of security
|
||||||
|
allowing you to restrict which Composer plugins are able to execute code during
|
||||||
|
a Composer run.
|
||||||
|
|
||||||
|
When a new plugin is first activated, which is not yet listed in the config option,
|
||||||
|
Composer will print a warning. If you run Composer interactively it will
|
||||||
|
prompt you to decide if you want to execute the plugin or not.
|
||||||
|
|
||||||
|
Use this setting to allow only packages you trust to execute code. Set it to
|
||||||
|
an object with package name patterns as keys. The values are **true** to allow
|
||||||
|
and **false** to disallow while suppressing further warnings and prompts.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"allow-plugins": {
|
||||||
|
"third-party/required-plugin": true,
|
||||||
|
"my-organization/*": true,
|
||||||
|
"unnecessary/plugin": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also set the config option itself to `false` to disallow all plugins, or `true` to allow all plugins to run (NOT recommended).
|
||||||
|
|
||||||
## use-include-path
|
## use-include-path
|
||||||
|
|
||||||
Defaults to `false`. If `true`, the Composer autoloader will also look for classes
|
Defaults to `false`. If `true`, the Composer autoloader will also look for classes
|
||||||
|
@ -33,7 +64,7 @@ in the PHP include path.
|
||||||
|
|
||||||
Defaults to `dist` and can be any of `source`, `dist` or `auto`. This option
|
Defaults to `dist` and can be any of `source`, `dist` or `auto`. This option
|
||||||
allows you to set the install method Composer will prefer to use. Can
|
allows you to set the install method Composer will prefer to use. Can
|
||||||
optionally be a hash of patterns for more granular install preferences.
|
optionally be an object with package name patterns for keys for more granular install preferences.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
|
|
@ -138,42 +138,42 @@
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "This is a hash of package name (keys) and version constraints (values) that are required to run this package.",
|
"description": "This is an object of package name (keys) and version constraints (values) that are required to run this package.",
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "This is a hash of package name (keys) and version constraints (values) that this package requires for developing it (testing tools and such).",
|
"description": "This is an object of package name (keys) and version constraints (values) that this package requires for developing it (testing tools and such).",
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"replace": {
|
"replace": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "This is a hash of package name (keys) and version constraints (values) that can be replaced by this package.",
|
"description": "This is an object of package name (keys) and version constraints (values) that can be replaced by this package.",
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"conflict": {
|
"conflict": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "This is a hash of package name (keys) and version constraints (values) that conflict with this package.",
|
"description": "This is an object of package name (keys) and version constraints (values) that conflict with this package.",
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"provide": {
|
"provide": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "This is a hash of package name (keys) and version constraints (values) that this package provides in addition to this package's name.",
|
"description": "This is an object of package name (keys) and version constraints (values) that this package provides in addition to this package's name.",
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"suggest": {
|
"suggest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "This is a hash of package name (keys) and descriptions (values) that this package suggests work well with it (this will be suggested to the user during installation).",
|
"description": "This is an object of package name (keys) and descriptions (values) that this package suggests work well with it (this will be suggested to the user during installation).",
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
@ -217,7 +217,7 @@
|
||||||
"properties": {
|
"properties": {
|
||||||
"psr-0": {
|
"psr-0": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "This is a hash of namespaces (keys) and the directories they can be found into (values, can be arrays of paths) by the autoloader.",
|
"description": "This is an object of namespaces (keys) and the directories they can be found into (values, can be arrays of paths) by the autoloader.",
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"type": ["string", "array"],
|
"type": ["string", "array"],
|
||||||
"items": {
|
"items": {
|
||||||
|
@ -227,7 +227,7 @@
|
||||||
},
|
},
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "This is a hash of namespaces (keys) and the PSR-4 directories they can map to (values, can be arrays of paths) by the autoloader.",
|
"description": "This is an object of namespaces (keys) and the PSR-4 directories they can map to (values, can be arrays of paths) by the autoloader.",
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"type": ["string", "array"],
|
"type": ["string", "array"],
|
||||||
"items": {
|
"items": {
|
||||||
|
@ -283,11 +283,18 @@
|
||||||
"properties": {
|
"properties": {
|
||||||
"platform": {
|
"platform": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "This is a hash of package name (keys) and version (values) that will be used to mock the platform packages on this machine.",
|
"description": "This is an object of package name (keys) and version (values) that will be used to mock the platform packages on this machine.",
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"type": ["string", "boolean"]
|
"type": ["string", "boolean"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"allow-plugins": {
|
||||||
|
"type": ["object", "boolean"],
|
||||||
|
"description": "This is an object of {\"pattern\": true|false} with packages which are allowed to be loaded as plugins, or true to allow all, false to allow none. Defaults to {} which prompts when an unknown plugin is added.",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": ["boolean"]
|
||||||
|
}
|
||||||
|
},
|
||||||
"process-timeout": {
|
"process-timeout": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "The timeout in seconds for process executions, defaults to 300 (5mins)."
|
"description": "The timeout in seconds for process executions, defaults to 300 (5mins)."
|
||||||
|
@ -302,7 +309,10 @@
|
||||||
},
|
},
|
||||||
"preferred-install": {
|
"preferred-install": {
|
||||||
"type": ["string", "object"],
|
"type": ["string", "object"],
|
||||||
"description": "The install method Composer will prefer to use, defaults to auto and can be any of source, dist, auto, or a hash of {\"pattern\": \"preference\"}."
|
"description": "The install method Composer will prefer to use, defaults to auto and can be any of source, dist, auto, or an object of {\"pattern\": \"preference\"}.",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": ["string"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"notify-on-install": {
|
"notify-on-install": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
|
@ -317,21 +327,21 @@
|
||||||
},
|
},
|
||||||
"github-oauth": {
|
"github-oauth": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "A hash of domain name => github API oauth tokens, typically {\"github.com\":\"<token>\"}.",
|
"description": "An object of domain name => github API oauth tokens, typically {\"github.com\":\"<token>\"}.",
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"gitlab-oauth": {
|
"gitlab-oauth": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "A hash of domain name => gitlab API oauth tokens, typically {\"gitlab.com\":\"<token>\"}.",
|
"description": "An object of domain name => gitlab API oauth tokens, typically {\"gitlab.com\":\"<token>\"}.",
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"gitlab-token": {
|
"gitlab-token": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "A hash of domain name => gitlab private tokens, typically {\"gitlab.com\":\"<token>\"}.",
|
"description": "An object of domain name => gitlab private tokens, typically {\"gitlab.com\":\"<token>\"}.",
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
@ -342,7 +352,7 @@
|
||||||
},
|
},
|
||||||
"bearer": {
|
"bearer": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "A hash of domain name => bearer authentication token, for example {\"example.com\":\"<token>\"}.",
|
"description": "An object of domain name => bearer authentication token, for example {\"example.com\":\"<token>\"}.",
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
@ -372,7 +382,7 @@
|
||||||
},
|
},
|
||||||
"http-basic": {
|
"http-basic": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "A hash of domain name => {\"username\": \"...\", \"password\": \"...\"}.",
|
"description": "An object of domain name => {\"username\": \"...\", \"password\": \"...\"}.",
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["username", "password"],
|
"required": ["username", "password"],
|
||||||
|
@ -631,7 +641,7 @@
|
||||||
"properties": {
|
"properties": {
|
||||||
"psr-0": {
|
"psr-0": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "This is a hash of namespaces (keys) and the directories they can be found in (values, can be arrays of paths) by the autoloader.",
|
"description": "This is an object of namespaces (keys) and the directories they can be found in (values, can be arrays of paths) by the autoloader.",
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"type": ["string", "array"],
|
"type": ["string", "array"],
|
||||||
"items": {
|
"items": {
|
||||||
|
@ -641,7 +651,7 @@
|
||||||
},
|
},
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "This is a hash of namespaces (keys) and the PSR-4 directories they can map to (values, can be arrays of paths) by the autoloader.",
|
"description": "This is an object of namespaces (keys) and the PSR-4 directories they can map to (values, can be arrays of paths) by the autoloader.",
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"type": ["string", "array"],
|
"type": ["string", "array"],
|
||||||
"items": {
|
"items": {
|
||||||
|
|
|
@ -214,7 +214,7 @@ EOT
|
||||||
protected function execute(InputInterface $input, OutputInterface $output)
|
protected function execute(InputInterface $input, OutputInterface $output)
|
||||||
{
|
{
|
||||||
// Open file in editor
|
// Open file in editor
|
||||||
if ($input->getOption('editor')) {
|
if (true === $input->getOption('editor')) {
|
||||||
$editor = escapeshellcmd(Platform::getEnv('EDITOR'));
|
$editor = escapeshellcmd(Platform::getEnv('EDITOR'));
|
||||||
if (!$editor) {
|
if (!$editor) {
|
||||||
if (Platform::isWindows()) {
|
if (Platform::isWindows()) {
|
||||||
|
@ -235,20 +235,20 @@ EOT
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$input->getOption('global')) {
|
if (false === $input->getOption('global')) {
|
||||||
$this->config->merge($this->configFile->read(), $this->configFile->getPath());
|
$this->config->merge($this->configFile->read(), $this->configFile->getPath());
|
||||||
$this->config->merge(array('config' => $this->authConfigFile->exists() ? $this->authConfigFile->read() : array()), $this->authConfigFile->getPath());
|
$this->config->merge(array('config' => $this->authConfigFile->exists() ? $this->authConfigFile->read() : array()), $this->authConfigFile->getPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
// List the configuration of the file settings
|
// List the configuration of the file settings
|
||||||
if ($input->getOption('list')) {
|
if (true === $input->getOption('list')) {
|
||||||
$this->listConfiguration($this->config->all(), $this->config->raw(), $output, null, (bool) $input->getOption('source'));
|
$this->listConfiguration($this->config->all(), $this->config->raw(), $output, null, (bool) $input->getOption('source'));
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
$settingKey = $input->getArgument('setting-key');
|
$settingKey = $input->getArgument('setting-key');
|
||||||
if (!$settingKey || !is_string($settingKey)) {
|
if (!is_string($settingKey)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -446,6 +446,7 @@ EOT
|
||||||
'github-expose-hostname' => array($booleanValidator, $booleanNormalizer),
|
'github-expose-hostname' => array($booleanValidator, $booleanNormalizer),
|
||||||
'htaccess-protect' => array($booleanValidator, $booleanNormalizer),
|
'htaccess-protect' => array($booleanValidator, $booleanNormalizer),
|
||||||
'lock' => array($booleanValidator, $booleanNormalizer),
|
'lock' => array($booleanValidator, $booleanNormalizer),
|
||||||
|
'allow-plugins' => array($booleanValidator, $booleanNormalizer),
|
||||||
'platform-check' => array(
|
'platform-check' => array(
|
||||||
function ($val) {
|
function ($val) {
|
||||||
return in_array($val, array('php-only', 'true', 'false', '1', '0'), true);
|
return in_array($val, array('php-only', 'true', 'false', '1', '0'), true);
|
||||||
|
@ -553,6 +554,28 @@ EOT
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handle allow-plugins config setting elements true or false to add/remove
|
||||||
|
if (Preg::isMatch('{^allow-plugins\.([a-zA-Z0-9/*-]+)}', $settingKey, $matches)) {
|
||||||
|
if ($input->getOption('unset')) {
|
||||||
|
$this->configSource->removeConfigSetting($settingKey);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (true !== $booleanValidator($values[0])) {
|
||||||
|
throw new \RuntimeException(sprintf(
|
||||||
|
'"%s" is an invalid value',
|
||||||
|
$values[0]
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedValue = $booleanNormalizer($values[0]);
|
||||||
|
|
||||||
|
$this->configSource->addConfigSetting($settingKey, $normalizedValue);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
// handle properties
|
// handle properties
|
||||||
$uniqueProps = array(
|
$uniqueProps = array(
|
||||||
'name' => array('is_string', function ($val) {
|
'name' => array('is_string', function ($val) {
|
||||||
|
|
|
@ -34,6 +34,7 @@ class Config
|
||||||
public static $defaultConfig = array(
|
public static $defaultConfig = array(
|
||||||
'process-timeout' => 300,
|
'process-timeout' => 300,
|
||||||
'use-include-path' => false,
|
'use-include-path' => false,
|
||||||
|
'allow-plugins' => null, // null for BC for now, will become array() after July 2022
|
||||||
'use-parent-dir' => 'prompt',
|
'use-parent-dir' => 'prompt',
|
||||||
'preferred-install' => 'dist',
|
'preferred-install' => 'dist',
|
||||||
'notify-on-install' => true,
|
'notify-on-install' => true,
|
||||||
|
@ -117,6 +118,12 @@ class Config
|
||||||
{
|
{
|
||||||
// load defaults
|
// load defaults
|
||||||
$this->config = static::$defaultConfig;
|
$this->config = static::$defaultConfig;
|
||||||
|
|
||||||
|
// TODO after July 2022 remove this and update the default value above in self::$defaultConfig + remove note from 06-config.md
|
||||||
|
if (strtotime('2022-07-01') < time()) {
|
||||||
|
$this->config['allow-plugins'] = array();
|
||||||
|
}
|
||||||
|
|
||||||
$this->repositories = static::$defaultRepositories;
|
$this->repositories = static::$defaultRepositories;
|
||||||
$this->useEnvironment = (bool) $useEnvironment;
|
$this->useEnvironment = (bool) $useEnvironment;
|
||||||
$this->baseDir = $baseDir;
|
$this->baseDir = $baseDir;
|
||||||
|
@ -165,7 +172,7 @@ class Config
|
||||||
/**
|
/**
|
||||||
* Merges new config values with the existing ones (overriding)
|
* Merges new config values with the existing ones (overriding)
|
||||||
*
|
*
|
||||||
* @param array<string, mixed> $config
|
* @param array{config?: array<string, mixed>, repositories?: array<mixed>} $config
|
||||||
* @param string $source
|
* @param string $source
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
|
@ -175,10 +182,15 @@ class Config
|
||||||
// override defaults with given config
|
// override defaults with given config
|
||||||
if (!empty($config['config']) && is_array($config['config'])) {
|
if (!empty($config['config']) && is_array($config['config'])) {
|
||||||
foreach ($config['config'] as $key => $val) {
|
foreach ($config['config'] as $key => $val) {
|
||||||
if (in_array($key, array('bitbucket-oauth', 'github-oauth', 'gitlab-oauth', 'gitlab-token', 'http-basic', 'bearer')) && isset($this->config[$key])) {
|
if (in_array($key, array('bitbucket-oauth', 'github-oauth', 'gitlab-oauth', 'gitlab-token', 'http-basic', 'bearer'), true) && isset($this->config[$key])) {
|
||||||
$this->config[$key] = array_merge($this->config[$key], $val);
|
$this->config[$key] = array_merge($this->config[$key], $val);
|
||||||
$this->setSourceOfConfigValue($val, $key, $source);
|
$this->setSourceOfConfigValue($val, $key, $source);
|
||||||
} elseif (in_array($key, array('gitlab-domains', 'github-domains')) && isset($this->config[$key])) {
|
} elseif (in_array($key, array('allow-plugins'), true) && isset($this->config[$key]) && is_array($this->config[$key])) {
|
||||||
|
// merging $val first to get the local config on top of the global one, then appending the global config,
|
||||||
|
// then merging local one again to make sure the values from local win over global ones for keys present in both
|
||||||
|
$this->config[$key] = array_merge($val, $this->config[$key], $val);
|
||||||
|
$this->setSourceOfConfigValue($val, $key, $source);
|
||||||
|
} elseif (in_array($key, array('gitlab-domains', 'github-domains'), true) && isset($this->config[$key])) {
|
||||||
$this->config[$key] = array_unique(array_merge($this->config[$key], $val));
|
$this->config[$key] = array_unique(array_merge($this->config[$key], $val));
|
||||||
$this->setSourceOfConfigValue($val, $key, $source);
|
$this->setSourceOfConfigValue($val, $key, $source);
|
||||||
} elseif ('preferred-install' === $key && isset($this->config[$key])) {
|
} elseif ('preferred-install' === $key && isset($this->config[$key])) {
|
||||||
|
|
|
@ -287,7 +287,7 @@ class JsonConfigSource implements ConfigSourceInterface
|
||||||
} catch (JsonValidationException $e) {
|
} catch (JsonValidationException $e) {
|
||||||
// restore contents to the original state
|
// restore contents to the original state
|
||||||
file_put_contents($this->file->getPath(), $contents);
|
file_put_contents($this->file->getPath(), $contents);
|
||||||
throw new \RuntimeException('Failed to update composer.json with a valid format, reverting to the original content. Please report an issue to us with details (command you run and a copy of your composer.json).', 0, $e);
|
throw new \RuntimeException('Failed to update composer.json with a valid format, reverting to the original content. Please report an issue to us with details (command you run and a copy of your composer.json). '.PHP_EOL.implode(PHP_EOL, $e->getErrors()), 0, $e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($newFile) {
|
if ($newFile) {
|
||||||
|
|
|
@ -15,6 +15,7 @@ namespace Composer\Plugin;
|
||||||
use Composer\Composer;
|
use Composer\Composer;
|
||||||
use Composer\EventDispatcher\EventSubscriberInterface;
|
use Composer\EventDispatcher\EventSubscriberInterface;
|
||||||
use Composer\IO\IOInterface;
|
use Composer\IO\IOInterface;
|
||||||
|
use Composer\Package\BasePackage;
|
||||||
use Composer\Package\CompletePackage;
|
use Composer\Package\CompletePackage;
|
||||||
use Composer\Package\Package;
|
use Composer\Package\Package;
|
||||||
use Composer\Package\Version\VersionParser;
|
use Composer\Package\Version\VersionParser;
|
||||||
|
@ -52,6 +53,16 @@ class PluginManager
|
||||||
/** @var array<string, PluginInterface> */
|
/** @var array<string, PluginInterface> */
|
||||||
protected $registeredPlugins = array();
|
protected $registeredPlugins = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, bool>|null
|
||||||
|
*/
|
||||||
|
private $allowPluginRules;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, bool>|null
|
||||||
|
*/
|
||||||
|
private $allowGlobalPluginRules;
|
||||||
|
|
||||||
/** @var int */
|
/** @var int */
|
||||||
private static $classCounter = 0;
|
private static $classCounter = 0;
|
||||||
|
|
||||||
|
@ -70,6 +81,9 @@ class PluginManager
|
||||||
$this->globalComposer = $globalComposer;
|
$this->globalComposer = $globalComposer;
|
||||||
$this->versionParser = new VersionParser();
|
$this->versionParser = new VersionParser();
|
||||||
$this->disablePlugins = $disablePlugins;
|
$this->disablePlugins = $disablePlugins;
|
||||||
|
|
||||||
|
$this->allowPluginRules = $this->parseAllowedPlugins($composer->getConfig()->get('allow-plugins'));
|
||||||
|
$this->allowGlobalPluginRules = $this->parseAllowedPlugins($globalComposer !== null ? $globalComposer->getConfig()->get('allow-plugins') : false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -84,7 +98,7 @@ class PluginManager
|
||||||
}
|
}
|
||||||
|
|
||||||
$repo = $this->composer->getRepositoryManager()->getLocalRepository();
|
$repo = $this->composer->getRepositoryManager()->getLocalRepository();
|
||||||
$globalRepo = $this->globalComposer ? $this->globalComposer->getRepositoryManager()->getLocalRepository() : null;
|
$globalRepo = $this->globalComposer !== null ? $this->globalComposer->getRepositoryManager()->getLocalRepository() : null;
|
||||||
$this->loadRepository($repo, false);
|
$this->loadRepository($repo, false);
|
||||||
if ($globalRepo) {
|
if ($globalRepo) {
|
||||||
$this->loadRepository($globalRepo, true);
|
$this->loadRepository($globalRepo, true);
|
||||||
|
@ -150,6 +164,11 @@ class PluginManager
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!$this->isPluginAllowed($package->getName(), $isGlobalPlugin)) {
|
||||||
|
$this->io->writeError('Skipped loading "'.$package->getName() . '" '.($isGlobalPlugin ? '(installed globally) ' : '').'as it is not in config.allow-plugins', true, IOInterface::DEBUG);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if ($package->getType() === 'composer-plugin') {
|
if ($package->getType() === 'composer-plugin') {
|
||||||
$requiresComposer = null;
|
$requiresComposer = null;
|
||||||
foreach ($package->getRequires() as $link) { /** @var Link $link */
|
foreach ($package->getRequires() as $link) { /** @var Link $link */
|
||||||
|
@ -194,7 +213,7 @@ class PluginManager
|
||||||
$classes = is_array($extra['class']) ? $extra['class'] : array($extra['class']);
|
$classes = is_array($extra['class']) ? $extra['class'] : array($extra['class']);
|
||||||
|
|
||||||
$localRepo = $this->composer->getRepositoryManager()->getLocalRepository();
|
$localRepo = $this->composer->getRepositoryManager()->getLocalRepository();
|
||||||
$globalRepo = $this->globalComposer ? $this->globalComposer->getRepositoryManager()->getLocalRepository() : null;
|
$globalRepo = $this->globalComposer !== null ? $this->globalComposer->getRepositoryManager()->getLocalRepository() : null;
|
||||||
|
|
||||||
$rootPackage = clone $this->composer->getPackage();
|
$rootPackage = clone $this->composer->getPackage();
|
||||||
$rootPackageRepo = new RootPackageRepository($rootPackage);
|
$rootPackageRepo = new RootPackageRepository($rootPackage);
|
||||||
|
@ -357,6 +376,13 @@ class PluginManager
|
||||||
*/
|
*/
|
||||||
public function addPlugin(PluginInterface $plugin, $isGlobalPlugin = false, PackageInterface $sourcePackage = null)
|
public function addPlugin(PluginInterface $plugin, $isGlobalPlugin = false, PackageInterface $sourcePackage = null)
|
||||||
{
|
{
|
||||||
|
if ($sourcePackage === null) {
|
||||||
|
trigger_error('Calling PluginManager::addPlugin without $sourcePackage is deprecated, if you are using this please get in touch with us to explain the use case', E_USER_DEPRECATED);
|
||||||
|
} elseif (!$this->isPluginAllowed($sourcePackage->getName(), $isGlobalPlugin)) {
|
||||||
|
$this->io->writeError('Skipped loading "'.get_class($plugin).' from '.$sourcePackage->getName() . '" '.($isGlobalPlugin ? '(installed globally) ' : '').' as it is not in config.allow-plugins', true, IOInterface::DEBUG);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$details = array();
|
$details = array();
|
||||||
if ($sourcePackage) {
|
if ($sourcePackage) {
|
||||||
$details[] = 'from '.$sourcePackage->getName();
|
$details[] = 'from '.$sourcePackage->getName();
|
||||||
|
@ -597,4 +623,109 @@ class PluginManager
|
||||||
|
|
||||||
return $capabilities;
|
return $capabilities;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, bool>|bool|null $allowPluginsConfig
|
||||||
|
* @return array<string, bool>|null
|
||||||
|
*/
|
||||||
|
private function parseAllowedPlugins($allowPluginsConfig)
|
||||||
|
{
|
||||||
|
if (null === $allowPluginsConfig) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (true === $allowPluginsConfig) {
|
||||||
|
return array('{}' => true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (false === $allowPluginsConfig) {
|
||||||
|
return array('{^$}D' => false);
|
||||||
|
}
|
||||||
|
|
||||||
|
$rules = array();
|
||||||
|
foreach ($allowPluginsConfig as $pattern => $allow) {
|
||||||
|
$rules[BasePackage::packageNameToRegexp($pattern)] = $allow;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rules;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $package
|
||||||
|
* @param bool $isGlobalPlugin
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function isPluginAllowed($package, $isGlobalPlugin)
|
||||||
|
{
|
||||||
|
static $warned = array();
|
||||||
|
$rules = $isGlobalPlugin ? $this->allowGlobalPluginRules : $this->allowPluginRules;
|
||||||
|
|
||||||
|
if ($rules === null) {
|
||||||
|
if (!$this->io->isInteractive()) {
|
||||||
|
if (!isset($warned['all'])) {
|
||||||
|
$this->io->writeError('<warning>For additional security you should declare the allow-plugins config with a list of packages names that are allowed to run code. See https://getcomposer.org/allow-plugins</warning>');
|
||||||
|
$this->io->writeError('<warning>You have until July 2022 to add the setting. Composer will then switch the default behavior to disallow all plugins.</warning>');
|
||||||
|
$warned['all'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if no config is defined we allow all plugins for BC
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep going and prompt the user
|
||||||
|
$rules = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($rules as $pattern => $allow) {
|
||||||
|
if (Preg::isMatch($pattern, $package)) {
|
||||||
|
return $allow === true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($warned[$package])) {
|
||||||
|
if ($this->io->isInteractive()) {
|
||||||
|
$composer = $isGlobalPlugin && $this->globalComposer !== null ? $this->globalComposer : $this->composer;
|
||||||
|
|
||||||
|
$this->io->writeError('<warning>'.$package.($isGlobalPlugin ? ' (installed globally)' : '').' contains a Composer plugin which is currently not in your allow-plugins config. See https://getcomposer.org/allow-plugins</warning>');
|
||||||
|
while (true) {
|
||||||
|
switch ($answer = $this->io->ask('<warning>Do you trust "'.$package.'" to execute code and wish to enable it now? (writes "allow-plugins" to composer.json) [y,n,d,?]</warning> ', '?')) {
|
||||||
|
case 'y':
|
||||||
|
case 'n':
|
||||||
|
case 'd':
|
||||||
|
$allow = $answer === 'y';
|
||||||
|
|
||||||
|
// persist answer in current rules to avoid prompting again if the package gets reloaded
|
||||||
|
if ($isGlobalPlugin) {
|
||||||
|
$this->allowGlobalPluginRules[BasePackage::packageNameToRegexp($package)] = $allow;
|
||||||
|
} else {
|
||||||
|
$this->allowPluginRules[BasePackage::packageNameToRegexp($package)] = $allow;
|
||||||
|
}
|
||||||
|
|
||||||
|
// persist answer in composer.json if it wasn't simply discarded
|
||||||
|
if ($answer === 'y' || $answer === 'n') {
|
||||||
|
$composer->getConfig()->getConfigSource()->addConfigSetting('allow-plugins.'.$package, $allow);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $allow;
|
||||||
|
|
||||||
|
case '?':
|
||||||
|
default:
|
||||||
|
$this->io->writeError(array(
|
||||||
|
'y - add package to allow-plugins in composer.json and let it run immediately',
|
||||||
|
'n - add package (as disallowed) to allow-plugins in composer.json to suppress further prompts',
|
||||||
|
'd - discard this, do not change composer.json and do not allow the plugin to run',
|
||||||
|
'? - print help'
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->io->writeError('<warning>'.$package.($isGlobalPlugin ? ' (installed globally)' : '').' contains a Composer plugin which is blocked by your allow-plugins config. You may add it to the list if you consider it safe. See https://getcomposer.org/allow-plugins</warning>');
|
||||||
|
$this->io->writeError('<warning>You can run "composer '.($isGlobalPlugin ? 'global ' : '').'config --no-plugins allow-plugins.'.$package.' [true|false]" to enable it (true) or keep it disabled and suppress this warning (false)</warning>');
|
||||||
|
}
|
||||||
|
$warned[$package] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,5 +17,8 @@
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"classmap": ["Hooks.php"]
|
"classmap": ["Hooks.php"]
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"allow-plugins": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,5 +17,8 @@
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"classmap": ["Hooks.php"]
|
"classmap": ["Hooks.php"]
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"allow-plugins": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -114,16 +114,17 @@ class PluginInstallerTest extends TestCase
|
||||||
$this->composer->setEventDispatcher(new EventDispatcher($this->composer, $this->io));
|
$this->composer->setEventDispatcher(new EventDispatcher($this->composer, $this->io));
|
||||||
$this->composer->setPackage(new RootPackage('dummy/root', '1.0.0.0', '1.0.0'));
|
$this->composer->setPackage(new RootPackage('dummy/root', '1.0.0.0', '1.0.0'));
|
||||||
|
|
||||||
$this->pm = new PluginManager($this->io, $this->composer);
|
|
||||||
$this->composer->setPluginManager($this->pm);
|
|
||||||
|
|
||||||
$config->merge(array(
|
$config->merge(array(
|
||||||
'config' => array(
|
'config' => array(
|
||||||
'vendor-dir' => $this->directory.'/Fixtures/',
|
'vendor-dir' => $this->directory.'/Fixtures/',
|
||||||
'home' => $this->directory.'/Fixtures',
|
'home' => $this->directory.'/Fixtures',
|
||||||
'bin-dir' => $this->directory.'/Fixtures/bin',
|
'bin-dir' => $this->directory.'/Fixtures/bin',
|
||||||
|
'allow-plugins' => true,
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
|
|
||||||
|
$this->pm = new PluginManager($this->io, $this->composer);
|
||||||
|
$this->composer->setPluginManager($this->pm);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function tearDown()
|
protected function tearDown()
|
||||||
|
@ -145,7 +146,10 @@ class PluginInstallerTest extends TestCase
|
||||||
|
|
||||||
$plugins = $this->pm->getPlugins();
|
$plugins = $this->pm->getPlugins();
|
||||||
$this->assertEquals('installer-v1', $plugins[0]->version); // @phpstan-ignore-line
|
$this->assertEquals('installer-v1', $plugins[0]->version); // @phpstan-ignore-line
|
||||||
$this->assertEquals('activate v1'.PHP_EOL, $this->io->getOutput());
|
$this->assertEquals(
|
||||||
|
'activate v1'.PHP_EOL,
|
||||||
|
$this->io->getOutput()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testInstallMultiplePlugins()
|
public function testInstallMultiplePlugins()
|
||||||
|
|
Loading…
Reference in New Issue