From fbb85e2f4f4550dcdf9a0b8f28855959a5a53a47 Mon Sep 17 00:00:00 2001 From: Tristan Darricau Date: Wed, 9 Sep 2015 19:56:52 +0200 Subject: [PATCH] [ticket/11150] Add ability to manage extensions through composer PHPBB3-11150 --- phpBB/composer.json | 3 +- phpBB/composer.lock | 563 ++++++++++++++++-- phpBB/config/default/container/services.yml | 13 +- .../default/container/services_console.yml | 32 + .../default/container/services_extensions.yml | 39 ++ .../composer/exception/runtime_exception.php | 37 ++ phpBB/phpbb/composer/installer.php | 392 ++++++++++++ phpBB/phpbb/composer/manager.php | 190 ++++++ .../console/command/extension/install.php | 72 +++ .../command/extension/list_available.php | 75 +++ .../console/command/extension/remove.php | 72 +++ .../console/command/extension/update.php | 72 +++ .../data/v320/extensions_composer.php | 27 + 13 files changed, 1523 insertions(+), 64 deletions(-) create mode 100644 phpBB/config/default/container/services_extensions.yml create mode 100644 phpBB/phpbb/composer/exception/runtime_exception.php create mode 100644 phpBB/phpbb/composer/installer.php create mode 100644 phpBB/phpbb/composer/manager.php create mode 100644 phpBB/phpbb/console/command/extension/install.php create mode 100644 phpBB/phpbb/console/command/extension/list_available.php create mode 100644 phpBB/phpbb/console/command/extension/remove.php create mode 100644 phpBB/phpbb/console/command/extension/update.php create mode 100644 phpBB/phpbb/db/migration/data/v320/extensions_composer.php diff --git a/phpBB/composer.json b/phpBB/composer.json index 2207d10b9d..324ef32a1f 100644 --- a/phpBB/composer.json +++ b/phpBB/composer.json @@ -47,7 +47,8 @@ "symfony/routing": "~3.1", "symfony/twig-bridge": "~3.1", "symfony/yaml": "~3.1", - "twig/twig": "^1.0,<1.25" + "twig/twig": "^1.0,<1.25", + "composer/composer": "^1.0" }, "require-dev": { "fabpot/goutte": "~3.1", diff --git a/phpBB/composer.lock b/phpBB/composer.lock index 0d258bc677..6ad23abf13 100644 --- a/phpBB/composer.lock +++ b/phpBB/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "c53f2fa544168309d695bb1855c98c24", - "content-hash": "4bc93e90a4852f936c13986c3823831b", + "hash": "80752e19604f3bd6ac0af14d488a748b", + "content-hash": "60c4fa2116744111294d5c3a5c21afdd", "packages": [ { "name": "bantu/ini-get-wrapper", @@ -37,6 +37,263 @@ "description": "Convenience wrapper around ini_get()", "time": "2014-09-15 13:12:35" }, + { + "name": "composer/ca-bundle", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/composer/ca-bundle.git", + "reference": "5df9ed0ed0c9506ea6404a23450854e5df15cc12" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/5df9ed0ed0c9506ea6404a23450854e5df15cc12", + "reference": "5df9ed0ed0c9506ea6404a23450854e5df15cc12", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "ext-pcre": "*", + "php": "^5.3.2 || ^7.0" + }, + "require-dev": { + "symfony/process": "^2.5 || ^3.0" + }, + "suggest": { + "symfony/process": "This is necessary to reliably check whether openssl_x509_parse is vulnerable on older php versions, but can be ignored on PHP 5.5.6+" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\CaBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.", + "keywords": [ + "cabundle", + "cacert", + "certificate", + "ssl", + "tls" + ], + "time": "2016-07-18 23:07:53" + }, + { + "name": "composer/composer", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/composer/composer.git", + "reference": "b49a006748a460f8dae6500ec80ed021501ce969" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/composer/zipball/b49a006748a460f8dae6500ec80ed021501ce969", + "reference": "b49a006748a460f8dae6500ec80ed021501ce969", + "shasum": "" + }, + "require": { + "composer/ca-bundle": "^1.0", + "composer/semver": "^1.0", + "composer/spdx-licenses": "^1.0", + "justinrainbow/json-schema": "^1.6 || ^2.0", + "php": "^5.3.2 || ^7.0", + "psr/log": "^1.0", + "seld/cli-prompt": "^1.0", + "seld/jsonlint": "^1.4", + "seld/phar-utils": "^1.0", + "symfony/console": "^2.5 || ^3.0", + "symfony/filesystem": "^2.5 || ^3.0", + "symfony/finder": "^2.2 || ^3.0", + "symfony/process": "^2.1 || ^3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.5 || ^5.0.5", + "phpunit/phpunit-mock-objects": "^2.3 || ^3.0" + }, + "suggest": { + "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages", + "ext-zip": "Enabling the zip extension allows you to unzip archives", + "ext-zlib": "Allow gzip compression of HTTP requests" + }, + "bin": [ + "bin/composer" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\": "src/Composer" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Composer helps you declare, manage and install dependencies of PHP projects, ensuring you have the right stack everywhere.", + "homepage": "https://getcomposer.org/", + "keywords": [ + "autoload", + "dependency", + "package" + ], + "time": "2016-07-18 23:28:52" + }, + { + "name": "composer/semver", + "version": "1.4.2", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "c7cb9a2095a074d131b65a8a0cd294479d785573" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/c7cb9a2095a074d131b65a8a0cd294479d785573", + "reference": "c7cb9a2095a074d131b65a8a0cd294479d785573", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.5 || ^5.0.5", + "phpunit/phpunit-mock-objects": "2.3.0 || ^3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "time": "2016-08-30 16:08:34" + }, + { + "name": "composer/spdx-licenses", + "version": "1.1.4", + "source": { + "type": "git", + "url": "https://github.com/composer/spdx-licenses.git", + "reference": "88c26372b1afac36d8db601cdf04ad8716f53d88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/88c26372b1afac36d8db601cdf04ad8716f53d88", + "reference": "88c26372b1afac36d8db601cdf04ad8716f53d88", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.5 || ^5.0.5", + "phpunit/phpunit-mock-objects": "2.3.0 || ^3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Spdx\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "SPDX licenses list and validation library.", + "keywords": [ + "license", + "spdx", + "validator" + ], + "time": "2016-05-04 12:27:30" + }, { "name": "google/recaptcha", "version": "1.1.2", @@ -253,6 +510,72 @@ ], "time": "2016-06-24 23:00:38" }, + { + "name": "justinrainbow/json-schema", + "version": "2.0.5", + "source": { + "type": "git", + "url": "https://github.com/justinrainbow/json-schema.git", + "reference": "6b2a33e6a768f96bdc2ead5600af0822eed17d67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/6b2a33e6a768f96bdc2ead5600af0822eed17d67", + "reference": "6b2a33e6a768f96bdc2ead5600af0822eed17d67", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "json-schema/json-schema-test-suite": "1.2.0", + "phpdocumentor/phpdocumentor": "~2", + "phpunit/phpunit": "^4.8.22" + }, + "bin": [ + "bin/validate-json" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "JsonSchema\\": "src/JsonSchema/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bruno Prieto Reis", + "email": "bruno.p.reis@gmail.com" + }, + { + "name": "Justin Rainbow", + "email": "justin.rainbow@gmail.com" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Robert Schönthal", + "email": "seroscho@googlemail.com" + } + ], + "description": "A library to validate a json schema.", + "homepage": "https://github.com/justinrainbow/json-schema", + "keywords": [ + "json", + "schema" + ], + "time": "2016-06-02 10:59:52" + }, { "name": "lusitanian/oauth", "version": "v0.8.10", @@ -703,6 +1026,144 @@ ], "time": "2017-01-22 17:12:21" }, + { + "name": "seld/cli-prompt", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/cli-prompt.git", + "reference": "8cbe10923cae5bcd7c5a713f6703fc4727c8c1b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/cli-prompt/zipball/8cbe10923cae5bcd7c5a713f6703fc4727c8c1b4", + "reference": "8cbe10923cae5bcd7c5a713f6703fc4727c8c1b4", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Seld\\CliPrompt\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be" + } + ], + "description": "Allows you to prompt for user input on the command line, and optionally hide the characters they type", + "keywords": [ + "cli", + "console", + "hidden", + "input", + "prompt" + ], + "time": "2016-04-18 09:31:41" + }, + { + "name": "seld/jsonlint", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/jsonlint.git", + "reference": "66834d3e3566bb5798db7294619388786ae99394" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/66834d3e3566bb5798db7294619388786ae99394", + "reference": "66834d3e3566bb5798db7294619388786ae99394", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0" + }, + "bin": [ + "bin/jsonlint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Seld\\JsonLint\\": "src/Seld/JsonLint/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "JSON Linter", + "keywords": [ + "json", + "linter", + "parser", + "validator" + ], + "time": "2015-11-21 02:21:41" + }, + { + "name": "seld/phar-utils", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/phar-utils.git", + "reference": "7009b5139491975ef6486545a39f3e6dad5ac30a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/7009b5139491975ef6486545a39f3e6dad5ac30a", + "reference": "7009b5139491975ef6486545a39f3e6dad5ac30a", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Seld\\PharUtils\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be" + } + ], + "description": "PHAR file format utilities, for when PHP phars you up", + "keywords": [ + "phra" + ], + "time": "2015-10-13 18:44:15" + }, { "name": "symfony/config", "version": "v3.2.0", @@ -1294,6 +1755,55 @@ ], "time": "2016-11-14 01:06:16" }, + { + "name": "symfony/process", + "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "04c2dfaae4ec56a5c677b0c69fac34637d815758" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/04c2dfaae4ec56a5c677b0c69fac34637d815758", + "reference": "04c2dfaae4ec56a5c677b0c69fac34637d815758", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Process Component", + "homepage": "https://symfony.com", + "time": "2016-07-28 11:13:48" + }, { "name": "symfony/proxy-manager-bridge", "version": "v3.2.0", @@ -3231,55 +3741,6 @@ "homepage": "https://symfony.com", "time": "2016-11-25 12:32:42" }, - { - "name": "symfony/process", - "version": "v3.2.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "02ea84847aad71be7e32056408bb19f3a616cdd3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/02ea84847aad71be7e32056408bb19f3a616cdd3", - "reference": "02ea84847aad71be7e32056408bb19f3a616cdd3", - "shasum": "" - }, - "require": { - "php": ">=5.5.9" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.2-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Process\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Process Component", - "homepage": "https://symfony.com", - "time": "2016-11-24 10:40:28" - }, { "name": "webmozart/assert", "version": "1.2.0", diff --git a/phpBB/config/default/container/services.yml b/phpBB/config/default/container/services.yml index 9bb1d673f4..17fa223dda 100644 --- a/phpBB/config/default/container/services.yml +++ b/phpBB/config/default/container/services.yml @@ -8,6 +8,7 @@ imports: - { resource: services_cron.yml } - { resource: services_db.yml } - { resource: services_event.yml } + - { resource: services_extensions.yml } - { resource: services_feed.yml } - { resource: services_files.yml } - { resource: services_filesystem.yml } @@ -98,18 +99,6 @@ services: - '%core.root_path%' - '@template' - ext.manager: - class: phpbb\extension\manager - arguments: - - '@service_container' - - '@dbal.conn' - - '@config' - - '@filesystem' - - '%tables.ext%' - - '%core.root_path%' - - '%core.php_ext%' - - '@cache' - file_downloader: class: phpbb\file_downloader diff --git a/phpBB/config/default/container/services_console.yml b/phpBB/config/default/container/services_console.yml index e25ab4f03f..3305ede490 100644 --- a/phpBB/config/default/container/services_console.yml +++ b/phpBB/config/default/container/services_console.yml @@ -141,6 +141,22 @@ services: tags: - { name: console.command } + console.command.extension.install: + class: phpbb\console\command\extension\install + arguments: + - @user + - @ext.composer.manager + tags: + - { name: console.command } + + console.command.extension.list_available: + class: phpbb\console\command\extension\list_available + arguments: + - @user + - @ext.composer.manager + tags: + - { name: console.command } + console.command.extension.purge: class: phpbb\console\command\extension\purge arguments: @@ -150,6 +166,14 @@ services: tags: - { name: console.command } + console.command.extension.remove: + class: phpbb\console\command\extension\remove + arguments: + - @user + - @ext.composer.manager + tags: + - { name: console.command } + console.command.extension.show: class: phpbb\console\command\extension\show arguments: @@ -159,6 +183,14 @@ services: tags: - { name: console.command } + console.command.extension.update: + class: phpbb\console\command\extension\update + arguments: + - @user + - @ext.composer.manager + tags: + - { name: console.command } + console.command.fixup.recalculate_email_hash: class: phpbb\console\command\fixup\recalculate_email_hash arguments: diff --git a/phpBB/config/default/container/services_extensions.yml b/phpBB/config/default/container/services_extensions.yml new file mode 100644 index 0000000000..3a2e83f73a --- /dev/null +++ b/phpBB/config/default/container/services_extensions.yml @@ -0,0 +1,39 @@ +services: + ext.manager: + class: phpbb\extension\manager + arguments: + - @service_container + - @dbal.conn + - @config + - @filesystem + - %tables.ext% + - %core.root_path% + - %core.php_ext% + - @cache + + ext.composer.installer: + class: phpbb\composer\installer + arguments: + - %core.root_path% + - @config + + ext.composer.manager: + class: phpbb\composer\manager + arguments: + - @ext.composer.installer + - phpbb-extension + - EXTENSIONS_ + + style.composer.manager: + class: phpbb\composer\manager + arguments: + - @ext.composer.installer + - phpbb-style + - STYLES_ + + lang.composer.manager: + class: phpbb\composer\manager + arguments: + - @ext.composer.installer + - phpbb-language + - LANGUAGES_ diff --git a/phpBB/phpbb/composer/exception/runtime_exception.php b/phpBB/phpbb/composer/exception/runtime_exception.php new file mode 100644 index 0000000000..eb92759318 --- /dev/null +++ b/phpBB/phpbb/composer/exception/runtime_exception.php @@ -0,0 +1,37 @@ + + * @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); + } + +} diff --git a/phpBB/phpbb/composer/installer.php b/phpBB/phpbb/composer/installer.php new file mode 100644 index 0000000000..05c3ef2d68 --- /dev/null +++ b/phpBB/phpbb/composer/installer.php @@ -0,0 +1,392 @@ + + * @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; + } +} diff --git a/phpBB/phpbb/composer/manager.php b/phpBB/phpbb/composer/manager.php new file mode 100644 index 0000000000..836f39b509 --- /dev/null +++ b/phpBB/phpbb/composer/manager.php @@ -0,0 +1,190 @@ + + * @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; + } +} diff --git a/phpBB/phpbb/console/command/extension/install.php b/phpBB/phpbb/console/command/extension/install.php new file mode 100644 index 0000000000..f2358e7e4a --- /dev/null +++ b/phpBB/phpbb/console/command/extension/install.php @@ -0,0 +1,72 @@ + +* @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\console\command\extension; + +use phpbb\composer\manager; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +class install extends \phpbb\console\command\command +{ + /** + * @var \phpbb\composer\manager Composer extensions manager + */ + protected $manager; + + public function __construct(\phpbb\user $user, manager $manager) + { + $this->manager = $manager; + + parent::__construct($user); + } + + /** + * Sets the command name and description + * + * @return null + */ + protected function configure() + { + $this + ->setName('extension:install') + ->setDescription($this->user->lang('CLI_DESCRIPTION_EXTENSION_INSTALL')) + ->addArgument( + 'extensions', + InputArgument::IS_ARRAY | InputArgument::REQUIRED, + $this->user->lang('CLI_DESCRIPTION_EXTENSION_INSTALL')) + ; + } + + /** + * Executes the command extension:install + * + * @param InputInterface $input + * @param OutputInterface $output + * @return integer + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output); + + $extensions = $input->getArgument('extensions'); + + $this->manager->install($extensions); + + $io->success('All extensions installed'); + + return 0; + } +} diff --git a/phpBB/phpbb/console/command/extension/list_available.php b/phpBB/phpbb/console/command/extension/list_available.php new file mode 100644 index 0000000000..0b20ce2d5e --- /dev/null +++ b/phpBB/phpbb/console/command/extension/list_available.php @@ -0,0 +1,75 @@ + +* @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\console\command\extension; + +use Composer\Package\CompletePackage; +use Composer\Package\PackageInterface; +use phpbb\composer\installer; +use phpbb\composer\manager; +use Symfony\Component\Console\Formatter\OutputFormatter; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +class list_available extends \phpbb\console\command\command +{ + /** + * @var \phpbb\composer\manager Composer extensions manager + */ + protected $manager; + + public function __construct(\phpbb\user $user, manager $manager) + { + $this->manager = $manager; + + parent::__construct($user); + } + + /** + * Sets the command name and description + * + * @return null + */ + protected function configure() + { + $this + ->setName('extension:list-available') + ->setDescription($this->user->lang('CLI_DESCRIPTION_EXTENSION_LIST_AVAILABLE')) + ; + } + + /** + * Executes the command extension:install + * + * @param InputInterface $input + * @param OutputInterface $output + * @return integer + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output); + + $extensions = []; + + foreach ($this->manager->get_available_packages() as $package) + { + $extensions[] = '' . $package['name'] . ' ' . $package['url'] . "\n" . $package['description']; + } + + $io->listing($extensions); + + return 0; + } +} diff --git a/phpBB/phpbb/console/command/extension/remove.php b/phpBB/phpbb/console/command/extension/remove.php new file mode 100644 index 0000000000..a668322cdf --- /dev/null +++ b/phpBB/phpbb/console/command/extension/remove.php @@ -0,0 +1,72 @@ + +* @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\console\command\extension; + +use phpbb\composer\manager; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +class remove extends \phpbb\console\command\command +{ + /** + * @var \phpbb\composer\manager Composer extensions manager + */ + protected $manager; + + public function __construct(\phpbb\user $user, manager $manager) + { + $this->manager = $manager; + + parent::__construct($user); + } + + /** + * Sets the command name and description + * + * @return null + */ + protected function configure() + { + $this + ->setName('extension:remove') + ->setDescription($this->user->lang('CLI_DESCRIPTION_EXTENSION_REMOVE')) + ->addArgument( + 'extensions', + InputArgument::IS_ARRAY | InputArgument::REQUIRED, + $this->user->lang('CLI_DESCRIPTION_EXTENSION_REMOVE')) + ; + } + + /** + * Executes the command extension:install + * + * @param InputInterface $input + * @param OutputInterface $output + * @return integer + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output); + + $extensions = $input->getArgument('extensions'); + + $this->manager->remove($extensions); + + $io->success('All extensions removed'); + + return 0; + } +} diff --git a/phpBB/phpbb/console/command/extension/update.php b/phpBB/phpbb/console/command/extension/update.php new file mode 100644 index 0000000000..01c9db0c28 --- /dev/null +++ b/phpBB/phpbb/console/command/extension/update.php @@ -0,0 +1,72 @@ + +* @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\console\command\extension; + +use phpbb\composer\manager; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +class update extends \phpbb\console\command\command +{ + /** + * @var \phpbb\composer\manager Composer extensions manager + */ + protected $manager; + + public function __construct(\phpbb\user $user, manager $manager) + { + $this->manager = $manager; + + parent::__construct($user); + } + + /** + * Sets the command name and description + * + * @return null + */ + protected function configure() + { + $this + ->setName('extension:update') + ->setDescription($this->user->lang('CLI_DESCRIPTION_EXTENSION_UPDATE')) + ->addArgument( + 'extensions', + InputArgument::IS_ARRAY | InputArgument::REQUIRED, + $this->user->lang('CLI_DESCRIPTION_EXTENSION_UPDATE')) + ; + } + + /** + * Executes the command extension:install + * + * @param InputInterface $input + * @param OutputInterface $output + * @return integer + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output); + + $extensions = $input->getArgument('extensions'); + + $this->manager->update($extensions); + + $io->success('All extensions updated'); + + return 0; + } +} diff --git a/phpBB/phpbb/db/migration/data/v320/extensions_composer.php b/phpBB/phpbb/db/migration/data/v320/extensions_composer.php new file mode 100644 index 0000000000..09771f1797 --- /dev/null +++ b/phpBB/phpbb/db/migration/data/v320/extensions_composer.php @@ -0,0 +1,27 @@ + +* @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\db\migration\data\v320; + +class extensions_composer extends \phpbb\db\migration\migration +{ + public function update_data() + { + return array( + array('config.add', array('exts_composer_repositories', serialize([]))), + array('config.add', array('exts_composer_packagist', true)), + array('config.add', array('exts_composer_json_file', 'composer-ext.json')), + array('config.add', array('exts_composer_vendor_dir', 'vendor-ext/')), + ); + } +}