dependabot-composer 0.128.2 → 0.129.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ parameters:
2
+ inferPrivatePropertyTypeFromConstructor: true
3
+ level: 5
4
+ paths:
5
+ - src
@@ -0,0 +1,67 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Dependabot\Composer;
6
+
7
+ use Composer\DependencyResolver\Operation\InstallOperation;
8
+ use Composer\DependencyResolver\Operation\UninstallOperation;
9
+ use Composer\DependencyResolver\Operation\UpdateOperation;
10
+ use Composer\Installer\InstallationManager;
11
+ use Composer\Package\PackageInterface;
12
+ use Composer\Repository\RepositoryInterface;
13
+
14
+ final class DependabotInstallationManager extends InstallationManager
15
+ {
16
+ private array $installed = [];
17
+ private array $updated = [];
18
+ private array $uninstalled = [];
19
+
20
+ public function execute(RepositoryInterface $repo, array $operations, $devMode = true, $runScripts = true): void
21
+ {
22
+ foreach ($operations as $operation) {
23
+ $method = $operation->getOperationType();
24
+ // NOTE: skipping download() step
25
+ $this->$method($repo, $operation);
26
+ }
27
+ }
28
+
29
+ public function install(RepositoryInterface $repo, InstallOperation $operation): void
30
+ {
31
+ $this->installed[] = $operation->getPackage();
32
+ }
33
+
34
+ public function update(RepositoryInterface $repo, UpdateOperation $operation): void
35
+ {
36
+ $this->updated[] = [$operation->getInitialPackage(), $operation->getTargetPackage()];
37
+ }
38
+
39
+ public function uninstall(RepositoryInterface $repo, UninstallOperation $operation): void
40
+ {
41
+ $this->uninstalled[] = $operation->getPackage();
42
+ }
43
+
44
+ /**
45
+ * @return PackageInterface[]
46
+ */
47
+ public function getInstalledPackages(): array
48
+ {
49
+ return $this->installed;
50
+ }
51
+
52
+ /**
53
+ * @return PackageInterface[]
54
+ */
55
+ public function getUpdatedPackages(): array
56
+ {
57
+ return $this->updated;
58
+ }
59
+
60
+ /**
61
+ * @return PackageInterface[]
62
+ */
63
+ public function getUninstalledPackages(): array
64
+ {
65
+ return $this->uninstalled;
66
+ }
67
+ }
@@ -0,0 +1,23 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Dependabot\Composer;
6
+
7
+ use Composer\Package\PackageInterface;
8
+ use Composer\Plugin\PluginManager;
9
+
10
+ final class DependabotPluginManager extends PluginManager
11
+ {
12
+ public function registerPackage(PackageInterface $package, $failOnMissingClasses = false, $isGlobalPlugin = false): void
13
+ {
14
+ // This package does some setup for PHP_CodeSniffer, but errors out the
15
+ // install if Symfony isn't installed (which it won't be for a lockfile
16
+ // only install run). Safe to ignore
17
+ if (strpos($package->getName(), 'phpcodesniffer') !== false) {
18
+ return;
19
+ }
20
+
21
+ parent::registerPackage($package, $failOnMissingClasses, $isGlobalPlugin);
22
+ }
23
+ }
@@ -0,0 +1,25 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Dependabot\Composer;
6
+
7
+ use Composer\IO\NullIO;
8
+
9
+ final class ExceptionIO extends NullIO
10
+ {
11
+ private bool $raise_next_error = false;
12
+
13
+ public function writeError($messages, $newline = true, $verbosity = self::NORMAL): void
14
+ {
15
+ if (is_array($messages)) {
16
+ return;
17
+ }
18
+ if ($this->raise_next_error) {
19
+ throw new \RuntimeException('Your requirements could not be resolved to an installable set of packages.' . $messages);
20
+ }
21
+ if (strpos($messages, 'Your requirements could not be resolved') !== false) {
22
+ $this->raise_next_error = true;
23
+ }
24
+ }
25
+ }
@@ -0,0 +1,28 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Dependabot\Composer;
6
+
7
+ use Composer\Package\Locker;
8
+
9
+ final class Hasher
10
+ {
11
+ /**
12
+ * @throws \RuntimeException
13
+ */
14
+ public static function getContentHash(array $args): string
15
+ {
16
+ [$workingDirectory] = $args;
17
+
18
+ $config = $workingDirectory . '/composer.json';
19
+
20
+ $contents = file_get_contents($config);
21
+
22
+ if (!is_string($contents)) {
23
+ throw new \RuntimeException(sprintf('Failed to load contents of "%s".', $config));
24
+ }
25
+
26
+ return Locker::getContentHash($contents);
27
+ }
28
+ }
@@ -0,0 +1,133 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Dependabot\Composer;
6
+
7
+ use Composer\DependencyResolver\Request;
8
+ use Composer\Factory;
9
+ use Composer\Installer;
10
+ use Composer\Package\PackageInterface;
11
+ use Composer\Util\Filesystem;
12
+
13
+ final class UpdateChecker
14
+ {
15
+ public static function getLatestResolvableVersion(array $args): ?string
16
+ {
17
+ [$workingDirectory, $dependencyName, $gitCredentials, $registryCredentials] = $args;
18
+
19
+ $httpBasicCredentials = [];
20
+
21
+ foreach ($gitCredentials as $credentials) {
22
+ $httpBasicCredentials[$credentials['host']] = [
23
+ 'username' => $credentials['username'],
24
+ 'password' => $credentials['password'],
25
+ ];
26
+ }
27
+
28
+ foreach ($registryCredentials as $credentials) {
29
+ $httpBasicCredentials[$credentials['registry']] = [
30
+ 'username' => $credentials['username'],
31
+ 'password' => $credentials['password'],
32
+ ];
33
+ }
34
+
35
+ $io = new ExceptionIO();
36
+
37
+ $composer = Factory::create($io, $workingDirectory . '/composer.json');
38
+
39
+ $config = $composer->getConfig();
40
+
41
+ if (0 < count($httpBasicCredentials)) {
42
+ $config->merge([
43
+ 'config' => [
44
+ 'http-basic' => $httpBasicCredentials,
45
+ ],
46
+ ]);
47
+
48
+ $io->loadConfiguration($config);
49
+ }
50
+
51
+ $installationManager = new DependabotInstallationManager($composer->getLoop(), $io);
52
+
53
+ $fs = new Filesystem(null);
54
+ $binaryInstaller = new Installer\BinaryInstaller($io, rtrim($composer->getConfig()->get('bin-dir'), '/'), $composer->getConfig()->get('bin-compat'), $fs);
55
+
56
+ $installationManager->addInstaller(new Installer\LibraryInstaller($io, $composer, null, $fs, $binaryInstaller));
57
+ $installationManager->addInstaller(new Installer\PluginInstaller($io, $composer, $fs, $binaryInstaller));
58
+ $installationManager->addInstaller(new Installer\MetapackageInstaller($io));
59
+
60
+ $install = new Installer(
61
+ $io,
62
+ $config,
63
+ $composer->getPackage(),
64
+ $composer->getDownloadManager(),
65
+ $composer->getRepositoryManager(),
66
+ $composer->getLocker(),
67
+ $installationManager,
68
+ $composer->getEventDispatcher(),
69
+ $composer->getAutoloadGenerator()
70
+ );
71
+
72
+ // For all potential options, see UpdateCommand in composer
73
+ $install
74
+ ->setUpdate(true)
75
+ ->setDevMode(true)
76
+ ->setUpdateAllowTransitiveDependencies(Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS)
77
+ ->setDumpAutoloader(false)
78
+ ->setRunScripts(false)
79
+ ->setIgnorePlatformRequirements(false);
80
+
81
+ // if no lock is present, we do not do a partial update as
82
+ // this is not supported by the Installer
83
+ if ($composer->getLocker()->isLocked()) {
84
+ $install->setUpdateAllowList([$dependencyName]);
85
+ }
86
+
87
+ $install->run();
88
+
89
+ $installedPackages = $installationManager->getInstalledPackages();
90
+
91
+ $updatedPackage = current(array_filter($installedPackages, static function (PackageInterface $package) use ($dependencyName): bool {
92
+ return $package->getName() === $dependencyName;
93
+ }));
94
+
95
+ // We found the package in the list of updated packages. Return its version.
96
+ if ($updatedPackage instanceof PackageInterface) {
97
+ return preg_replace('/^([v])/', '', $updatedPackage->getPrettyVersion());
98
+ }
99
+
100
+ // We didn't find the package in the list of updated packages. Check if
101
+ // it was replaced by another package (in which case we can ignore).
102
+ foreach ($composer->getPackage()->getReplaces() as $link) {
103
+ if ($link->getTarget() === $dependencyName) {
104
+ return null;
105
+ }
106
+ }
107
+
108
+ foreach ($installedPackages as $package) {
109
+ foreach ($package->getReplaces() as $link) {
110
+ if ($link->getTarget() === $dependencyName) {
111
+ return null;
112
+ }
113
+ }
114
+ }
115
+
116
+ // Similarly, check if the package was provided by any other package.
117
+ foreach ($composer->getPackage()->getProvides() as $link) {
118
+ if ($link->getTarget() === $dependencyName) {
119
+ return preg_replace('/^([v])/', '', $link->getPrettyConstraint());
120
+ }
121
+ }
122
+
123
+ foreach ($installedPackages as $package) {
124
+ foreach ($package->getProvides() as $link) {
125
+ if ($link->getTarget() === $dependencyName) {
126
+ return preg_replace('/^([v])/', '', $link->getPrettyConstraint());
127
+ }
128
+ }
129
+ }
130
+
131
+ throw new \RuntimeException('Package not found in updated packages!');
132
+ }
133
+ }
@@ -0,0 +1,99 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Dependabot\Composer;
6
+
7
+ use Composer\DependencyResolver\Request;
8
+ use Composer\Factory;
9
+ use Composer\Installer;
10
+
11
+ final class Updater
12
+ {
13
+ /**
14
+ * @throws \RuntimeException
15
+ */
16
+ public static function update(array $args): array
17
+ {
18
+ [$workingDirectory, $dependencyName, $dependencyVersion, $gitCredentials, $registryCredentials] = $args;
19
+
20
+ // Change working directory to the one provided, this ensures that we
21
+ // install dependencies into the working dir, rather than a vendor folder
22
+ // in the root of the project
23
+ $originalDir = getcwd();
24
+
25
+ if (!is_string($originalDir)) {
26
+ throw new \RuntimeException('Failed determining the current working directory.');
27
+ }
28
+
29
+ chdir($workingDirectory);
30
+
31
+ $io = new ExceptionIO();
32
+ $composer = Factory::create($io);
33
+ $config = $composer->getConfig();
34
+ $httpBasicCredentials = [];
35
+
36
+ $pm = new DependabotPluginManager($io, $composer, null, false);
37
+ $composer->setPluginManager($pm);
38
+ $pm->loadInstalledPlugins();
39
+
40
+ foreach ($gitCredentials as &$cred) {
41
+ $httpBasicCredentials[$cred['host']] = [
42
+ 'username' => $cred['username'],
43
+ 'password' => $cred['password'],
44
+ ];
45
+ }
46
+
47
+ foreach ($registryCredentials as &$cred) {
48
+ $httpBasicCredentials[$cred['registry']] = [
49
+ 'username' => $cred['username'],
50
+ 'password' => $cred['password'],
51
+ ];
52
+ }
53
+
54
+ if ($httpBasicCredentials) {
55
+ $config->merge(
56
+ [
57
+ 'config' => [
58
+ 'http-basic' => $httpBasicCredentials,
59
+ ],
60
+ ]
61
+ );
62
+ $io->loadConfiguration($config);
63
+ }
64
+
65
+ $install = new Installer(
66
+ $io,
67
+ $config,
68
+ $composer->getPackage(),
69
+ $composer->getDownloadManager(),
70
+ $composer->getRepositoryManager(),
71
+ $composer->getLocker(),
72
+ $composer->getInstallationManager(),
73
+ $composer->getEventDispatcher(),
74
+ $composer->getAutoloadGenerator()
75
+ );
76
+
77
+ // For all potential options, see UpdateCommand in composer
78
+ $install
79
+ ->setWriteLock(true)
80
+ ->setUpdate(true)
81
+ ->setDevMode(true)
82
+ ->setUpdateAllowList([$dependencyName])
83
+ ->setUpdateAllowTransitiveDependencies(Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS)
84
+ ->setExecuteOperations(true)
85
+ ->setDumpAutoloader(false)
86
+ ->setRunScripts(false)
87
+ ->setIgnorePlatformRequirements(false);
88
+
89
+ $install->run();
90
+
91
+ $result = [
92
+ 'composer.lock' => file_get_contents('composer.lock'),
93
+ ];
94
+
95
+ chdir($originalDir);
96
+
97
+ return $result;
98
+ }
99
+ }
@@ -34,6 +34,7 @@ module Dependabot
34
34
  (?<!with|for|by)\sext\-[^\s/]+\s.*?\s(?=->)|
