365 lines
11 KiB
PHP
365 lines
11 KiB
PHP
<?php
|
|
/**
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program; if not, write to the Free Software Foundation, Inc.,
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
* http://www.gnu.org/copyleft/gpl.html
|
|
*
|
|
* @file
|
|
*/
|
|
|
|
namespace MediaWiki\Registration;
|
|
|
|
use Composer\Semver\Constraint\Constraint;
|
|
use Composer\Semver\VersionParser;
|
|
use UnexpectedValueException;
|
|
|
|
/**
|
|
* Check whether extensions and their dependencies meet certain version requirements.
|
|
*
|
|
* @since 1.29
|
|
* @ingroup ExtensionRegistry
|
|
* @author Legoktm
|
|
* @author Florian Schmidt
|
|
*/
|
|
class VersionChecker {
|
|
/**
|
|
* @var Constraint|bool representing MediaWiki core
|
|
*/
|
|
private $coreVersion = false;
|
|
|
|
/**
|
|
* @var Constraint|bool representing the PHP engine
|
|
*/
|
|
private $phpVersion = false;
|
|
|
|
/**
|
|
* @var string[] List of installed PHP extensions
|
|
*/
|
|
private $phpExtensions;
|
|
|
|
/**
|
|
* @var bool[] List of provided abilities
|
|
*/
|
|
private $abilities;
|
|
|
|
/**
|
|
* @var string[] List of provided ability errors
|
|
*/
|
|
private $abilityErrors;
|
|
|
|
/**
|
|
* @var array Loaded extensions
|
|
*/
|
|
private $loaded = [];
|
|
|
|
/**
|
|
* @var VersionParser
|
|
*/
|
|
private $versionParser;
|
|
|
|
/**
|
|
* @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,
|
|
array $abilities = [], array $abilityErrors = []
|
|
) {
|
|
$this->versionParser = new VersionParser();
|
|
$this->setCoreVersion( $coreVersion );
|
|
$this->setPhpVersion( $phpVersion );
|
|
$this->phpExtensions = $phpExtensions;
|
|
$this->abilities = $abilities;
|
|
$this->abilityErrors = $abilityErrors;
|
|
}
|
|
|
|
/**
|
|
* Set an array with credits of all loaded extensions and skins.
|
|
*
|
|
* @param array $credits An array of installed extensions with credits of them
|
|
*
|
|
* @return VersionChecker $this
|
|
*/
|
|
public function setLoadedExtensionsAndSkins( array $credits ) {
|
|
$this->loaded = $credits;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set MediaWiki core version.
|
|
*
|
|
* @param string $coreVersion Current version of core
|
|
*/
|
|
private function setCoreVersion( $coreVersion ) {
|
|
try {
|
|
$this->coreVersion = new Constraint(
|
|
'==',
|
|
$this->versionParser->normalize( $coreVersion )
|
|
);
|
|
$this->coreVersion->setPrettyString( $coreVersion );
|
|
} catch ( UnexpectedValueException $e ) {
|
|
// Non-parsable version, don't fatal.
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param string $phpVersion Current PHP version. Must be well-formed.
|
|
*
|
|
* @throws UnexpectedValueException
|
|
*/
|
|
private function setPhpVersion( $phpVersion ) {
|
|
// normalize to make this throw an exception if the version is invalid
|
|
$this->phpVersion = new Constraint(
|
|
'==',
|
|
$this->versionParser->normalize( $phpVersion )
|
|
);
|
|
$this->phpVersion->setPrettyString( $phpVersion );
|
|
}
|
|
|
|
/**
|
|
* Check all given dependencies if they are compatible with the named
|
|
* installed extensions in the $credits array.
|
|
*
|
|
* Example $extDependencies:
|
|
* {
|
|
* 'FooBar' => {
|
|
* 'MediaWiki' => '>= 1.25.0',
|
|
* 'platform': {
|
|
* 'php': '>= 7.0.0',
|
|
* 'ext-foo': '*',
|
|
* 'ability-bar': true
|
|
* },
|
|
* 'extensions' => {
|
|
* 'FooBaz' => '>= 1.25.0'
|
|
* },
|
|
* 'skins' => {
|
|
* 'BazBar' => '>= 1.0.0'
|
|
* }
|
|
* }
|
|
* }
|
|
*
|
|
* @param array $extDependencies All extensions that depend on other ones
|
|
*
|
|
* @return array[] List of errors
|
|
*/
|
|
public function checkArray( array $extDependencies ) {
|
|
$errors = [];
|
|
foreach ( $extDependencies as $extension => $dependencies ) {
|
|
foreach ( $dependencies as $dependencyType => $values ) {
|
|
switch ( $dependencyType ) {
|
|
case ExtensionRegistry::MEDIAWIKI_CORE:
|
|
$mwError = $this->handleDependency(
|
|
$this->coreVersion,
|
|
$values
|
|
);
|
|
if ( $mwError !== false ) {
|
|
$errors[] = [
|
|
'msg' =>
|
|
"{$extension} is not compatible with the current MediaWiki "
|
|
. "core (version {$this->coreVersion->getPrettyString()}), "
|
|
. "it requires: $values.",
|
|
'type' => 'incompatible-core',
|
|
];
|
|
}
|
|
break;
|
|
case 'platform':
|
|
foreach ( $values as $dependency => $constraint ) {
|
|
if ( $dependency === 'php' ) {
|
|
// PHP version
|
|
$phpError = $this->handleDependency(
|
|
$this->phpVersion,
|
|
$constraint
|
|
);
|
|
if ( $phpError !== false ) {
|
|
$errors[] = [
|
|
'msg' =>
|
|
"{$extension} is not compatible with the current PHP "
|
|
. "version {$this->phpVersion->getPrettyString()}), "
|
|
. "it requires: $constraint.",
|
|
'type' => 'incompatible-php',
|
|
];
|
|
}
|
|
} elseif ( substr( $dependency, 0, 4 ) === 'ext-' ) {
|
|
// PHP extensions
|
|
$phpExtension = substr( $dependency, 4 );
|
|
if ( $constraint !== '*' ) {
|
|
throw new UnexpectedValueException( 'Version constraints for '
|
|
. 'PHP extensions are not supported in ' . $extension );
|
|
}
|
|
if ( !in_array( $phpExtension, $this->phpExtensions, true ) ) {
|
|
$errors[] = [
|
|
'msg' =>
|
|
"{$extension} requires {$phpExtension} PHP extension "
|
|
. "to be installed.",
|
|
'type' => 'missing-phpExtension',
|
|
'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 &&
|
|
$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 .
|
|
' unknown in ' . $extension );
|
|
}
|
|
}
|
|
break;
|
|
case 'extensions':
|
|
case 'skins':
|
|
foreach ( $values as $dependency => $constraint ) {
|
|
$extError = $this->handleExtensionDependency(
|
|
$dependency, $constraint, $extension, $dependencyType
|
|
);
|
|
if ( $extError !== false ) {
|
|
$errors[] = $extError;
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
throw new UnexpectedValueException( 'Dependency type ' . $dependencyType .
|
|
' unknown in ' . $extension );
|
|
}
|
|
}
|
|
}
|
|
|
|
return $errors;
|
|
}
|
|
|
|
/**
|
|
* Handle a simple dependency to MediaWiki core or PHP. See handleMediaWikiDependency and
|
|
* handlePhpDependency for details.
|
|
*
|
|
* @param Constraint|false $version The version installed
|
|
* @param string $constraint The required version constraint for this dependency
|
|
*
|
|
* @return bool false if no error, true else
|
|
*/
|
|
private function handleDependency( $version, $constraint ) {
|
|
if ( $version === false ) {
|
|
// Couldn't parse the version, so we can't check anything
|
|
return false;
|
|
}
|
|
|
|
// if the installed and required version are compatible, return an empty array
|
|
if ( $this->versionParser->parseConstraints( $constraint )
|
|
->matches( $version ) ) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Handle a dependency to another extension.
|
|
*
|
|
* @param string $dependencyName The name of the dependency
|
|
* @param string $constraint The required version constraint for this dependency
|
|
* @param string $checkedExt The Extension, which depends on this dependency
|
|
* @param string $type Either 'extensions' or 'skins'
|
|
*
|
|
* @return bool|array false for no errors, or an array of info
|
|
*/
|
|
private function handleExtensionDependency( $dependencyName, $constraint, $checkedExt,
|
|
$type
|
|
) {
|
|
// Check if the dependency is even installed
|
|
if ( !isset( $this->loaded[$dependencyName] ) ) {
|
|
return [
|
|
'msg' => "{$checkedExt} requires {$dependencyName} to be installed.",
|
|
'type' => "missing-$type",
|
|
'missing' => $dependencyName,
|
|
];
|
|
}
|
|
if ( $constraint === '*' ) {
|
|
// short-circuit since any version is OK.
|
|
return false;
|
|
}
|
|
// Check if the dependency has specified a version
|
|
if ( !isset( $this->loaded[$dependencyName]['version'] ) ) {
|
|
$msg = "{$dependencyName} does not expose its version, but {$checkedExt}"
|
|
. " requires: {$constraint}.";
|
|
return [
|
|
'msg' => $msg,
|
|
'type' => "incompatible-$type",
|
|
'incompatible' => $checkedExt,
|
|
];
|
|
} else {
|
|
// Try to get a constraint for the dependency version
|
|
try {
|
|
$installedVersion = new Constraint(
|
|
'==',
|
|
$this->versionParser->normalize( $this->loaded[$dependencyName]['version'] )
|
|
);
|
|
} catch ( UnexpectedValueException $e ) {
|
|
// Non-parsable version, output an error message that the version
|
|
// string is invalid
|
|
return [
|
|
'msg' => "$dependencyName does not have a valid version string.",
|
|
'type' => 'invalid-version',
|
|
];
|
|
}
|
|
// Check if the constraint actually matches...
|
|
if (
|
|
!$this->versionParser->parseConstraints( $constraint )->matches( $installedVersion )
|
|
) {
|
|
$msg = "{$checkedExt} is not compatible with the current "
|
|
. "installed version of {$dependencyName} "
|
|
. "({$this->loaded[$dependencyName]['version']}), "
|
|
. "it requires: " . $constraint . '.';
|
|
return [
|
|
'msg' => $msg,
|
|
'type' => "incompatible-$type",
|
|
'incompatible' => $checkedExt,
|
|
];
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/** @deprecated class alias since 1.43 */
|
|
class_alias( VersionChecker::class, 'VersionChecker' );
|