Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 286 |
|
0.00% |
0 / 26 |
CRAP | |
0.00% |
0 / 1 |
| installer | |
0.00% |
0 / 286 |
|
0.00% |
0 / 26 |
8556 | |
0.00% |
0 / 1 |
| __construct | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 | |||
| install | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
| do_install | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
20 | |||
| get_installed_packages | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
| get_composer | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
| do_get_installed_packages | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
30 | |||
| get_available_packages | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
| do_get_available_packages | |
0.00% |
0 / 58 |
|
0.00% |
0 / 1 |
210 | |||
| check_requirements | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
| get_compatible_versions | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
156 | |||
| generate_ext_json_file | |
0.00% |
0 / 46 |
|
0.00% |
0 / 1 |
132 | |||
| resolve_highest_versions | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
156 | |||
| restore_ext_json_file | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
| get_core_packages | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
| get_core_php_requirement | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| get_composer_repositories | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
30 | |||
| get_composer_ext_json_filename | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| get_extra_dependencies | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| set_repositories | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| set_packagist | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| set_composer_filename | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| set_packages_vendor_dir | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| set_root_path | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| move_to_root | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
| restore_cwd | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
| wrap | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * |
| 4 | * This file is part of the phpBB Forum Software package. |
| 5 | * |
| 6 | * @copyright (c) phpBB Limited <https://www.phpbb.com> |
| 7 | * @license GNU General Public License, version 2 (GPL-2.0) |
| 8 | * |
| 9 | * For full copyright and license information, please see |
| 10 | * the docs/CREDITS.txt file. |
| 11 | * |
| 12 | */ |
| 13 | |
| 14 | namespace phpbb\composer; |
| 15 | |
| 16 | use Composer\Composer; |
| 17 | use Composer\DependencyResolver\Request as composer_request; |
| 18 | use Composer\Factory; |
| 19 | use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory; |
| 20 | use Composer\IO\IOInterface; |
| 21 | use Composer\IO\NullIO; |
| 22 | use Composer\Json\JsonFile; |
| 23 | use Composer\Json\JsonValidationException; |
| 24 | use Composer\Package\BasePackage; |
| 25 | use Composer\Package\CompleteAliasPackage; |
| 26 | use Composer\Package\CompletePackage; |
| 27 | use Composer\Package\PackageInterface; |
| 28 | use Composer\PartialComposer; |
| 29 | use Composer\Repository\ComposerRepository; |
| 30 | use Composer\Semver\Constraint\ConstraintInterface; |
| 31 | use Composer\Semver\VersionParser; |
| 32 | use Composer\Util\HttpDownloader; |
| 33 | use phpbb\composer\io\null_io; |
| 34 | use phpbb\config\config; |
| 35 | use phpbb\exception\runtime_exception; |
| 36 | use phpbb\filesystem\filesystem; |
| 37 | use phpbb\request\request; |
| 38 | use Seld\JsonLint\ParsingException; |
| 39 | use phpbb\filesystem\helper as filesystem_helper; |
| 40 | |
| 41 | /** |
| 42 | * Class to install packages through composer while freezing core dependencies. |
| 43 | */ |
| 44 | class installer |
| 45 | { |
| 46 | const PHPBB_TYPES = 'phpbb-extension,phpbb-style,phpbb-language'; |
| 47 | |
| 48 | /** |
| 49 | * @var array Repositories to look packages from |
| 50 | */ |
| 51 | protected $repositories = []; |
| 52 | |
| 53 | /** |
| 54 | * @var bool Indicates whether packagist usage is allowed or not |
| 55 | */ |
| 56 | protected $packagist = false; |
| 57 | |
| 58 | /** |
| 59 | * @var string Composer filename used to manage the packages |
| 60 | */ |
| 61 | protected $composer_filename = 'composer-ext.json'; |
| 62 | |
| 63 | /** |
| 64 | * @var string Directory where to install packages vendors |
| 65 | */ |
| 66 | protected $packages_vendor_dir = 'vendor-ext/'; |
| 67 | |
| 68 | /** |
| 69 | * @var string Minimum stability |
| 70 | */ |
| 71 | protected $minimum_stability = 'stable'; |
| 72 | |
| 73 | /** |
| 74 | * @var string phpBB root path |
| 75 | */ |
| 76 | protected $root_path; |
| 77 | |
| 78 | /** |
| 79 | * @var string|null Stores the original working directory in case it has been changed through move_to_root() |
| 80 | */ |
| 81 | private $original_cwd; |
| 82 | |
| 83 | /** |
| 84 | * @var array|null Stores the content of the ext json file before generate_ext_json_file() overrides it |
| 85 | */ |
| 86 | private $ext_json_file_backup; |
| 87 | |
| 88 | /** |
| 89 | * @var request phpBB request object |
| 90 | */ |
| 91 | private $request; |
| 92 | |
| 93 | /** |
| 94 | * @var filesystem phpBB filesystem |
| 95 | */ |
| 96 | private $filesystem; |
| 97 | |
| 98 | /** |
| 99 | * @param string $root_path phpBB root path |
| 100 | * @param filesystem $filesystem Filesystem object |
| 101 | * @param request $request phpBB request object |
| 102 | * @param config|null $config Config object |
| 103 | */ |
| 104 | public function __construct($root_path, filesystem $filesystem, request $request, config|null $config = null) |
| 105 | { |
| 106 | if ($config) |
| 107 | { |
| 108 | $repositories = json_decode($config['exts_composer_repositories'], true); |
| 109 | |
| 110 | if (is_array($repositories) && !empty($repositories)) |
| 111 | { |
| 112 | $this->repositories = (array) $repositories; |
| 113 | } |
| 114 | |
| 115 | $this->packagist = (bool) $config['exts_composer_packagist']; |
| 116 | $this->composer_filename = $config['exts_composer_json_file']; |
| 117 | $this->packages_vendor_dir = $config['exts_composer_vendor_dir']; |
| 118 | $this->minimum_stability = $config['exts_composer_minimum_stability']; |
| 119 | } |
| 120 | |
| 121 | $this->root_path = $root_path; |
| 122 | $this->request = $request; |
| 123 | $this->filesystem = $filesystem; |
| 124 | |
| 125 | putenv('COMPOSER_HOME=' . filesystem_helper::realpath($root_path) . '/store/composer'); |
| 126 | } |
| 127 | |
| 128 | /** |
| 129 | * Update the current installed set of packages |
| 130 | * |
| 131 | * @param array $packages Packages to install. |
| 132 | * Each entry may be a name or an array associating a version constraint to a name |
| 133 | * @param array $whitelist White-listed packages (packages that can be installed/updated/removed) |
| 134 | * @param IOInterface|null $io IO object used for the output |
| 135 | * |
| 136 | * @throws runtime_exception |
| 137 | */ |
| 138 | public function install(array $packages, $whitelist, IOInterface|null $io = null) |
| 139 | { |
| 140 | $this->wrap(function() use ($packages, $whitelist, $io) { |
| 141 | $this->do_install($packages, $whitelist, $io); |
| 142 | }); |
| 143 | } |
| 144 | |
| 145 | /** |
| 146 | * Update the current installed set of packages |
| 147 | * |
| 148 | * /!\ Doesn't change the current working directory |
| 149 | * |
| 150 | * @param array $packages Packages to install. |
| 151 | * Each entry may be a name or an array associating a version constraint to a name |
| 152 | * @param array $whitelist White-listed packages (packages that can be installed/updated/removed) |
| 153 | * @param io\io_interface|null $io IO object used for the output |
| 154 | * |
| 155 | * @throws runtime_exception |
| 156 | * @throws JsonValidationException |
| 157 | */ |
| 158 | protected function do_install(array $packages, $whitelist, io\io_interface|null $io = null) |
| 159 | { |
| 160 | if (!$io) |
| 161 | { |
| 162 | $this->restore_cwd(); |
| 163 | $io = new null_io(); |
| 164 | $this->move_to_root(); |
| 165 | } |
| 166 | |
| 167 | $this->generate_ext_json_file($packages); |
| 168 | |
| 169 | $composer = $this->get_composer($this->get_composer_ext_json_filename()); |
| 170 | |
| 171 | $install = \Composer\Installer::create($io, $composer); |
| 172 | |
| 173 | $composer->getInstallationManager()->setOutputProgress(false); |
| 174 | |
| 175 | $install |
| 176 | ->setVerbose(true) |
| 177 | ->setPreferSource(false) |
| 178 | ->setPreferDist(true) |
| 179 | ->setDevMode(false) |
| 180 | ->setUpdate(true) |
| 181 | ->setUpdateAllowList($whitelist) |
| 182 | ->setUpdateAllowTransitiveDependencies(composer_request::UPDATE_ONLY_LISTED) |
| 183 | ->setPlatformRequirementFilter(PlatformRequirementFilterFactory::fromBoolOrList(false)) |
| 184 | ->setOptimizeAutoloader(true) |
| 185 | ->setDumpAutoloader(true) |
| 186 | ->setPreferStable(true) |
| 187 | ->setRunScripts(false) |
| 188 | ->setDryRun(false); |
| 189 | |
| 190 | try |
| 191 | { |
| 192 | $result = $install->run(); |
| 193 | } |
| 194 | catch (\Exception $e) |
| 195 | { |
| 196 | $this->restore_ext_json_file(); |
| 197 | $this->restore_cwd(); |
| 198 | |
| 199 | throw new runtime_exception('COMPOSER_CANNOT_INSTALL', [], $e); |
| 200 | } |
| 201 | |
| 202 | if ($result !== 0) |
| 203 | { |
| 204 | $this->restore_ext_json_file(); |
| 205 | $this->restore_cwd(); |
| 206 | |
| 207 | throw new runtime_exception($io->get_composer_error(), []); |
| 208 | } |
| 209 | } |
| 210 | |
| 211 | /** |
| 212 | * Returns the list of currently installed packages |
| 213 | * |
| 214 | * @param string|array $types Returns only the packages with the given type(s) |
| 215 | * |
| 216 | * @return array The installed packages associated to their version. |
| 217 | * |
| 218 | * @throws runtime_exception |
| 219 | */ |
| 220 | public function get_installed_packages($types) |
| 221 | { |
| 222 | return $this->wrap(function() use ($types) { |
| 223 | return $this->do_get_installed_packages($types); |
| 224 | }); |
| 225 | } |
| 226 | |
| 227 | /** |
| 228 | * Create instance of composer for supplied config file |
| 229 | * |
| 230 | * @param string|null $config_file Path to config file relative to phpBB root dir or null |
| 231 | * |
| 232 | * @return Composer|PartialComposer |
| 233 | * @throws JsonValidationException |
| 234 | */ |
| 235 | protected function get_composer(string|null $config_file): PartialComposer |
| 236 | { |
| 237 | static $composer_factory; |
| 238 | if (!$composer_factory) |
| 239 | { |
| 240 | $composer_factory = new Factory(); |
| 241 | } |
| 242 | |
| 243 | $io = new NullIO(); |
| 244 | |
| 245 | return $composer_factory->createComposer( |
| 246 | $io, |
| 247 | $config_file, |
| 248 | false, |
| 249 | filesystem_helper::realpath('') |
| 250 | ); |
| 251 | } |
| 252 | |
| 253 | /** |
| 254 | * Returns the list of currently installed packages |
| 255 | * |
| 256 | * /!\ Doesn't change the current working directory |
| 257 | * |
| 258 | * @param string|array $types Returns only the packages with the given type(s) |
| 259 | * |
| 260 | * @return array The installed packages associated to their version. |
| 261 | */ |
| 262 | protected function do_get_installed_packages($types) |
| 263 | { |
| 264 | $types = (array) $types; |
| 265 | |
| 266 | try |
| 267 | { |
| 268 | $composer = $this->get_composer($this->get_composer_ext_json_filename()); |
| 269 | |
| 270 | $installed = []; |
| 271 | |
| 272 | /** @var \Composer\Package\Link[] $required_links */ |
| 273 | $required_links = $composer->getPackage()->getRequires(); |
| 274 | $installed_packages = $composer->getRepositoryManager()->getLocalRepository()->getCanonicalPackages(); |
| 275 | |
| 276 | foreach ($installed_packages as $package) |
| 277 | { |
| 278 | if (in_array($package->getType(), $types, true)) |
| 279 | { |
| 280 | $version = array_key_exists($package->getName(), $required_links) ? |
| 281 | $required_links[$package->getName()]->getPrettyConstraint() : '*'; |
| 282 | $installed[$package->getName()] = $version; |
| 283 | } |
| 284 | } |
| 285 | |
| 286 | return $installed; |
| 287 | } |
| 288 | catch (\Exception $e) |
| 289 | { |
| 290 | return []; |
| 291 | } |
| 292 | } |
| 293 | |
| 294 | /** |
| 295 | * Gets the list of the available packages of the configured type in the configured repositories |
| 296 | * |
| 297 | * /!\ Doesn't change the current working directory |
| 298 | * |
| 299 | * @param string $type Returns only the packages with the given type |
| 300 | * |
| 301 | * @return array The name of the available packages, associated to their definition. Ordered by name. |
| 302 | * |
| 303 | * @throws runtime_exception |
| 304 | */ |
| 305 | public function get_available_packages($type) |
| 306 | { |
| 307 | return $this->wrap(function() use ($type) { |
| 308 | return $this->do_get_available_packages($type); |
| 309 | }); |
| 310 | } |
| 311 | |
| 312 | /** |
| 313 | * Gets the list of the available packages of the configured type in the configured repositories |
| 314 | * |
| 315 | * @param string $type Returns only the packages with the given type |
| 316 | * |
| 317 | * @return array The name of the available packages, associated to their definition. Ordered by name. |
| 318 | */ |
| 319 | protected function do_get_available_packages($type) |
| 320 | { |
| 321 | try |
| 322 | { |
| 323 | $this->generate_ext_json_file($this->do_get_installed_packages(explode(',', self::PHPBB_TYPES))); |
| 324 | |
| 325 | $io = new NullIO(); |
| 326 | $composer = $this->get_composer($this->get_composer_ext_json_filename()); |
| 327 | |
| 328 | /** @var ConstraintInterface $core_constraint */ |
| 329 | $core_constraint = $composer->getPackage()->getRequires()['phpbb/phpbb']->getConstraint(); |
| 330 | $core_stability = $composer->getPackage()->getMinimumStability(); |
| 331 | |
| 332 | $available = []; |
| 333 | |
| 334 | $compatible_packages = []; |
| 335 | $repositories = $composer->getRepositoryManager()->getRepositories(); |
| 336 | |
| 337 | /** @var \Composer\Repository\RepositoryInterface $repository */ |
| 338 | foreach ($repositories as $repository) |
| 339 | { |
| 340 | try |
| 341 | { |
| 342 | if ($repository instanceof ComposerRepository) |
| 343 | { |
| 344 | // Special case for packagist which exposes an api to retrieve all packages of a given type. |
| 345 | // For the others composer repositories with providers we can't do anything. It would be too slow. |
| 346 | |
| 347 | $repositoryReflection = new \ReflectionObject($repository); |
| 348 | $repo_url = $repositoryReflection->getProperty('url'); |
| 349 | $repo_url->setAccessible(true); |
| 350 | |
| 351 | if ($repo_url->getValue($repository) === 'https://repo.packagist.org') |
| 352 | { |
| 353 | $url = 'https://packagist.org/packages/list.json?type=' . $type; |
| 354 | $composer_config = new \Composer\Config(); |
| 355 | $downloader = new HttpDownloader($io, $composer_config); |
| 356 | $json = $downloader->get($url)->getBody(); |
| 357 | |
| 358 | /** @var PackageInterface $package */ |
| 359 | foreach (JsonFile::parseJson($json, $url)['packageNames'] as $package) |
| 360 | { |
| 361 | $versions = $repository->findPackages($package); |
| 362 | $compatible_packages = $this->get_compatible_versions($compatible_packages, $core_constraint, $core_stability, $package, $versions); |
| 363 | } |
| 364 | } |
| 365 | } |
| 366 | else |
| 367 | { |
| 368 | // Pre-filter repo packages by their type |
| 369 | $packages = []; |
| 370 | /** @var PackageInterface $package */ |
| 371 | foreach ($repository->getPackages() as $package) |
| 372 | { |
| 373 | if ($package->getType() === $type) |
| 374 | { |
| 375 | $packages[$package->getName()][] = $package; |
| 376 | } |
| 377 | } |
| 378 | |
| 379 | // Filter the compatibles versions |
| 380 | foreach ($packages as $package => $versions) |
| 381 | { |
| 382 | $compatible_packages = $this->get_compatible_versions($compatible_packages, $core_constraint, $core_stability, $package, $versions); |
| 383 | } |
| 384 | } |
| 385 | } |
| 386 | catch (\Exception $e) |
| 387 | { |
| 388 | // If a repo fails, just skip it. |
| 389 | continue; |
| 390 | } |
| 391 | } |
| 392 | |
| 393 | foreach ($compatible_packages as $package_name => $package_versions) |
| 394 | { |
| 395 | // Determine the highest version of the package |
| 396 | /** @var CompletePackage|CompleteAliasPackage $highest_version */ |
| 397 | $highest_version = null; |
| 398 | |
| 399 | // Sort the versions array in descending order |
| 400 | usort($package_versions, function ($a, $b) |
| 401 | { |
| 402 | return version_compare($b->getVersion(), $a->getVersion()); |
| 403 | }); |
| 404 | |
| 405 | // The first element in the sorted array is the highest version |
| 406 | if (!empty($package_versions)) |
| 407 | { |
| 408 | $highest_version = $package_versions[0]; |
| 409 | |
| 410 | // If highest version is a non-numeric dev branch, it's an instance of CompleteAliasPackage, |
| 411 | // so we need to get the package being aliased in order to show the true non-numeric version. |
| 412 | if ($highest_version instanceof CompleteAliasPackage) |
| 413 | { |
| 414 | $highest_version = $highest_version->getAliasOf(); |
| 415 | } |
| 416 | } |
| 417 | |
| 418 | // Generates the entry |
| 419 | $available[$package_name] = []; |
| 420 | $available[$package_name]['name'] = $highest_version->getPrettyName(); |
| 421 | $available[$package_name]['display_name'] = $highest_version->getExtra()['display-name']; |
| 422 | $available[$package_name]['composer_name'] = $highest_version->getName(); |
| 423 | $available[$package_name]['version'] = $highest_version->getPrettyVersion(); |
| 424 | |
| 425 | if ($highest_version instanceof CompletePackage) |
| 426 | { |
| 427 | $available[$package_name]['description'] = $highest_version->getDescription(); |
| 428 | $available[$package_name]['url'] = $highest_version->getHomepage(); |
| 429 | $available[$package_name]['authors'] = $highest_version->getAuthors(); |
| 430 | } |
| 431 | else |
| 432 | { |
| 433 | $available[$package_name]['description'] = ''; |
| 434 | $available[$package_name]['url'] = ''; |
| 435 | $available[$package_name]['authors'] = []; |
| 436 | } |
| 437 | } |
| 438 | |
| 439 | usort($available, function($a, $b) |
| 440 | { |
| 441 | return strcasecmp($a['display_name'], $b['display_name']); |
| 442 | }); |
| 443 | |
| 444 | return $available; |
| 445 | } |
| 446 | catch (\Exception $e) |
| 447 | { |
| 448 | return []; |
| 449 | } |
| 450 | } |
| 451 | |
| 452 | /** |
| 453 | * Checks the requirements of the manager and returns true if it can be used. |
| 454 | * |
| 455 | * @return bool |
| 456 | */ |
| 457 | public function check_requirements() |
| 458 | { |
| 459 | return $this->filesystem->is_writable([ |
| 460 | $this->root_path . $this->composer_filename, |
| 461 | $this->root_path . $this->packages_vendor_dir, |
| 462 | $this->root_path . substr($this->composer_filename, 0, -5) . '.lock', |
| 463 | ]); |
| 464 | } |
| 465 | |
| 466 | /** |
| 467 | * Updates $compatible_packages with the versions of $versions compatibles with the $core_constraint |
| 468 | * |
| 469 | * @param array $compatible_packages List of compatibles versions |
| 470 | * @param ConstraintInterface $core_constraint Constraint against the phpBB version |
| 471 | * @param string $core_stability Core stability |
| 472 | * @param string $package_name Considered package |
| 473 | * @param array $versions List of available versions |
| 474 | * |
| 475 | * @return array |
| 476 | */ |
| 477 | private function get_compatible_versions(array $compatible_packages, ConstraintInterface $core_constraint, $core_stability, $package_name, array $versions) |
| 478 | { |
| 479 | $version_parser = new VersionParser(); |
| 480 | |
| 481 | $core_stability_value = BasePackage::$stabilities[$core_stability]; |
| 482 | |
| 483 | /** @var PackageInterface $version */ |
| 484 | foreach ($versions as $version) |
| 485 | { |
| 486 | try |
| 487 | { |
| 488 | // Check stability first to avoid unnecessary operations |
| 489 | if (BasePackage::$stabilities[$version->getStability()] > $core_stability_value) |
| 490 | { |
| 491 | continue; |
| 492 | } |
| 493 | |
| 494 | $requires = $version->getRequires(); |
| 495 | $extra = $version->getExtra(); |
| 496 | |
| 497 | // Check for compatibility with phpBB if 'phpbb/phpbb' exists in 'requires' |
| 498 | if (isset($requires['phpbb/phpbb'])) |
| 499 | { |
| 500 | $package_constraint = $requires['phpbb/phpbb']->getConstraint(); |
| 501 | if (!$package_constraint->matches($core_constraint)) |
| 502 | { |
| 503 | continue; |
| 504 | } |
| 505 | } |
| 506 | |
| 507 | // Check for compatibility with phpBB if 'phpbb/phpbb' exists in 'soft-require' |
| 508 | if (isset($extra['soft-require']['phpbb/phpbb'])) |
| 509 | { |
| 510 | $package_constraint = $version_parser->parseConstraints($extra['soft-require']['phpbb/phpbb']); |
| 511 | if (!$package_constraint->matches($core_constraint)) |
| 512 | { |
| 513 | continue; |
| 514 | } |
| 515 | } |
| 516 | |
| 517 | // Check for compatibility with php if 'php' exists in 'requires' |
| 518 | if (isset($requires['php'])) |
| 519 | { |
| 520 | $php_constraint = $version_parser->parseConstraints(PHP_VERSION); |
| 521 | $package_constraint = $requires['php']->getConstraint(); |
| 522 | if (!$package_constraint->matches($php_constraint)) |
| 523 | { |
| 524 | continue; |
| 525 | } |
| 526 | } |
| 527 | |
| 528 | // Check for composer/installers requirement - must support version 2.0 or later |
| 529 | if (isset($requires['composer/installers'])) |
| 530 | { |
| 531 | $installers_constraint = $requires['composer/installers']->getConstraint(); |
| 532 | $min_version_constraint = $version_parser->parseConstraints('>=2.0'); |
| 533 | if (!$min_version_constraint->matches($installers_constraint)) |
| 534 | { |
| 535 | continue; |
| 536 | } |
| 537 | } |
| 538 | else |
| 539 | { |
| 540 | continue; |
| 541 | } |
| 542 | |
| 543 | $compatible_packages[$package_name][] = $version; |
| 544 | } |
| 545 | catch (\Exception $e) |
| 546 | { |
| 547 | // Do nothing (to log when a true debug logger is available) |
| 548 | } |
| 549 | } |
| 550 | |
| 551 | return $compatible_packages; |
| 552 | } |
| 553 | |
| 554 | /** |
| 555 | * Generates and write the json file used to install the set of packages |
| 556 | * |
| 557 | * @param array $packages Packages to update. |
| 558 | * Each entry may be a name or an array associating a version constraint to a name |
| 559 | * @throws JsonValidationException |
| 560 | */ |
| 561 | protected function generate_ext_json_file(array $packages) |
| 562 | { |
| 563 | $composer = $this->get_composer(null); |
| 564 | |
| 565 | $core_packages = $this->get_core_packages($composer); |
| 566 | |
| 567 | // The composer/installers package must be installed on his own and not provided by the existing autoloader |
| 568 | $core_replace = $core_packages; |
| 569 | unset($core_replace['composer/installers']); |
| 570 | |
| 571 | $ext_json_data = [ |
| 572 | 'require' => array_merge( |
| 573 | ['php' => $this->get_core_php_requirement($composer)], |
| 574 | $core_packages, |
| 575 | $this->get_extra_dependencies(), |
| 576 | $packages), |
| 577 | 'replace' => $core_replace, |
| 578 | 'repositories' => $this->get_composer_repositories(), |
| 579 | 'config' => [ |
| 580 | 'vendor-dir' => $this->packages_vendor_dir, |
| 581 | 'allow-plugins' => [ |
| 582 | 'composer/installers' => true, |
| 583 | ] |
| 584 | ], |
| 585 | 'minimum-stability' => $this->minimum_stability, |
| 586 | ]; |
| 587 | |
| 588 | $this->ext_json_file_backup = null; |
| 589 | $json_file = new JsonFile($this->get_composer_ext_json_filename()); |
| 590 | |
| 591 | try |
| 592 | { |
| 593 | $ext_json_file_backup = $json_file->read(); |
| 594 | } |
| 595 | catch (ParsingException $e) |
| 596 | { |
| 597 | $ext_json_file_backup = '{}'; |
| 598 | |
| 599 | $lockFile = new JsonFile(substr($this->get_composer_ext_json_filename(), 0, -5) . '.lock'); |
| 600 | $lockFile->write([]); |
| 601 | } |
| 602 | |
| 603 | // First pass write: base file with requested packages as provided |
| 604 | $json_file->write($ext_json_data); |
| 605 | $this->ext_json_file_backup = $ext_json_file_backup; |
| 606 | |
| 607 | // Second pass: resolve and pin the highest compatible versions for unconstrained requested packages |
| 608 | try |
| 609 | { |
| 610 | // Build a list of requested packages without explicit constraints |
| 611 | $unconstrained = []; |
| 612 | foreach ($packages as $name => $constraint) |
| 613 | { |
| 614 | // The $packages array can be either ['vendor/package' => '^1.2'] or ['vendor/package'] (numeric keys). |
| 615 | if (is_int($name)) |
| 616 | { |
| 617 | // Numeric key means just a name |
| 618 | $package_name = $constraint; |
| 619 | $unconstrained[$package_name] = true; |
| 620 | } |
| 621 | else |
| 622 | { |
| 623 | // If constraint is empty or '*' treat as unconstrained |
| 624 | if ($constraint === '' || $constraint === '*' || $constraint === null) |
| 625 | { |
| 626 | $unconstrained[$name] = true; |
| 627 | } |
| 628 | } |
| 629 | } |
| 630 | |
| 631 | if (!empty($unconstrained)) |
| 632 | { |
| 633 | // Load composer on the just-written file so repositories and core constraints are available |
| 634 | $ext_composer = $this->get_composer($this->get_composer_ext_json_filename()); |
| 635 | |
| 636 | /** @var ConstraintInterface $core_constraint */ |
| 637 | $core_constraint = $ext_composer->getPackage()->getRequires()['phpbb/phpbb']->getConstraint(); |
| 638 | $core_stability = $ext_composer->getPackage()->getMinimumStability(); |
| 639 | |
| 640 | // Resolve highest compatible versions for each unconstrained package |
| 641 | $pins = $this->resolve_highest_versions(array_keys($unconstrained), $ext_composer, $core_constraint, $core_stability); |
| 642 | |
| 643 | if (!empty($pins)) |
| 644 | { |
| 645 | // Merge pins into require section, overwriting unconstrained entries |
| 646 | foreach ($pins as $pkg => $version) |
| 647 | { |
| 648 | $ext_json_data['require'][$pkg] = $version; |
| 649 | } |
| 650 | |
| 651 | // Rewrite composer-ext.json with pinned versions |
| 652 | $json_file->write($ext_json_data); |
| 653 | } |
| 654 | } |
| 655 | } |
| 656 | catch (\Exception $e) |
| 657 | { |
| 658 | // If resolution fails for any reason, keep the first-pass file intact (Composer will still resolve). |
| 659 | // Intentionally swallow to avoid breaking installation flow. |
| 660 | } |
| 661 | } |
| 662 | |
| 663 | /** |
| 664 | * Resolve the highest compatible versions for the given package names |
| 665 | * based on repositories and phpBB/PHP constraints from the provided Composer instance. |
| 666 | * |
| 667 | * @param array $package_names list of package names to resolve |
| 668 | * @param Composer|PartialComposer $composer Composer instance configured with repositories |
| 669 | * @param ConstraintInterface $core_constraint phpBB version constraint |
| 670 | * @param string $core_stability minimum stability |
| 671 | * @return array [packageName => prettyVersion] |
| 672 | */ |
| 673 | protected function resolve_highest_versions(array $package_names, $composer, ConstraintInterface $core_constraint, $core_stability): array |
| 674 | { |
| 675 | $compatible_packages = []; |
| 676 | $repositories = $composer->getRepositoryManager()->getRepositories(); |
| 677 | |
| 678 | foreach ($repositories as $repository) |
| 679 | { |
| 680 | try |
| 681 | { |
| 682 | if ($repository instanceof ComposerRepository) |
| 683 | { |
| 684 | foreach ($package_names as $name) |
| 685 | { |
| 686 | $versions = $repository->findPackages($name); |
| 687 | if (!empty($versions)) |
| 688 | { |
| 689 | $compatible_packages = $this->get_compatible_versions($compatible_packages, $core_constraint, $core_stability, $name, $versions); |
| 690 | } |
| 691 | } |
| 692 | } |
| 693 | else |
| 694 | { |
| 695 | // Preload and filter by name for non-composer repositories |
| 696 | $package_name = []; |
| 697 | foreach ($repository->getPackages() as $package) |
| 698 | { |
| 699 | $name = $package->getName(); |
| 700 | if (in_array($name, $package_names, true)) |
| 701 | { |
| 702 | $package_name[$name][] = $package; |
| 703 | } |
| 704 | } |
| 705 | |
| 706 | foreach ($package_name as $name => $versions) |
| 707 | { |
| 708 | $compatible_packages = $this->get_compatible_versions($compatible_packages, $core_constraint, $core_stability, $name, $versions); |
| 709 | } |
| 710 | } |
| 711 | } |
| 712 | catch (\Exception $e) |
| 713 | { |
| 714 | // If a repo fails, just skip it. |
| 715 | continue; |
| 716 | } |
| 717 | } |
| 718 | |
| 719 | $pins = []; |
| 720 | foreach ($package_names as $name) |
| 721 | { |
| 722 | if (empty($compatible_packages[$name])) |
| 723 | { |
| 724 | continue; |
| 725 | } |
| 726 | |
| 727 | $package_versions = $compatible_packages[$name]; |
| 728 | |
| 729 | // Sort descending by normalized version |
| 730 | usort($package_versions, function ($a, $b) { |
| 731 | return version_compare($b->getVersion(), $a->getVersion()); |
| 732 | }); |
| 733 | |
| 734 | $highest = $package_versions[0]; |
| 735 | if ($highest instanceof CompleteAliasPackage) |
| 736 | { |
| 737 | $highest = $highest->getAliasOf(); |
| 738 | } |
| 739 | |
| 740 | // Pin to the resolved highest compatible version using its pretty version |
| 741 | $pins[$name] = $highest->getPrettyVersion(); |
| 742 | } |
| 743 | |
| 744 | return $pins; |
| 745 | } |
| 746 | |
| 747 | /** |
| 748 | * Restore the json file overridden by generate_ext_json_file() |
| 749 | */ |
| 750 | protected function restore_ext_json_file() |
| 751 | { |
| 752 | if ($this->ext_json_file_backup) |
| 753 | { |
| 754 | try |
| 755 | { |
| 756 | $json_file = new JsonFile($this->get_composer_ext_json_filename()); |
| 757 | $json_file->write($this->ext_json_file_backup); |
| 758 | } |
| 759 | catch (\Exception $e) |
| 760 | { |
| 761 | } |
| 762 | |
| 763 | $this->ext_json_file_backup = null; |
| 764 | } |
| 765 | } |
| 766 | |
| 767 | /** |
| 768 | * Get the core installed packages |
| 769 | * |
| 770 | * @param Composer $composer Composer object to load the dependencies |
| 771 | * @return array The core packages with their version |
| 772 | */ |
| 773 | protected function get_core_packages(Composer $composer) |
| 774 | { |
| 775 | $core_deps = []; |
| 776 | $packages = $composer->getRepositoryManager()->getLocalRepository()->getCanonicalPackages(); |
| 777 | |
| 778 | foreach ($packages as $package) |
| 779 | { |
| 780 | $core_deps[$package->getName()] = $package->getPrettyVersion(); |
| 781 | } |
| 782 | |
| 783 | $core_deps['phpbb/phpbb'] = PHPBB_VERSION; |
| 784 | |
| 785 | return $core_deps; |
| 786 | } |
| 787 | |
| 788 | /** |
| 789 | * Get the PHP version required by the core |
| 790 | * |
| 791 | * @param Composer $composer Composer object to load the dependencies |
| 792 | * @return string The PHP version required by the core |
| 793 | */ |
| 794 | protected function get_core_php_requirement(Composer $composer) |
| 795 | { |
| 796 | return $composer->getLocker()->getLockData()['platform']['php']; |
| 797 | } |
| 798 | |
| 799 | /** |
| 800 | * Generate the repositories entry of the packages json file |
| 801 | * |
| 802 | * @return array repositories entry |
| 803 | */ |
| 804 | protected function get_composer_repositories() |
| 805 | { |
| 806 | $repositories = []; |
| 807 | |
| 808 | if (!$this->packagist) |
| 809 | { |
| 810 | $repositories[]['packagist'] = false; |
| 811 | } |
| 812 | |
| 813 | foreach ($this->repositories as $repository) |
| 814 | { |
| 815 | if (preg_match('#^' . get_preg_expression('url') . '$#iu', $repository)) |
| 816 | { |
| 817 | $repositories[] = [ |
| 818 | 'type' => 'composer', |
| 819 | 'url' => $repository, |
| 820 | 'canonical' => $this->packagist ? false : true, |
| 821 | ]; |
| 822 | } |
| 823 | } |
| 824 | |
| 825 | return $repositories; |
| 826 | } |
| 827 | |
| 828 | /** |
| 829 | * Get the name of the json file used for the packages. |
| 830 | * |
| 831 | * @return string The json filename |
| 832 | */ |
| 833 | protected function get_composer_ext_json_filename() |
| 834 | { |
| 835 | return $this->composer_filename; |
| 836 | } |
| 837 | |
| 838 | /** |
| 839 | * Get extra dependencies required to install the packages |
| 840 | * |
| 841 | * @return array Array of composer dependencies |
| 842 | */ |
| 843 | protected function get_extra_dependencies() |
| 844 | { |
| 845 | return []; |
| 846 | } |
| 847 | |
| 848 | /** |
| 849 | * Sets the customs repositories |
| 850 | * |
| 851 | * @param array $repositories An array of composer repositories to use |
| 852 | */ |
| 853 | public function set_repositories(array $repositories) |
| 854 | { |
| 855 | $this->repositories = $repositories; |
| 856 | } |
| 857 | |
| 858 | /** |
| 859 | * Allow or disallow packagist |
| 860 | * |
| 861 | * @param boolean $packagist |
| 862 | */ |
| 863 | public function set_packagist($packagist) |
| 864 | { |
| 865 | $this->packagist = $packagist; |
| 866 | } |
| 867 | |
| 868 | /** |
| 869 | * Sets the name of the managed packages' json file |
| 870 | * |
| 871 | * @param string $composer_filename |
| 872 | */ |
| 873 | public function set_composer_filename($composer_filename) |
| 874 | { |
| 875 | $this->composer_filename = $composer_filename; |
| 876 | } |
| 877 | |
| 878 | /** |
| 879 | * Sets the location of the managed packages' vendors |
| 880 | * |
| 881 | * @param string $packages_vendor_dir |
| 882 | */ |
| 883 | public function set_packages_vendor_dir($packages_vendor_dir) |
| 884 | { |
| 885 | $this->packages_vendor_dir = $packages_vendor_dir; |
| 886 | } |
| 887 | |
| 888 | /** |
| 889 | * Sets the phpBB root path |
| 890 | * |
| 891 | * @param string $root_path |
| 892 | */ |
| 893 | public function set_root_path($root_path) |
| 894 | { |
| 895 | $this->root_path = $root_path; |
| 896 | } |
| 897 | |
| 898 | /** |
| 899 | * Change the current directory to phpBB root |
| 900 | */ |
| 901 | protected function move_to_root() |
| 902 | { |
| 903 | if ($this->original_cwd === null) |
| 904 | { |
| 905 | $this->original_cwd = getcwd(); |
| 906 | chdir($this->root_path); |
| 907 | } |
| 908 | } |
| 909 | |
| 910 | /** |
| 911 | * Restore the current working directory if move_to_root() have been called |
| 912 | */ |
| 913 | protected function restore_cwd() |
| 914 | { |
| 915 | if ($this->original_cwd) |
| 916 | { |
| 917 | chdir($this->original_cwd); |
| 918 | $this->original_cwd = null; |
| 919 | } |
| 920 | } |
| 921 | |
| 922 | /** |
| 923 | * Wraps a callable in order to adjust the context needed by composer |
| 924 | * |
| 925 | * @param callable $callable |
| 926 | * |
| 927 | * @return mixed |
| 928 | */ |
| 929 | protected function wrap(callable $callable) |
| 930 | { |
| 931 | // The composer installers works with a path relative to the current directory |
| 932 | $this->move_to_root(); |
| 933 | |
| 934 | // The composer installers uses some super globals |
| 935 | $super_globals = $this->request->super_globals_disabled(); |
| 936 | $this->request->enable_super_globals(); |
| 937 | |
| 938 | try |
| 939 | { |
| 940 | return $callable(); |
| 941 | } |
| 942 | finally |
| 943 | { |
| 944 | $this->restore_cwd(); |
| 945 | |
| 946 | if ($super_globals) |
| 947 | { |
| 948 | $this->request->disable_super_globals(); |
| 949 | } |
| 950 | } |
| 951 | } |
| 952 | } |