forked from composer/composer
/
PluginManager.php
763 lines (671 loc) · 31.1 KB
/
PluginManager.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Plugin;
use Composer\Composer;
use Composer\EventDispatcher\EventSubscriberInterface;
use Composer\Installer\InstallerInterface;
use Composer\IO\IOInterface;
use Composer\Package\BasePackage;
use Composer\Package\CompletePackage;
use Composer\Package\Package;
use Composer\Package\Version\VersionParser;
use Composer\Pcre\Preg;
use Composer\Repository\RepositoryInterface;
use Composer\Repository\InstalledRepository;
use Composer\Repository\RootPackageRepository;
use Composer\Package\PackageInterface;
use Composer\Package\Link;
use Composer\Semver\Constraint\Constraint;
use Composer\Plugin\Capability\Capability;
use Composer\Util\PackageSorter;
/**
* Plugin manager
*
* @author Nils Adermann <naderman@naderman.de>
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
class PluginManager
{
/** @var Composer */
protected $composer;
/** @var IOInterface */
protected $io;
/** @var ?Composer */
protected $globalComposer;
/** @var VersionParser */
protected $versionParser;
/** @var bool */
protected $disablePlugins = false;
/** @var array<PluginInterface> */
protected $plugins = array();
/** @var array<string, PluginInterface|InstallerInterface> */
protected $registeredPlugins = array();
/**
* @var array<non-empty-string, bool>|null
*/
private $allowPluginRules;
/**
* @var array<non-empty-string, bool>|null
*/
private $allowGlobalPluginRules;
/** @var int */
private static $classCounter = 0;
/**
* Initializes plugin manager
*
* @param IOInterface $io
* @param Composer $composer
* @param Composer $globalComposer
* @param bool $disablePlugins
*/
public function __construct(IOInterface $io, Composer $composer, Composer $globalComposer = null, $disablePlugins = false)
{
$this->io = $io;
$this->composer = $composer;
$this->globalComposer = $globalComposer;
$this->versionParser = new VersionParser();
$this->disablePlugins = $disablePlugins;
$this->allowPluginRules = $this->parseAllowedPlugins($composer->getConfig()->get('allow-plugins'));
$this->allowGlobalPluginRules = $this->parseAllowedPlugins($globalComposer !== null ? $globalComposer->getConfig()->get('allow-plugins') : false);
}
/**
* Loads all plugins from currently installed plugin packages
*
* @return void
*/
public function loadInstalledPlugins()
{
if ($this->disablePlugins) {
return;
}
$repo = $this->composer->getRepositoryManager()->getLocalRepository();
$globalRepo = $this->globalComposer !== null ? $this->globalComposer->getRepositoryManager()->getLocalRepository() : null;
$this->loadRepository($repo, false);
if ($globalRepo) {
$this->loadRepository($globalRepo, true);
}
}
/**
* Deactivate all plugins from currently installed plugin packages
*
* @return void
*/
public function deactivateInstalledPlugins()
{
if ($this->disablePlugins) {
return;
}
$repo = $this->composer->getRepositoryManager()->getLocalRepository();
$globalRepo = $this->globalComposer ? $this->globalComposer->getRepositoryManager()->getLocalRepository() : null;
$this->deactivateRepository($repo, false);
if ($globalRepo) {
$this->deactivateRepository($globalRepo, true);
}
}
/**
* Gets all currently active plugin instances
*
* @return array<PluginInterface> plugins
*/
public function getPlugins()
{
return $this->plugins;
}
/**
* Gets global composer or null when main composer is not fully loaded
*
* @return Composer|null
*/
public function getGlobalComposer()
{
return $this->globalComposer;
}
/**
* Register a plugin package, activate it etc.
*
* If it's of type composer-installer it is registered as an installer
* instead for BC
*
* @param PackageInterface $package
* @param bool $failOnMissingClasses By default this silently skips plugins that can not be found, but if set to true it fails with an exception
* @param bool $isGlobalPlugin Set to true to denote plugins which are installed in the global Composer directory
*
* @return void
*
* @throws \UnexpectedValueException
*/
public function registerPackage(PackageInterface $package, $failOnMissingClasses = false, $isGlobalPlugin = false)
{
if ($this->disablePlugins) {
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') {
$requiresComposer = null;
foreach ($package->getRequires() as $link) { /** @var Link $link */
if ('composer-plugin-api' === $link->getTarget()) {
$requiresComposer = $link->getConstraint();
break;
}
}
if (!$requiresComposer) {
throw new \RuntimeException("Plugin ".$package->getName()." is missing a require statement for a version of the composer-plugin-api package.");
}
$currentPluginApiVersion = $this->getPluginApiVersion();
$currentPluginApiConstraint = new Constraint('==', $this->versionParser->normalize($currentPluginApiVersion));
if ($requiresComposer->getPrettyString() === $this->getPluginApiVersion()) {
$this->io->writeError('<warning>The "' . $package->getName() . '" plugin requires composer-plugin-api '.$this->getPluginApiVersion().', this *WILL* break in the future and it should be fixed ASAP (require ^'.$this->getPluginApiVersion().' instead for example).</warning>');
} elseif (!$requiresComposer->matches($currentPluginApiConstraint)) {
$this->io->writeError('<warning>The "' . $package->getName() . '" plugin '.($isGlobalPlugin ? '(installed globally) ' : '').'was skipped because it requires a Plugin API version ("' . $requiresComposer->getPrettyString() . '") that does not match your Composer installation ("' . $currentPluginApiVersion . '"). You may need to run composer update with the "--no-plugins" option.</warning>');
return;
}
if ($package->getName() === 'symfony/flex' && Preg::isMatch('{^[0-9.]+$}', $package->getVersion()) && version_compare($package->getVersion(), '1.9.8', '<')) {
$this->io->writeError('<warning>The "' . $package->getName() . '" plugin '.($isGlobalPlugin ? '(installed globally) ' : '').'was skipped because it is not compatible with Composer 2+. Make sure to update it to version 1.9.8 or greater.</warning>');
return;
}
}
$oldInstallerPlugin = ($package->getType() === 'composer-installer');
if (isset($this->registeredPlugins[$package->getName()])) {
return;
}
$extra = $package->getExtra();
if (empty($extra['class'])) {
throw new \UnexpectedValueException('Error while installing '.$package->getPrettyName().', composer-plugin packages should have a class defined in their extra key to be usable.');
}
$classes = is_array($extra['class']) ? $extra['class'] : array($extra['class']);
$localRepo = $this->composer->getRepositoryManager()->getLocalRepository();
$globalRepo = $this->globalComposer !== null ? $this->globalComposer->getRepositoryManager()->getLocalRepository() : null;
$rootPackage = clone $this->composer->getPackage();
// clear files autoload rules from the root package as the root dependencies are not
// necessarily all present yet when booting this runtime autoloader
$rootPackageAutoloads = $rootPackage->getAutoload();
$rootPackageAutoloads['files'] = array();
$rootPackage->setAutoload($rootPackageAutoloads);
$rootPackageAutoloads = $rootPackage->getDevAutoload();
$rootPackageAutoloads['files'] = array();
$rootPackage->setDevAutoload($rootPackageAutoloads);
unset($rootPackageAutoloads);
$rootPackageRepo = new RootPackageRepository($rootPackage);
$installedRepo = new InstalledRepository(array($localRepo, $rootPackageRepo));
if ($globalRepo) {
$installedRepo->addRepository($globalRepo);
}
$autoloadPackages = array($package->getName() => $package);
$autoloadPackages = $this->collectDependencies($installedRepo, $autoloadPackages, $package);
$generator = $this->composer->getAutoloadGenerator();
$autoloads = array(array($rootPackage, ''));
foreach ($autoloadPackages as $autoloadPackage) {
if ($autoloadPackage === $rootPackage) {
continue;
}
$downloadPath = $this->getInstallPath($autoloadPackage, $globalRepo && $globalRepo->hasPackage($autoloadPackage));
$autoloads[] = array($autoloadPackage, $downloadPath);
}
$map = $generator->parseAutoloads($autoloads, $rootPackage);
$classLoader = $generator->createLoader($map, $this->composer->getConfig()->get('vendor-dir'));
$classLoader->register(false);
foreach ($map['files'] as $fileIdentifier => $file) {
// exclude laminas/laminas-zendframework-bridge:src/autoload.php as it breaks Composer in some conditions
// see https://github.com/composer/composer/issues/10349 and https://github.com/composer/composer/issues/10401
// this hack can be removed once this deprecated package stop being installed
if ($fileIdentifier === '7e9bd612cc444b3eed788ebbe46263a0') {
continue;
}
\Composer\Autoload\composerRequire($fileIdentifier, $file);
}
foreach ($classes as $class) {
if (class_exists($class, false)) {
$class = trim($class, '\\');
$path = $classLoader->findFile($class);
$code = file_get_contents($path);
$separatorPos = strrpos($class, '\\');
$className = $class;
if ($separatorPos) {
$className = substr($class, $separatorPos + 1);
}
$code = Preg::replace('{^((?:final\s+)?(?:\s*))class\s+('.preg_quote($className).')}mi', '$1class $2_composer_tmp'.self::$classCounter, $code, 1);
$code = strtr($code, array(
'__FILE__' => var_export($path, true),
'__DIR__' => var_export(dirname($path), true),
'__CLASS__' => var_export($class, true),
));
$code = Preg::replace('/^\s*<\?(php)?/i', '', $code, 1);
eval($code);
$class .= '_composer_tmp'.self::$classCounter;
self::$classCounter++;
}
if ($oldInstallerPlugin) {
if (!is_a($class, 'Composer\Installer\InstallerInterface', true)) {
throw new \RuntimeException('Could not activate plugin "'.$package->getName().'" as "'.$class.'" does not implement Composer\Installer\InstallerInterface');
}
$this->io->writeError('<warning>Loading "'.$package->getName() . '" '.($isGlobalPlugin ? '(installed globally) ' : '').'which is a legacy composer-installer built for Composer 1.x, it is likely to cause issues as you are running Composer 2.x.</warning>');
$installer = new $class($this->io, $this->composer);
$this->composer->getInstallationManager()->addInstaller($installer);
$this->registeredPlugins[$package->getName()] = $installer;
} elseif (class_exists($class)) {
if (!is_a($class, 'Composer\Plugin\PluginInterface', true)) {
throw new \RuntimeException('Could not activate plugin "'.$package->getName().'" as "'.$class.'" does not implement Composer\Plugin\PluginInterface');
}
$plugin = new $class();
$this->addPlugin($plugin, $isGlobalPlugin, $package);
$this->registeredPlugins[$package->getName()] = $plugin;
} elseif ($failOnMissingClasses) {
throw new \UnexpectedValueException('Plugin '.$package->getName().' could not be initialized, class not found: '.$class);
}
}
}
/**
* Deactivates a plugin package
*
* If it's of type composer-installer it is unregistered from the installers
* instead for BC
*
* @param PackageInterface $package
*
* @return void
*
* @throws \UnexpectedValueException
*/
public function deactivatePackage(PackageInterface $package)
{
if ($this->disablePlugins) {
return;
}
if (!isset($this->registeredPlugins[$package->getName()])) {
return;
}
$plugin = $this->registeredPlugins[$package->getName()];
unset($this->registeredPlugins[$package->getName()]);
if ($plugin instanceof InstallerInterface) {
$this->composer->getInstallationManager()->removeInstaller($plugin);
} else {
$this->removePlugin($plugin);
}
}
/**
* Uninstall a plugin package
*
* If it's of type composer-installer it is unregistered from the installers
* instead for BC
*
* @param PackageInterface $package
*
* @return void
*
* @throws \UnexpectedValueException
*/
public function uninstallPackage(PackageInterface $package)
{
if ($this->disablePlugins) {
return;
}
if (!isset($this->registeredPlugins[$package->getName()])) {
return;
}
$plugin = $this->registeredPlugins[$package->getName()];
if ($plugin instanceof InstallerInterface) {
$this->deactivatePackage($package);
} else {
unset($this->registeredPlugins[$package->getName()]);
$this->removePlugin($plugin);
$this->uninstallPlugin($plugin);
}
}
/**
* Returns the version of the internal composer-plugin-api package.
*
* @return string
*/
protected function getPluginApiVersion()
{
return PluginInterface::PLUGIN_API_VERSION;
}
/**
* Adds a plugin, activates it and registers it with the event dispatcher
*
* Ideally plugin packages should be registered via registerPackage, but if you use Composer
* programmatically and want to register a plugin class directly this is a valid way
* to do it.
*
* @param PluginInterface $plugin plugin instance
* @param bool $isGlobalPlugin
* @param ?PackageInterface $sourcePackage Package from which the plugin comes from
*
* @return void
*/
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();
if ($sourcePackage) {
$details[] = 'from '.$sourcePackage->getName();
}
if ($isGlobalPlugin) {
$details[] = 'installed globally';
}
$this->io->writeError('Loading plugin '.get_class($plugin).($details ? ' ('.implode(', ', $details).')' : ''), true, IOInterface::DEBUG);
$this->plugins[] = $plugin;
$plugin->activate($this->composer, $this->io);
if ($plugin instanceof EventSubscriberInterface) {
$this->composer->getEventDispatcher()->addSubscriber($plugin);
}
}
/**
* Removes a plugin, deactivates it and removes any listener the plugin has set on the plugin instance
*
* Ideally plugin packages should be deactivated via deactivatePackage, but if you use Composer
* programmatically and want to deregister a plugin class directly this is a valid way
* to do it.
*
* @param PluginInterface $plugin plugin instance
*
* @return void
*/
public function removePlugin(PluginInterface $plugin)
{
$index = array_search($plugin, $this->plugins, true);
if ($index === false) {
return;
}
$this->io->writeError('Unloading plugin '.get_class($plugin), true, IOInterface::DEBUG);
unset($this->plugins[$index]);
$plugin->deactivate($this->composer, $this->io);
$this->composer->getEventDispatcher()->removeListener($plugin);
}
/**
* Notifies a plugin it is being uninstalled and should clean up
*
* Ideally plugin packages should be uninstalled via uninstallPackage, but if you use Composer
* programmatically and want to deregister a plugin class directly this is a valid way
* to do it.
*
* @param PluginInterface $plugin plugin instance
*
* @return void
*/
public function uninstallPlugin(PluginInterface $plugin)
{
$this->io->writeError('Uninstalling plugin '.get_class($plugin), true, IOInterface::DEBUG);
$plugin->uninstall($this->composer, $this->io);
}
/**
* Load all plugins and installers from a repository
*
* If a plugin requires another plugin, the required one will be loaded first
*
* Note that plugins in the specified repository that rely on events that
* have fired prior to loading will be missed. This means you likely want to
* call this method as early as possible.
*
* @param RepositoryInterface $repo Repository to scan for plugins to install
* @param bool $isGlobalRepo
*
* @return void
*
* @throws \RuntimeException
*/
private function loadRepository(RepositoryInterface $repo, $isGlobalRepo)
{
$packages = $repo->getPackages();
$weights = array();
foreach ($packages as $package) {
if ($package->getType() === 'composer-plugin') {
$extra = $package->getExtra();
if ($package->getName() === 'composer/installers' || (isset($extra['plugin-modifies-install-path']) && $extra['plugin-modifies-install-path'] === true)) {
$weights[$package->getName()] = -10000;
}
}
}
$sortedPackages = PackageSorter::sortPackages($packages, $weights);
foreach ($sortedPackages as $package) {
if (!($package instanceof CompletePackage)) {
continue;
}
if ('composer-plugin' === $package->getType()) {
$this->registerPackage($package, false, $isGlobalRepo);
// Backward compatibility
} elseif ('composer-installer' === $package->getType()) {
$this->registerPackage($package, false, $isGlobalRepo);
}
}
}
/**
* Deactivate all plugins and installers from a repository
*
* If a plugin requires another plugin, the required one will be deactivated last
*
* @param RepositoryInterface $repo Repository to scan for plugins to install
* @param bool $isGlobalRepo
*
* @return void
*/
private function deactivateRepository(RepositoryInterface $repo, $isGlobalRepo)
{
$packages = $repo->getPackages();
$sortedPackages = array_reverse(PackageSorter::sortPackages($packages));
foreach ($sortedPackages as $package) {
if (!($package instanceof CompletePackage)) {
continue;
}
if ('composer-plugin' === $package->getType()) {
$this->deactivatePackage($package);
// Backward compatibility
} elseif ('composer-installer' === $package->getType()) {
$this->deactivatePackage($package);
}
}
}
/**
* Recursively generates a map of package names to packages for all deps
*
* @param InstalledRepository $installedRepo Set of local repos
* @param array<string, PackageInterface> $collected Current state of the map for recursion
* @param PackageInterface $package The package to analyze
*
* @return array<string, PackageInterface> Map of package names to packages
*/
private function collectDependencies(InstalledRepository $installedRepo, array $collected, PackageInterface $package)
{
foreach ($package->getRequires() as $requireLink) {
foreach ($installedRepo->findPackagesWithReplacersAndProviders($requireLink->getTarget()) as $requiredPackage) {
if (!isset($collected[$requiredPackage->getName()])) {
$collected[$requiredPackage->getName()] = $requiredPackage;
$collected = $this->collectDependencies($installedRepo, $collected, $requiredPackage);
}
}
}
return $collected;
}
/**
* Retrieves the path a package is installed to.
*
* @param PackageInterface $package
* @param bool $global Whether this is a global package
*
* @return string Install path
*/
private function getInstallPath(PackageInterface $package, $global = false)
{
if (!$global) {
return $this->composer->getInstallationManager()->getInstallPath($package);
}
return $this->globalComposer->getInstallationManager()->getInstallPath($package);
}
/**
* @param PluginInterface $plugin
* @param string $capability
* @throws \RuntimeException On empty or non-string implementation class name value
* @return null|string The fully qualified class of the implementation or null if Plugin is not of Capable type or does not provide it
*/
protected function getCapabilityImplementationClassName(PluginInterface $plugin, $capability)
{
if (!($plugin instanceof Capable)) {
return null;
}
$capabilities = (array) $plugin->getCapabilities();
if (!empty($capabilities[$capability]) && is_string($capabilities[$capability]) && trim($capabilities[$capability])) {
return trim($capabilities[$capability]);
}
if (
array_key_exists($capability, $capabilities)
&& (empty($capabilities[$capability]) || !is_string($capabilities[$capability]) || !trim($capabilities[$capability]))
) {
throw new \UnexpectedValueException('Plugin '.get_class($plugin).' provided invalid capability class name(s), got '.var_export($capabilities[$capability], true));
}
return null;
}
/**
* @template CapabilityClass of Capability
* @param PluginInterface $plugin
* @param class-string<CapabilityClass> $capabilityClassName The fully qualified name of the API interface which the plugin may provide
* an implementation of.
* @param array<mixed> $ctorArgs Arguments passed to Capability's constructor.
* Keeping it an array will allow future values to be passed w\o changing the signature.
* @return null|Capability
* @phpstan-param class-string<CapabilityClass> $capabilityClassName
* @phpstan-return null|CapabilityClass
*/
public function getPluginCapability(PluginInterface $plugin, $capabilityClassName, array $ctorArgs = array())
{
if ($capabilityClass = $this->getCapabilityImplementationClassName($plugin, $capabilityClassName)) {
if (!class_exists($capabilityClass)) {
throw new \RuntimeException("Cannot instantiate Capability, as class $capabilityClass from plugin ".get_class($plugin)." does not exist.");
}
$ctorArgs['plugin'] = $plugin;
$capabilityObj = new $capabilityClass($ctorArgs);
// FIXME these could use is_a and do the check *before* instantiating once drop support for php<5.3.9
if (!$capabilityObj instanceof Capability || !$capabilityObj instanceof $capabilityClassName) {
throw new \RuntimeException(
'Class ' . $capabilityClass . ' must implement both Composer\Plugin\Capability\Capability and '. $capabilityClassName . '.'
);
}
return $capabilityObj;
}
return null;
}
/**
* @template CapabilityClass of Capability
* @param class-string<CapabilityClass> $capabilityClassName The fully qualified name of the API interface which the plugin may provide
* an implementation of.
* @param array<mixed> $ctorArgs Arguments passed to Capability's constructor.
* Keeping it an array will allow future values to be passed w\o changing the signature.
* @return CapabilityClass[]
*/
public function getPluginCapabilities($capabilityClassName, array $ctorArgs = array())
{
$capabilities = array();
foreach ($this->getPlugins() as $plugin) {
if ($capability = $this->getPluginCapability($plugin, $capabilityClassName, $ctorArgs)) {
$capabilities[] = $capability;
}
}
return $capabilities;
}
/**
* @param array<string, bool>|bool|null $allowPluginsConfig
* @return array<non-empty-string, bool>|null
*/
private function parseAllowedPlugins($allowPluginsConfig)
{
if (null === $allowPluginsConfig) {
return null;
}
if (true === $allowPluginsConfig) {
return array('{}' => true);
}
if (false === $allowPluginsConfig) {
return array('{}' => 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 ($package === 'composer/package-versions-deprecated') {
return false;
}
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('Do you trust "<info>'.$package.'</info>" to execute code and wish to enable it now? (writes "allow-plugins" to composer.json) [<comment>y,n,d,?</comment>] ', '?')) {
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;
}
}