1
0
mirror of https://github.com/phpbb/phpbb.git synced 2025-05-08 08:35:31 +02:00
php-phpbb/phpBB/phpbb/composer/installer.php
Tristan Darricau 5f3898fc5f [ticket/15536] Catalog: adds satis.phpbb.com to repositories
Also always installs composer/installers in vendor-ext too.

PHPBB3-15536
2018-07-07 19:50:28 +02:00

717 lines
18 KiB
PHP

<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @license GNU General Public License, version 2 (GPL-2.0)
*
* For full copyright and license information, please see
* the docs/CREDITS.txt file.
*
*/
namespace phpbb\composer;
use Composer\Composer;
use Composer\Factory;
use Composer\IO\IOInterface;
use Composer\IO\NullIO;
use Composer\Json\JsonFile;
use Composer\Package\BasePackage;
use Composer\Package\CompletePackage;
use Composer\Repository\ComposerRepository;
use Composer\Semver\Constraint\ConstraintInterface;
use Composer\Util\RemoteFilesystem;
use phpbb\composer\io\null_io;
use phpbb\config\config;
use phpbb\exception\runtime_exception;
use phpbb\filesystem\filesystem;
use phpbb\request\request;
use Seld\JsonLint\ParsingException;
use phpbb\filesystem\helper as filesystem_helper;
/**
* Class to install packages through composer while freezing core dependencies.
*/
class installer
{
const PHPBB_TYPES = 'phpbb-extension,phpbb-style,phpbb-language';
/**
* @var array Repositories to look packages from
*/
protected $repositories = [];
/**
* @var bool Indicates whether packagist usage is allowed or not
*/
protected $packagist = false;
/**
* @var string Composer filename used to manage the packages
*/
protected $composer_filename = 'composer-ext.json';
/**
* @var string Directory where to install packages vendors
*/
protected $packages_vendor_dir = 'vendor-ext/';
/**
* @var string Minimum stability
*/
protected $minimum_stability = 'stable';
/**
* @var string phpBB root path
*/
protected $root_path;
/**
* @var string Stores the original working directory in case it has been changed through move_to_root()
*/
private $original_cwd;
/**
* @var array Stores the content of the ext json file before generate_ext_json_file() overrides it
*/
private $ext_json_file_backup;
/**
* @var request phpBB request object
*/
private $request;
/**
* @param string $root_path phpBB root path
* @param filesystem $filesystem Filesystem object
* @param request $request phpBB request object
* @param config $config Config object
*/
public function __construct($root_path, filesystem $filesystem, request $request, config $config = null)
{
if ($config)
{
$repositories = json_decode($config['exts_composer_repositories'], true);
if (is_array($repositories) && !empty($repositories))
{
$this->repositories = (array) $repositories;
}
$this->packagist = (bool) $config['exts_composer_packagist'];
$this->composer_filename = $config['exts_composer_json_file'];
$this->packages_vendor_dir = $config['exts_composer_vendor_dir'];
$this->minimum_stability = $config['exts_composer_minimum_stability'];
}
$this->root_path = $root_path;
$this->request = $request;
putenv('COMPOSER_HOME=' . filesystem_helper::realpath($root_path) . '/store/composer');
}
/**
* Update the current installed set of packages
*
* @param array $packages Packages to install.
* Each entry may be a name or an array associating a version constraint to a name
* @param array $whitelist White-listed packages (packages that can be installed/updated/removed)
* @param IOInterface $io IO object used for the output
*
* @throws runtime_exception
*/
public function install(array $packages, $whitelist, IOInterface $io = null)
{
$this->wrap(function() use ($packages, $whitelist, $io) {
$this->do_install($packages, $whitelist, $io);
});
}
/**
* Update the current installed set of packages
*
* /!\ Doesn't change the current working directory
*
* @param array $packages Packages to install.
* Each entry may be a name or an array associating a version constraint to a name
* @param array $whitelist White-listed packages (packages that can be installed/updated/removed)
* @param IOInterface $io IO object used for the output
*
* @throws runtime_exception
*/
protected function do_install(array $packages, $whitelist, IOInterface $io = null)
{
if (!$io)
{
$io = new null_io();
}
$this->generate_ext_json_file($packages);
$composer = Factory::create($io, $this->get_composer_ext_json_filename(), false);
$install = \Composer\Installer::create($io, $composer);
$composer->getDownloadManager()->setOutputProgress(false);
$install
->setVerbose(true)
->setPreferSource(false)
->setPreferDist(true)
->setDevMode(false)
->setUpdate(true)
->setUpdateWhitelist($whitelist)
->setWhitelistDependencies(false)
->setIgnorePlatformRequirements(false)
->setOptimizeAutoloader(true)
->setDumpAutoloader(true)
->setPreferStable(true)
->setRunScripts(false)
->setDryRun(false);
try
{
$result = $install->run();
}
catch (\Exception $e)
{
$this->restore_ext_json_file();
throw new runtime_exception('COMPOSER_CANNOT_INSTALL', [], $e);
}
if ($result !== 0)
{
$this->restore_ext_json_file();
throw new runtime_exception($io->get_composer_error(), []);
}
}
/**
* Returns the list of currently installed packages
*
* @param string|array $types Returns only the packages with the given type(s)
*
* @return array The installed packages associated to their version.
*
* @throws runtime_exception
*/
public function get_installed_packages($types)
{
return $this->wrap(function() use ($types) {
return $this->do_get_installed_packages($types);
});
}
/**
* Returns the list of currently installed packages
*
* /!\ Doesn't change the current working directory
*
* @param string|array $types Returns only the packages with the given type(s)
*
* @return array The installed packages associated to their version.
*/
protected function do_get_installed_packages($types)
{
$types = (array) $types;
try
{
$io = new NullIO();
$composer = Factory::create($io, $this->get_composer_ext_json_filename(), false);
$installed = [];
/** @var \Composer\Package\Link[] $required_links */
$required_links = $composer->getPackage()->getRequires();
$installed_packages = $composer->getRepositoryManager()->getLocalRepository()->getCanonicalPackages();
foreach ($installed_packages as $package)
{
if (in_array($package->getType(), $types, true))
{
$version = array_key_exists($package->getName(), $required_links) ?
$required_links[$package->getName()]->getPrettyConstraint() : '*';
$installed[$package->getName()] = $version;
}
}
return $installed;
}
catch (\Exception $e)
{
return [];
}
}
/**
* Gets the list of the available packages of the configured type in the configured repositories
*
* /!\ Doesn't change the current working directory
*
* @param string $type Returns only the packages with the given type
*
* @return array The name of the available packages, associated to their definition. Ordered by name.
*
* @throws runtime_exception
*/
public function get_available_packages($type)
{
return $this->wrap(function() use ($type) {
return $this->do_get_available_packages($type);
});
}
/**
* Gets the list of the available packages of the configured type in the configured repositories
*
* @param string $type Returns only the packages with the given type
*
* @return array The name of the available packages, associated to their definition. Ordered by name.
*/
protected function do_get_available_packages($type)
{
try
{
$this->generate_ext_json_file($this->do_get_installed_packages(explode(',', self::PHPBB_TYPES)));
$io = new NullIO();
$composer = Factory::create($io, $this->get_composer_ext_json_filename(), false);
/** @var ConstraintInterface $core_constraint */
$core_constraint = $composer->getPackage()->getRequires()['phpbb/phpbb']->getConstraint();
$core_stability = $composer->getPackage()->getMinimumStability();
$available = [];
$compatible_packages = [];
$repositories = $composer->getRepositoryManager()->getRepositories();
/** @var \Composer\Repository\RepositoryInterface $repository */
foreach ($repositories as $repository)
{
try
{
if ($repository instanceof ComposerRepository && $repository->hasProviders())
{
// Special case for packagist which exposes an api to retrieve all packages of a given type.
// For the others composer repositories with providers we can't do anything. It would be too slow.
$r = new \ReflectionObject($repository);
$repo_url = $r->getProperty('url');
$repo_url->setAccessible(true);
if ($repo_url->getValue($repository) === 'http://packagist.org')
{
$url = 'https://packagist.org/packages/list.json?type=' . $type;
$rfs = new RemoteFilesystem($io);
$hostname = parse_url($url, PHP_URL_HOST) ?: $url;
$json = $rfs->getContents($hostname, $url, false);
/** @var \Composer\Package\PackageInterface $package */
foreach (JsonFile::parseJson($json, $url)['packageNames'] as $package)
{
$versions = $repository->findPackages($package);
$compatible_packages = $this->get_compatible_versions($compatible_packages, $core_constraint, $core_stability, $package, $versions);
}
}
}
else
{
// Pre-filter repo packages by their type
$packages = [];
/** @var \Composer\Package\PackageInterface $package */
foreach ($repository->getPackages() as $package)
{
if ($package->getType() === $type)
{
$packages[$package->getName()][] = $package;
}
}
// Filter the compatibles versions
foreach ($packages as $package => $versions)
{
$compatible_packages = $this->get_compatible_versions($compatible_packages, $core_constraint, $core_stability, $package, $versions);
}
}
}
catch (\Exception $e)
{
// If a repo fails, just skip it.
continue;
}
}
foreach ($compatible_packages as $name => $versions)
{
// Determine the highest version of the package
/** @var CompletePackage $highest_version */
$highest_version = null;
/** @var CompletePackage $version */
foreach ($versions as $version)
{
if (!$highest_version || version_compare($version->getVersion(), $highest_version->getVersion(), '>'))
{
$highest_version = $version;
}
}
// Generates the entry
$available[$name] = [];
$available[$name]['name'] = $highest_version->getPrettyName();
$available[$name]['display_name'] = $highest_version->getExtra()['display-name'];
$available[$name]['composer_name'] = $highest_version->getName();
$available[$name]['version'] = $highest_version->getPrettyVersion();
if ($version instanceof CompletePackage)
{
$available[$name]['description'] = $highest_version->getDescription();
$available[$name]['url'] = $highest_version->getHomepage();
$available[$name]['authors'] = $highest_version->getAuthors();
}
else
{
$available[$name]['description'] = '';
$available[$name]['url'] = '';
$available[$name]['authors'] = [];
}
}
usort($available, function($a, $b)
{
return strcasecmp($a['display_name'], $b['display_name']);
});
return $available;
}
catch (\Exception $e)
{
return [];
}
}
/**
* Checks the requirements of the manager and returns true if it can be used.
*
* @return bool
*/
public function check_requirements()
{
$filesystem = new \phpbb\filesystem\filesystem();
return $filesystem->is_writable([
$this->root_path . $this->composer_filename,
$this->root_path . $this->packages_vendor_dir,
$this->root_path . substr($this->composer_filename, 0, -5) . '.lock',
]);
}
/**
* Updates $compatible_packages with the versions of $versions compatibles with the $core_constraint
*
* @param array $compatible_packages List of compatibles versions
* @param ConstraintInterface $core_constraint Constraint against the phpBB version
* @param string $core_stability Core stability
* @param string $package_name Considered package
* @param array $versions List of available versions
*
* @return array
*/
private function get_compatible_versions(array $compatible_packages, ConstraintInterface $core_constraint, $core_stability, $package_name, array $versions)
{
$core_stability_value = BasePackage::$stabilities[$core_stability];
/** @var \Composer\Package\PackageInterface $version */
foreach ($versions as $version)
{
try
{
if (BasePackage::$stabilities[$version->getStability()] > $core_stability_value)
{
continue;
}
if (array_key_exists('phpbb/phpbb', $version->getRequires()))
{
/** @var ConstraintInterface $package_constraint */
$package_constraint = $version->getRequires()['phpbb/phpbb']->getConstraint();
if (!$package_constraint->matches($core_constraint))
{
continue;
}
}
$compatible_packages[$package_name][] = $version;
}
catch (\Exception $e)
{
// Do nothing (to log when a true debug logger is available)
}
}
return $compatible_packages;
}
/**
* Generates and write the json file used to install the set of packages
*
* @param array $packages Packages to update.
* Each entry may be a name or an array associating a version constraint to a name
*/
protected function generate_ext_json_file(array $packages)
{
$io = new NullIO();
$composer = Factory::create($io, null, false);
$core_packages = $this->get_core_packages($composer);
// The composer/installers package must be installed on his own and not provided by the existing autoloader
$core_replace = $core_packages;
unset($core_replace['composer/installers']);
$ext_json_data = [
'require' => array_merge(
['php' => $this->get_core_php_requirement($composer)],
$core_packages,
$this->get_extra_dependencies(),
$packages),
'replace' => $core_replace,
'repositories' => $this->get_composer_repositories(),
'config' => [
'vendor-dir'=> $this->packages_vendor_dir,
],
'minimum-stability' => $this->minimum_stability,
];
$this->ext_json_file_backup = null;
$json_file = new JsonFile($this->get_composer_ext_json_filename());
try
{
$ext_json_file_backup = $json_file->read();
}
catch (ParsingException $e)
{
$ext_json_file_backup = '{}';
$lockFile = new JsonFile(substr($this->get_composer_ext_json_filename(), 0, -5) . '.lock');
$lockFile->write([]);
}
$json_file->write($ext_json_data);
$this->ext_json_file_backup = $ext_json_file_backup;
}
/**
* Restore the json file overridden by generate_ext_json_file()
*/
protected function restore_ext_json_file()
{
if ($this->ext_json_file_backup)
{
try
{
$json_file = new JsonFile($this->get_composer_ext_json_filename());
$json_file->write($this->ext_json_file_backup);
}
catch (\Exception $e)
{
}
$this->ext_json_file_backup = null;
}
}
/**
* Get the core installed packages
*
* @param Composer $composer Composer object to load the dependencies
* @return array The core packages with their version
*/
protected function get_core_packages(Composer $composer)
{
$core_deps = [];
$packages = $composer->getRepositoryManager()->getLocalRepository()->getCanonicalPackages();
foreach ($packages as $package)
{
$core_deps[$package->getName()] = $package->getPrettyVersion();
}
$core_deps['phpbb/phpbb'] = PHPBB_VERSION;
return $core_deps;
}
/**
* Get the PHP version required by the core
*
* @param Composer $composer Composer object to load the dependencies
* @return string The PHP version required by the core
*/
protected function get_core_php_requirement(Composer $composer)
{
return $composer->getLocker()->getLockData()['platform']['php'];
}
/**
* Generate the repositories entry of the packages json file
*
* @return array repositories entry
*/
protected function get_composer_repositories()
{
$repositories = [];
if (!$this->packagist)
{
$repositories[]['packagist'] = false;
}
foreach ($this->repositories as $repository)
{
if (preg_match('#^' . get_preg_expression('url') . '$#iu', $repository))
{
$repositories[] = [
'type' => 'composer',
'url' => $repository,
];
}
}
return $repositories;
}
/**
* Get the name of the json file used for the packages.
*
* @return string The json filename
*/
protected function get_composer_ext_json_filename()
{
return $this->composer_filename;
}
/**
* Get extra dependencies required to install the packages
*
* @return array Array of composer dependencies
*/
protected function get_extra_dependencies()
{
return [];
}
/**
* Sets the customs repositories
*
* @param array $repositories An array of composer repositories to use
*/
public function set_repositories(array $repositories)
{
$this->repositories = $repositories;
}
/**
* Allow or disallow packagist
*
* @param boolean $packagist
*/
public function set_packagist($packagist)
{
$this->packagist = $packagist;
}
/**
* Sets the name of the managed packages' json file
*
* @param string $composer_filename
*/
public function set_composer_filename($composer_filename)
{
$this->composer_filename = $composer_filename;
}
/**
* Sets the location of the managed packages' vendors
*
* @param string $packages_vendor_dir
*/
public function set_packages_vendor_dir($packages_vendor_dir)
{
$this->packages_vendor_dir = $packages_vendor_dir;
}
/**
* Sets the phpBB root path
*
* @param string $root_path
*/
public function set_root_path($root_path)
{
$this->root_path = $root_path;
}
/**
* Change the current directory to phpBB root
*/
protected function move_to_root()
{
if ($this->original_cwd === null)
{
$this->original_cwd = getcwd();
chdir($this->root_path);
}
}
/**
* Restore the current working directory if move_to_root() have been called
*/
protected function restore_cwd()
{
if ($this->original_cwd)
{
chdir($this->original_cwd);
$this->original_cwd = null;
}
}
/**
* Wraps a callable in order to adjust the context needed by composer
*
* @param callable $callable
*
* @return mixed
*/
protected function wrap(callable $callable)
{
// The composer installers works with a path relative to the current directory
$this->move_to_root();
// The composer installers uses some super globals
$super_globals = $this->request->super_globals_disabled();
$this->request->enable_super_globals();
try
{
return $callable();
}
finally
{
$this->restore_cwd();
if ($super_globals)
{
$this->request->disable_super_globals();
}
}
}
}