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/')),
+ );
+ }
+}