mirror of
https://github.com/phpbb/phpbb.git
synced 2025-08-09 10:16:36 +02:00
[ticket/11150] Add ability to manage extensions through composer
PHPBB3-11150
This commit is contained in:
committed by
Tristan Darricau
parent
712626d845
commit
fbb85e2f4f
37
phpBB/phpbb/composer/exception/runtime_exception.php
Normal file
37
phpBB/phpbb/composer/exception/runtime_exception.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?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\exception;
|
||||
|
||||
use phpbb\exception\runtime_exception as base;
|
||||
|
||||
/**
|
||||
* Base class for exceptions thrown when managing packages through composer
|
||||
*/
|
||||
class runtime_exception extends base
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $prefix The language string prefix
|
||||
* @param string $message The Exception message to throw (must be a language variable).
|
||||
* @param array $parameters The parameters to use with the language var.
|
||||
* @param \Exception $previous The previous runtime_exception used for the runtime_exception chaining.
|
||||
* @param integer $code The Exception code.
|
||||
*/
|
||||
public function __construct($prefix, $message = '', array $parameters = [], \Exception $previous = null, $code = 0)
|
||||
{
|
||||
parent::__construct($prefix . $message, $parameters, $previous, $code);
|
||||
}
|
||||
|
||||
}
|
392
phpBB/phpbb/composer/installer.php
Normal file
392
phpBB/phpbb/composer/installer.php
Normal file
@@ -0,0 +1,392 @@
|
||||
<?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\BufferIO;
|
||||
use Composer\IO\NullIO;
|
||||
use Composer\Json\JsonFile;
|
||||
use Composer\Package\CompletePackage;
|
||||
use Composer\Package\PackageInterface;
|
||||
use Composer\Repository\ComposerRepository;
|
||||
use Composer\Repository\RepositoryInterface;
|
||||
use Composer\Util\RemoteFilesystem;
|
||||
use phpbb\config\config;
|
||||
use phpbb\exception\runtime_exception;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* Class to install packages through composer while freezing core dependencies.
|
||||
*/
|
||||
class installer
|
||||
{
|
||||
/**
|
||||
* @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 phpBB root path
|
||||
*/
|
||||
protected $root_path;
|
||||
|
||||
/**
|
||||
* @param \phpbb\config\config $config Config object
|
||||
* @param string $root_path phpBB root path
|
||||
*/
|
||||
public function __construct($root_path, config $config = null)
|
||||
{
|
||||
if ($config)
|
||||
{
|
||||
$this->repositories = (array) unserialize($config['exts_composer_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->root_path = $root_path;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
* @throws runtime_exception
|
||||
*/
|
||||
public function install(array $packages, $whitelist)
|
||||
{
|
||||
$this->generate_ext_json_file($packages);
|
||||
|
||||
putenv('COMPOSER_VENDOR_DIR=' . $this->root_path . '/' . $this->packages_vendor_dir);
|
||||
|
||||
$io = new BufferIO('', OutputInterface::VERBOSITY_DEBUG);
|
||||
$composer = Factory::create($io, $this->get_composer_ext_json_filename(), false);
|
||||
$install = \Composer\Installer::create($io, $composer);
|
||||
|
||||
$install
|
||||
->setVerbose(true)
|
||||
->setPreferSource(false)
|
||||
->setPreferDist(true)
|
||||
->setDevMode(false)
|
||||
->setUpdate(true)
|
||||
->setUpdateWhitelist($whitelist)
|
||||
->setWhitelistDependencies(false)
|
||||
->setIgnorePlatformRequirements(false)
|
||||
->setDumpAutoloader(false)
|
||||
->setPreferStable(true)
|
||||
->setRunScripts(false)
|
||||
->setDryRun(false);
|
||||
|
||||
try
|
||||
{
|
||||
$install->run();
|
||||
$output = $io->getOutput();
|
||||
$error_pos = strpos($output, 'Your requirements could not be resolved to an installable set of packages.');
|
||||
|
||||
if ($error_pos)
|
||||
{
|
||||
// TODO Extract the precise error and use language string
|
||||
throw new \RuntimeException(substr($output, $error_pos));
|
||||
}
|
||||
|
||||
}
|
||||
catch (\Exception $e)
|
||||
{
|
||||
throw new runtime_exception('Cannot install packages', [], $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of currently installed packages
|
||||
*
|
||||
* @param string $type Returns only the packages with the given type
|
||||
*
|
||||
* @return array The installed packages associated to their version.
|
||||
*/
|
||||
public function get_installed_packages($type)
|
||||
{
|
||||
try
|
||||
{
|
||||
$io = new NullIO();
|
||||
putenv('COMPOSER_VENDOR_DIR=' . $this->root_path . '/' . $this->packages_vendor_dir);
|
||||
$composer = Factory::create($io, $this->get_composer_ext_json_filename(), false);
|
||||
|
||||
$installed = [];
|
||||
$packages = $composer->getRepositoryManager()->getLocalRepository()->getCanonicalPackages();
|
||||
|
||||
foreach ($packages as $package)
|
||||
{
|
||||
if ($package->getType() === $type)
|
||||
{
|
||||
$installed[$package->getName()] = $package->getPrettyVersion();
|
||||
}
|
||||
}
|
||||
|
||||
return $installed;
|
||||
}
|
||||
catch (\Exception $e)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public function get_available_packages($type)
|
||||
{
|
||||
try
|
||||
{
|
||||
$io = new NullIO();
|
||||
|
||||
$composer = Factory::create($io, $this->get_composer_ext_json_filename(), false);
|
||||
|
||||
$available = [];
|
||||
$repositories = $composer->getRepositoryManager()->getRepositories();
|
||||
|
||||
/** @var RepositoryInterface $repository */
|
||||
foreach ($repositories as $repository)
|
||||
{
|
||||
if ($repository instanceof ComposerRepository && $repository->hasProviders())
|
||||
{
|
||||
$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 PackageInterface $package */
|
||||
foreach (JsonFile::parseJson($json, $url)['packageNames'] as $package)
|
||||
{
|
||||
$packages = $repository->findPackages($package);
|
||||
$package = array_pop($packages);
|
||||
$available[$package->getName()] = ['name' => $package->getPrettyName()];
|
||||
|
||||
if ($package instanceof CompletePackage)
|
||||
{
|
||||
$available[$package->getName()]['description'] = $package->getDescription();
|
||||
$available[$package->getName()]['url'] = $package->getHomepage();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
/** @var PackageInterface $package */
|
||||
foreach ($repository->getPackages() as $package)
|
||||
{
|
||||
if ($package->getType() === $type)
|
||||
{
|
||||
$available[$package->getName()] = ['name' => $package];
|
||||
|
||||
if ($package instanceof CompletePackage)
|
||||
{
|
||||
$available[$package->getName()]['description'] = $package->getDescription();
|
||||
$available[$package->getName()]['url'] = $package->getHomepage();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ksort($available);
|
||||
|
||||
return $available;
|
||||
}
|
||||
catch (\Exception $e)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
$core_json_data = [
|
||||
'require' => array_merge(
|
||||
['php' => $this->get_core_php_requirement($composer)],
|
||||
$core_packages,
|
||||
$this->get_extra_dependencies(),
|
||||
$packages),
|
||||
'replace' => $core_packages,
|
||||
'repositories' => $this->get_composer_repositories(),
|
||||
];
|
||||
|
||||
$json_file = new JsonFile($this->get_composer_ext_json_filename());
|
||||
$json_file->write($core_json_data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'] = $composer->getPackage()->getPrettyVersion();
|
||||
|
||||
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)
|
||||
{
|
||||
$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->root_path . $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($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;
|
||||
}
|
||||
}
|
190
phpBB/phpbb/composer/manager.php
Normal file
190
phpBB/phpbb/composer/manager.php
Normal file
@@ -0,0 +1,190 @@
|
||||
<?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 phpbb\composer\exception\runtime_exception;
|
||||
|
||||
/**
|
||||
* Class to manage packages through composer.
|
||||
*/
|
||||
class manager
|
||||
{
|
||||
/**
|
||||
* @var installer Composer packages installer
|
||||
*/
|
||||
protected $installer;
|
||||
|
||||
/**
|
||||
* @var string Type of packages (phpbb-packages per example)
|
||||
*/
|
||||
protected $package_type;
|
||||
|
||||
/**
|
||||
* @var string Prefix used for the exception's language string
|
||||
*/
|
||||
protected $exception_prefix;
|
||||
|
||||
/**
|
||||
* @var array Caches the managed packages list
|
||||
*/
|
||||
private $managed_packages;
|
||||
|
||||
/**
|
||||
* @var array Caches the available packages list
|
||||
*/
|
||||
private $available_packages;
|
||||
|
||||
/**
|
||||
* @param installer $installer Installer object
|
||||
* @param string $package_type Composer type of managed packages
|
||||
* @param string $exception_prefix Exception prefix to use
|
||||
*/
|
||||
public function __construct(installer $installer, $package_type, $exception_prefix)
|
||||
{
|
||||
$this->installer = $installer;
|
||||
$this->package_type = $package_type;
|
||||
$this->exception_prefix = $exception_prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs (if necessary) a 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
|
||||
* @throws runtime_exception
|
||||
*/
|
||||
public function install(array $packages)
|
||||
{
|
||||
$packages = $this->normalize_version($packages);
|
||||
|
||||
$already_managed = array_intersect(array_keys($this->get_managed_packages()), array_keys($packages));
|
||||
if (count($already_managed) !== 0)
|
||||
{
|
||||
throw new runtime_exception($this->exception_prefix, 'ALREADY_INSTALLED', [implode('|', $already_managed)]);
|
||||
}
|
||||
|
||||
$managed_packages = array_merge($this->get_managed_packages(), $packages);
|
||||
ksort($managed_packages);
|
||||
|
||||
$this->installer->install($managed_packages, array_keys($packages));
|
||||
|
||||
$this->managed_packages = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates or installs a 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
|
||||
* @throws runtime_exception
|
||||
*/
|
||||
public function update(array $packages)
|
||||
{
|
||||
$packages = $this->normalize_version($packages);
|
||||
|
||||
// TODO: if the extension is already enabled, we should disabled and re-enable it
|
||||
$not_managed = array_diff_key($packages, $this->get_managed_packages());
|
||||
if (count($not_managed) !== 0)
|
||||
{
|
||||
throw new runtime_exception($this->exception_prefix, 'NOT_MANAGED', [implode('|', array_keys($not_managed))]);
|
||||
}
|
||||
|
||||
$managed_packages = array_merge($this->get_managed_packages(), $packages);
|
||||
ksort($managed_packages);
|
||||
|
||||
$this->installer->install($managed_packages, array_keys($packages));
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a set of packages
|
||||
*
|
||||
* @param array $packages Packages to remove.
|
||||
* Each entry may be a name or an array associating a version constraint to a name
|
||||
* @throws runtime_exception
|
||||
*/
|
||||
public function remove(array $packages)
|
||||
{
|
||||
$packages = $this->normalize_version($packages);
|
||||
|
||||
// TODO: if the extension is already enabled, we should disabled (with an option for purge)
|
||||
$not_managed = array_diff_key($packages, $this->get_managed_packages());
|
||||
if (count($not_managed) !== 0)
|
||||
{
|
||||
throw new runtime_exception($this->exception_prefix, 'NOT_MANAGED', [implode('|', array_keys($not_managed))]);
|
||||
}
|
||||
|
||||
$managed_packages = array_diff_key($this->get_managed_packages(), $packages);
|
||||
ksort($managed_packages);
|
||||
|
||||
$this->installer->install($managed_packages, array_keys($packages));
|
||||
|
||||
$this->managed_packages = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells whether or not a package is managed by Composer.
|
||||
*
|
||||
* @param string $packages Package name
|
||||
* @return bool
|
||||
*/
|
||||
public function is_managed($packages)
|
||||
{
|
||||
return array_key_exists($packages, $this->get_managed_packages());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of managed packages
|
||||
*
|
||||
* @return array The managed packages associated to their version.
|
||||
*/
|
||||
public function get_managed_packages()
|
||||
{
|
||||
if ($this->managed_packages === null)
|
||||
{
|
||||
$this->managed_packages = $this->installer->get_installed_packages($this->package_type);
|
||||
}
|
||||
|
||||
return $this->managed_packages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of available packages
|
||||
*
|
||||
* @return array The name of the available packages, associated to their definition. Ordered by name.
|
||||
*/
|
||||
public function get_available_packages()
|
||||
{
|
||||
if ($this->available_packages === null)
|
||||
{
|
||||
$this->available_packages = $this->installer->get_available_packages($this->package_type);
|
||||
}
|
||||
|
||||
return $this->available_packages;
|
||||
}
|
||||
|
||||
protected function normalize_version($packages)
|
||||
{
|
||||
$normalized_packages = [];
|
||||
|
||||
foreach ($packages as $package)
|
||||
{
|
||||
if (!is_array($package))
|
||||
{
|
||||
$normalized_packages[$package] = '*';
|
||||
}
|
||||
}
|
||||
|
||||
return $normalized_packages;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user