diff --git a/phpBB/phpbb/composer/installer.php b/phpBB/phpbb/composer/installer.php index d848c8b9af..993519dd5e 100644 --- a/phpBB/phpbb/composer/installer.php +++ b/phpBB/phpbb/composer/installer.php @@ -587,8 +587,149 @@ class installer $lockFile->write([]); } + // First pass write: base file with requested packages as provided $json_file->write($ext_json_data); $this->ext_json_file_backup = $ext_json_file_backup; + + // Second pass: resolve and pin the highest compatible versions for unconstrained requested packages + try + { + // Build a list of requested packages without explicit constraints + $unconstrained = []; + foreach ($packages as $name => $constraint) + { + // The $packages array can be either ['vendor/package' => '^1.2'] or ['vendor/package'] (numeric keys). + if (is_int($name)) + { + // Numeric key means just a name + $pkgName = $constraint; + $unconstrained[$pkgName] = true; + } + else + { + // If constraint is empty or '*' treat as unconstrained + if ($constraint === '' || $constraint === '*' || $constraint === null) + { + $unconstrained[$name] = true; + } + } + } + + if (!empty($unconstrained)) + { + // Load composer on the just-written file so repositories and core constraints are available + $extComposer = $this->get_composer($this->get_composer_ext_json_filename()); + + /** @var ConstraintInterface $core_constraint */ + $core_constraint = $extComposer->getPackage()->getRequires()['phpbb/phpbb']->getConstraint(); + $core_stability = $extComposer->getPackage()->getMinimumStability(); + + // Resolve highest compatible versions for each unconstrained package + $pins = $this->resolve_highest_versions(array_keys($unconstrained), $extComposer, $core_constraint, $core_stability); + + if (!empty($pins)) + { + // Merge pins into require section, overwriting unconstrained entries + foreach ($pins as $pkg => $version) + { + $ext_json_data['require'][$pkg] = $version; + } + + // Rewrite composer-ext.json with pinned versions + $json_file->write($ext_json_data); + } + } + } + catch (\Exception $e) + { + // If resolution fails for any reason, keep the first-pass file intact (Composer will still resolve). + // Intentionally swallow to avoid breaking installation flow. + } + } + + /** + * Resolve the highest compatible versions for the given package names + * based on repositories and phpBB/PHP constraints from the provided Composer instance. + * + * @param array $packageNames list of package names to resolve + * @param Composer|PartialComposer $composer Composer instance configured with repositories + * @param ConstraintInterface $core_constraint phpBB version constraint + * @param string $core_stability minimum stability + * @return array [packageName => prettyVersion] + */ + private function resolve_highest_versions(array $packageNames, $composer, ConstraintInterface $core_constraint, $core_stability): array + { + $io = new NullIO(); + + $compatible_packages = []; + $repositories = $composer->getRepositoryManager()->getRepositories(); + + foreach ($repositories as $repository) + { + try + { + if ($repository instanceof ComposerRepository) + { + foreach ($packageNames as $name) + { + $versions = $repository->findPackages($name); + if (!empty($versions)) + { + $compatible_packages = $this->get_compatible_versions($compatible_packages, $core_constraint, $core_stability, $name, $versions); + } + } + } + else + { + // Preload and filter by name for non-composer repositories + $byName = []; + foreach ($repository->getPackages() as $pkg) + { + $n = $pkg->getName(); + if (in_array($n, $packageNames, true)) + { + $byName[$n][] = $pkg; + } + } + + foreach ($byName as $name => $versions) + { + $compatible_packages = $this->get_compatible_versions($compatible_packages, $core_constraint, $core_stability, $name, $versions); + } + } + } + catch (\Exception $e) + { + continue; + } + } + + $pins = []; + foreach ($packageNames as $name) + { + if (empty($compatible_packages[$name])) + { + continue; + } + + $package_versions = $compatible_packages[$name]; + + // Sort descending by normalized version + usort($package_versions, function ($a, $b) { + return version_compare($b->getVersion(), $a->getVersion()); + }); + + $highest = $package_versions[0]; + if ($highest instanceof CompleteAliasPackage) + { + $highest = $highest->getAliasOf(); + } + + // Pin to the resolved highest compatible version using its pretty version + $pins[$name] = $highest->getPrettyVersion(); + } + + return $pins; } /**