From 5e2c7466ad0da71662ba6c1b7e9ffd6463e08530 Mon Sep 17 00:00:00 2001 From: MGChecker Date: Sun, 14 Oct 2018 00:24:31 +0200 Subject: [PATCH] registration: Allow to require environment abilities This patch adds the possibility for extensions and skins to require certain environment abiltites that are not necessarily PHP extensions. For now, the only ability introduced is the ability to shell out, but the processing and testing is written in a more general way to allow users to add more abilties later on by just changing getAbilities(). In theory, this allows using VersionChecker to check for random abilities if they are specified in the constructor, as it is comletely environment agnostic and not really bound to just be used for checking extension compatibility. Furthermore, it is possible to specify custom error messages for each of these abilities in the constructor. Other parts of MediaWiki may use these features to check for requirements while working with totally different abilities. Bug: T212472 Change-Id: Iff8512530b08ef509e7ac0b6ed8fe9578ef3e2a1 --- docs/extension.schema.v1.json | 5 + docs/extension.schema.v2.json | 5 + .../registration/ExtensionDependencyError.php | 8 + includes/registration/ExtensionRegistry.php | 44 ++++- includes/registration/VersionChecker.php | 53 ++++++- .../registration/VersionCheckerTest.php | 150 +++++++++++++++++- 6 files changed, 254 insertions(+), 11 deletions(-) diff --git a/docs/extension.schema.v1.json b/docs/extension.schema.v1.json index 8cd4e711c0..36e2fe21ca 100644 --- a/docs/extension.schema.v1.json +++ b/docs/extension.schema.v1.json @@ -70,6 +70,11 @@ "php": { "type": "string", "description": "Version constraint string against PHP." + }, + "ability-shell": { + "type": "boolean", + "default": false, + "description": "Whether this extension requires shell access." } }, "patternProperties": { diff --git a/docs/extension.schema.v2.json b/docs/extension.schema.v2.json index 1d64095a46..ed903f889d 100644 --- a/docs/extension.schema.v2.json +++ b/docs/extension.schema.v2.json @@ -77,6 +77,11 @@ "php": { "type": "string", "description": "Version constraint string against PHP." + }, + "ability-shell": { + "type": "boolean", + "default": false, + "description": "Whether this extension requires shell access." } }, "patternProperties": { diff --git a/includes/registration/ExtensionDependencyError.php b/includes/registration/ExtensionDependencyError.php index c27cd2c18c..5329572a18 100644 --- a/includes/registration/ExtensionDependencyError.php +++ b/includes/registration/ExtensionDependencyError.php @@ -58,6 +58,11 @@ class ExtensionDependencyError extends Exception { */ public $missingPhpExtensions = []; + /** + * @var string[] + */ + public $missingAbilities = []; + /** * @param array $errors Each error has a 'msg' and 'type' key at minimum */ @@ -75,6 +80,9 @@ class ExtensionDependencyError extends Exception { case 'missing-phpExtension': $this->missingPhpExtensions[] = $info['missing']; break; + case 'missing-ability': + $this->missingAbilities[] = $info['missing']; + break; case 'missing-skins': $this->missingSkins[] = $info['missing']; break; diff --git a/includes/registration/ExtensionRegistry.php b/includes/registration/ExtensionRegistry.php index e3df499987..2607e5ab29 100644 --- a/includes/registration/ExtensionRegistry.php +++ b/includes/registration/ExtensionRegistry.php @@ -2,6 +2,8 @@ use Composer\Semver\Semver; use Wikimedia\ScopedCallback; +use MediaWiki\Shell\Shell; +use MediaWiki\ShellDisabledError; /** * ExtensionRegistry class @@ -144,7 +146,8 @@ class ExtensionRegistry { // A few more things to vary the cache on $versions = [ 'registration' => self::CACHE_VERSION, - 'mediawiki' => $wgVersion + 'mediawiki' => $wgVersion, + 'abilities' => $this->getAbilities(), ]; // We use a try/catch because we don't want to fail here @@ -207,6 +210,38 @@ class ExtensionRegistry { $this->finished = true; } + /** + * Get the list of abilities and their values + * @return bool[] + */ + private function getAbilities() { + return [ + 'shell' => !Shell::isDisabled(), + ]; + } + + /** + * Queries information about the software environment and constructs an appropiate version checker + * + * @return VersionChecker + */ + private function buildVersionChecker() { + global $wgVersion; + // array to optionally specify more verbose error messages for + // missing abilities + $abilityErrors = [ + 'shell' => ( new ShellDisabledError() )->getMessage(), + ]; + + return new VersionChecker( + $wgVersion, + PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION, + get_loaded_extensions(), + $this->getAbilities(), + $abilityErrors + ); + } + /** * Process a queue of extensions and return their extracted data * @@ -216,16 +251,11 @@ class ExtensionRegistry { * @throws ExtensionDependencyError */ public function readFromQueue( array $queue ) { - global $wgVersion; $autoloadClasses = []; $autoloadNamespaces = []; $autoloaderPaths = []; $processor = new ExtensionProcessor(); - $versionChecker = new VersionChecker( - $wgVersion, - PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION, - get_loaded_extensions() - ); + $versionChecker = $this->buildVersionChecker(); $extDependencies = []; $incompatible = []; $warnings = false; diff --git a/includes/registration/VersionChecker.php b/includes/registration/VersionChecker.php index 586729d057..a5d1fa1fcf 100644 --- a/includes/registration/VersionChecker.php +++ b/includes/registration/VersionChecker.php @@ -45,6 +45,16 @@ class VersionChecker { */ private $phpExtensions = []; + /** + * @var bool[] List of provided abilities + */ + private $abilities = []; + + /** + * @var string[] List of provided ability errors + */ + private $abilityErrors = []; + /** * @var array Loaded extensions */ @@ -59,12 +69,19 @@ class VersionChecker { * @param string $coreVersion Current version of core * @param string $phpVersion Current PHP version * @param string[] $phpExtensions List of installed PHP extensions + * @param bool[] $abilities List of provided abilities + * @param string[] $abilityErrors Error messages for the abilities */ - public function __construct( $coreVersion, $phpVersion, array $phpExtensions ) { + public function __construct( + $coreVersion, $phpVersion, array $phpExtensions, + array $abilities = [], array $abilityErrors = [] + ) { $this->versionParser = new VersionParser(); $this->setCoreVersion( $coreVersion ); $this->setPhpVersion( $phpVersion ); $this->phpExtensions = $phpExtensions; + $this->abilities = $abilities; + $this->abilityErrors = $abilityErrors; } /** @@ -121,7 +138,8 @@ class VersionChecker { * 'MediaWiki' => '>= 1.25.0', * 'platform': { * 'php': '>= 7.0.0', - * 'ext-foo': '*' + * 'ext-foo': '*', + * 'ability-bar': true * }, * 'extensions' => { * 'FooBaz' => '>= 1.25.0' @@ -193,6 +211,37 @@ class VersionChecker { 'missing' => $phpExtension, ]; } + } elseif ( substr( $dependency, 0, 8 ) === 'ability-' ) { + // Other abilities the environment might provide. + $ability = substr( $dependency, 8 ); + if ( !isset( $this->abilities[$ability] ) ) { + throw new UnexpectedValueException( 'Dependency type ' + . $dependency . ' unknown in ' . $extension ); + } + if ( !is_bool( $constraint ) ) { + throw new UnexpectedValueException( 'Only booleans are ' + . 'allowed to to indicate the presence of abilities ' + . 'in ' . $extension ); + } + + if ( $constraint === true && + $this->abilities[$ability] !== true + ) { + // add custom error message for missing ability if specified + $customMessage = ''; + if ( isset( $this->abilityErrors[$ability] ) ) { + $customMessage = ': ' . $this->abilityErrors[$ability]; + } + + $errors[] = [ + 'msg' => + "{$extension} requires \"{$ability}\" ability" + . $customMessage + , + 'type' => 'missing-ability', + 'missing' => $ability, + ]; + } } else { // add other platform dependencies here throw new UnexpectedValueException( 'Dependency type ' . $dependency . diff --git a/tests/phpunit/includes/registration/VersionCheckerTest.php b/tests/phpunit/includes/registration/VersionCheckerTest.php index 6b92444442..e824e3f02c 100644 --- a/tests/phpunit/includes/registration/VersionCheckerTest.php +++ b/tests/phpunit/includes/registration/VersionCheckerTest.php @@ -101,7 +101,21 @@ class VersionCheckerTest extends PHPUnit\Framework\TestCase { * @dataProvider provideType */ public function testType( $given, $expected ) { - $checker = new VersionChecker( '1.0.0', '7.0.0', [ 'phpLoadedExtension' ] ); + $checker = new VersionChecker( + '1.0.0', + '7.0.0', + [ 'phpLoadedExtension' ], + [ + 'presentAbility' => true, + 'presentAbilityWithMessage' => true, + 'missingAbility' => false, + 'missingAbilityWithMessage' => false, + ], + [ + 'presentAbilityWithMessage' => 'Present.', + 'missingAbilityWithMessage' => 'Missing.', + ] + ); $checker->setLoadedExtensionsAndSkins( [ 'FakeDependency' => [ 'version' => '1.0.0', @@ -218,6 +232,83 @@ class VersionCheckerTest extends PHPUnit\Framework\TestCase { ], ], ], + [ + [ + 'platform' => [ + 'ability-presentAbility' => true, + ], + ], + [], + ], + [ + [ + 'platform' => [ + 'ability-presentAbilityWithMessage' => true, + ], + ], + [], + ], + [ + [ + 'platform' => [ + 'ability-presentAbility' => false, + ], + ], + [], + ], + [ + [ + 'platform' => [ + 'ability-presentAbilityWithMessage' => false, + ], + ], + [], + ], + [ + [ + 'platform' => [ + 'ability-missingAbility' => true, + ], + ], + [ + [ + 'missing' => 'missingAbility', + 'type' => 'missing-ability', + 'msg' => 'FakeExtension requires "missingAbility" ability', + ], + ], + ], + [ + [ + 'platform' => [ + 'ability-missingAbilityWithMessage' => true, + ], + ], + [ + [ + 'missing' => 'missingAbilityWithMessage', + 'type' => 'missing-ability', + // phpcs:ignore Generic.Files.LineLength.TooLong + 'msg' => 'FakeExtension requires "missingAbilityWithMessage" ability: Missing.', + ], + ], + ], + [ + [ + 'platform' => [ + 'ability-missingAbility' => false, + ], + ], + [], + ], + [ + [ + 'platform' => [ + 'ability-missingAbilityWithMessage' => false, + ], + ], + [], + ], ]; } @@ -282,6 +373,26 @@ class VersionCheckerTest extends PHPUnit\Framework\TestCase { ], 'phpLoadedExtension', ], + [ + [ + 'FakeExtension' => [ + 'platform' => [ + 'ability-invalidAbility' => true, + ], + ], + ], + 'ability-invalidAbility', + ], + [ + [ + 'FakeExtension' => [ + 'platform' => [ + 'presentAbility' => true, + ], + ], + ], + 'presentAbility', + ], [ [ 'FakeExtension' => [ @@ -308,7 +419,15 @@ class VersionCheckerTest extends PHPUnit\Framework\TestCase { * @dataProvider provideInvalidDependency */ public function testInvalidDependency( $depencency, $type ) { - $checker = new VersionChecker( '1.0.0', '7.0.0', [ 'phpLoadedExtension' ] ); + $checker = new VersionChecker( + '1.0.0', + '7.0.0', + [ 'phpLoadedExtension' ], + [ + 'presentAbility' => true, + 'missingAbility' => false, + ] + ); $this->setExpectedException( UnexpectedValueException::class, "Dependency type $type unknown in FakeExtension" @@ -330,4 +449,31 @@ class VersionCheckerTest extends PHPUnit\Framework\TestCase { ], ] ); } + + /** + * @dataProvider provideInvalidAbilityType + */ + public function testInvalidAbilityType( $value ) { + $checker = new VersionChecker( '1.0.0', '7.0.0', [], [ 'presentAbility' => true ] ); + $this->setExpectedException( + UnexpectedValueException::class, + 'Only booleans are allowed to to indicate the presence of abilities in FakeExtension' + ); + $checker->checkArray( [ + 'FakeExtension' => [ + 'platform' => [ + 'ability-presentAbility' => $value, + ], + ], + ] ); + } + + public function provideInvalidAbilityType() { + return [ + [ null ], + [ 1 ], + [ '1' ], + ]; + } + } -- 2.20.1