35
35
  (?<=requires\s)php(?:\-[^\s/]+)?\s.*?\s(?=->)
36
36
  }x.freeze
37
+ MISSING_ENV_VAR_REGEX = /Environment variable '(?<env_var>.[^']+)' is not set/.freeze
37
38
 
38
39
  def initialize(dependencies:, dependency_files:, credentials:)
39
40
  @dependencies = dependencies
@@ -179,10 +180,21 @@ module Dependabot
179
180
  raise GitDependenciesNotReachable, dependency_url
180
181
  end
181
182
 
182
- if error.message.start_with?("Could not find a key for ACF PRO")
183
+ # NOTE: This matches an error message from composer plugins used to install ACF PRO
184
+ # https://github.com/PhilippBaschke/acf-pro-installer/blob/772cec99c6ef8bc67ba6768419014cc60d141b27/src/ACFProInstaller/Exceptions/MissingKeyException.php#L14
185
+ # https://github.com/pivvenit/acf-pro-installer/blob/f2d4812839ee2c333709b0ad4c6c134e4c25fd6d/src/Exceptions/MissingKeyException.php#L25
186
+ if error.message.start_with?("Could not find a key for ACF PRO") ||
187
+ error.message.start_with?("Could not find a license key for ACF PRO")
183
188
  raise MissingEnvironmentVariable, "ACF_PRO_KEY"
