Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 286
0.00% covered (danger)
0.00%
0 / 26
CRAP
0.00% covered (danger)
0.00%
0 / 1
installer
0.00% covered (danger)
0.00%
0 / 286
0.00% covered (danger)
0.00%
0 / 26
8556
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 install
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 do_install
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
20
 get_installed_packages
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 get_composer
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 do_get_installed_packages
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
30
 get_available_packages
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 do_get_available_packages
0.00% covered (danger)
0.00%
0 / 58
0.00% covered (danger)
0.00%
0 / 1
210
 check_requirements
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 get_compatible_versions
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
156
 generate_ext_json_file
0.00% covered (danger)
0.00%
0 / 46
0.00% covered (danger)
0.00%
0 / 1
132
 resolve_highest_versions
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
156
 restore_ext_json_file
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 get_core_packages
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 get_core_php_requirement
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_composer_repositories
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 get_composer_ext_json_filename
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_extra_dependencies
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 set_repositories
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 set_packagist
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 set_composer_filename
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 set_packages_vendor_dir
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 set_root_path
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 move_to_root
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 restore_cwd
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 wrap
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
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
14namespace phpbb\composer;
15
16use Composer\Composer;
17use Composer\DependencyResolver\Request as composer_request;
18use Composer\Factory;
19use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory;
20use Composer\IO\IOInterface;
21use Composer\IO\NullIO;
22use Composer\Json\JsonFile;
23use Composer\Json\JsonValidationException;
24use Composer\Package\BasePackage;
25use Composer\Package\CompleteAliasPackage;
26use Composer\Package\CompletePackage;
27use Composer\Package\PackageInterface;
28use Composer\PartialComposer;
29use Composer\Repository\ComposerRepository;
30use Composer\Semver\Constraint\ConstraintInterface;
31use Composer\Semver\VersionParser;
32use Composer\Util\HttpDownloader;
33use phpbb\composer\io\null_io;
34use phpbb\config\config;
35use phpbb\exception\runtime_exception;
36use phpbb\filesystem\filesystem;
37use phpbb\request\request;
38use Seld\JsonLint\ParsingException;
39use phpbb\filesystem\helper as filesystem_helper;
40
41/**
42 * Class to install packages through composer while freezing core dependencies.
43 */
44class 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}