"php": {
"type": "string",
"description": "Version constraint string against PHP."
+ },
+ "ability-shell": {
+ "type": "boolean",
+ "default": false,
+ "description": "Whether this extension requires shell access."
}
},
"patternProperties": {
"php": {
"type": "string",
"description": "Version constraint string against PHP."
+ },
+ "ability-shell": {
+ "type": "boolean",
+ "default": false,
+ "description": "Whether this extension requires shell access."
}
},
"patternProperties": {
*/
public $missingPhpExtensions = [];
+ /**
+ * @var string[]
+ */
+ public $missingAbilities = [];
+
/**
* @param array $errors Each error has a 'msg' and 'type' key at minimum
*/
case 'missing-phpExtension':
$this->missingPhpExtensions[] = $info['missing'];
break;
+ case 'missing-ability':
+ $this->missingAbilities[] = $info['missing'];
+ break;
case 'missing-skins':
$this->missingSkins[] = $info['missing'];
break;
use Composer\Semver\Semver;
use Wikimedia\ScopedCallback;
+use MediaWiki\Shell\Shell;
+use MediaWiki\ShellDisabledError;
/**
* ExtensionRegistry class
// 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
$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
*
* @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;
*/
private $phpExtensions = [];
+ /**
+ * @var bool[] List of provided abilities
+ */
+ private $abilities = [];
+
+ /**
+ * @var string[] List of provided ability errors
+ */
+ private $abilityErrors = [];
+
/**
* @var array Loaded extensions
*/
* @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;
}
/**
* 'MediaWiki' => '>= 1.25.0',
* 'platform': {
* 'php': '>= 7.0.0',
- * 'ext-foo': '*'
+ * 'ext-foo': '*',
+ * 'ability-bar': true
* },
* 'extensions' => {
* 'FooBaz' => '>= 1.25.0'
'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 .
* @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',
],
],
],
+ [
+ [
+ '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,
+ ],
+ ],
+ [],
+ ],
];
}
],
'phpLoadedExtension',
],
+ [
+ [
+ 'FakeExtension' => [
+ 'platform' => [
+ 'ability-invalidAbility' => true,
+ ],
+ ],
+ ],
+ 'ability-invalidAbility',
+ ],
+ [
+ [
+ 'FakeExtension' => [
+ 'platform' => [
+ 'presentAbility' => true,
+ ],
+ ],
+ ],
+ 'presentAbility',
+ ],
[
[
'FakeExtension' => [
* @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"
],
] );
}
+
+ /**
+ * @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' ],
+ ];
+ }
+
}