184
189
  end
185
190
 
191
+ # NOTE: This matches error output from a composer plugin (private-composer-installer):
192
+ # https://github.com/ffraenz/private-composer-installer/blob/8655e3da4e8f99203f13ccca33b9ab953ad30a31/src/Exception/MissingEnvException.php#L22
193
+ if error.message.match?(MISSING_ENV_VAR_REGEX)
194
+ env_var = error.message.match(MISSING_ENV_VAR_REGEX).named_captures.fetch("env_var")
195
+ raise MissingEnvironmentVariable, env_var
196
+ end
197
+
186
198
  if error.message.start_with?("Unknown downloader type: npm-sign") ||
187
199
  error.message.include?("file could not be downloaded") ||
188
200
  error.message.include?("configuration does not allow connect")
@@ -197,6 +209,7 @@ module Dependabot
197
209
  raise PrivateSourceAuthenticationFailure, source
198
210
  end
199
211
 
212
+ # NOTE: This error is raised by composer v1
200
213
  if error.message.include?("Argument 1 passed to Composer")
201
214
  msg = "One of your Composer plugins is not compatible with the "\
202
215
  "latest version of Composer. Please update Composer and "\
@@ -204,6 +217,12 @@ module Dependabot
204
217
  raise DependencyFileNotResolvable, msg
205
218
  end
206
219
 
220
+ # NOTE: This error is raised by composer v2 and includes helpful
221
+ # information about which plugins or dependencies are not compatible
222
+ if error.message.include?("Your requirements could not be resolved")
223
+ raise DependencyFileNotResolvable, error.message
224
+ end
225
+
207
226
  raise error
208
227
  end
209
228
  # rubocop:enable Metrics/AbcSize
@@ -425,7 +444,17 @@ module Dependabot
425
444
  end
426
445
 
427
446
  def php_helper_path
428
- NativeHelpers.composer_helper_path
447
+ NativeHelpers.composer_helper_path(composer_version: composer_version)
448
+ end
449
+
450
+ def composer_version
451
+ @composer_version ||=
452
+ begin
453
+ return "v2" unless parsed_lockfile["plugin-api-version"]
454
+
455
+ version = Version.new(parsed_lockfile["plugin-api-version"])
456
+ version.canonical_segments.first == 1 ? "v1" : "v2"
457
+ end
429
458
  end
430
459
 
431
460
  def credentials_env