Merge "registration: Allow specifying extension dependencies"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Fri, 16 Dec 2016 01:00:04 +0000 (01:00 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Fri, 16 Dec 2016 01:00:04 +0000 (01:00 +0000)
docs/extension.schema.json
includes/registration/ExtensionRegistry.php
includes/registration/VersionChecker.php
tests/phpunit/includes/registration/VersionCheckerTest.php

index 30feaef..a5543d1 100644 (file)
                },
                "requires": {
                        "type": "object",
-                       "description": "Indicates what versions of MediaWiki core are required. This syntax may be extended in the future, for example to check dependencies between other extensions.",
+                       "description": "Indicates what versions of MediaWiki core or extensions are required. This syntax may be extended in the future, for example to check dependencies between other services.",
                        "properties": {
                                "MediaWiki": {
                                        "type": "string",
                                        "description": "Version constraint string against MediaWiki core."
+                               },
+                               "extensions": {
+                                       "type": "object",
+                                       "description": "Set of version constraint strings against specific extensions."
+                               },
+                               "skins": {
+                                       "type": "object",
+                                       "description": "Set of version constraint strings against specific skins."
                                }
                        }
                },
index 0521f3b..76d25b6 100644 (file)
@@ -203,9 +203,10 @@ class ExtensionRegistry {
                $autoloadClasses = [];
                $autoloaderPaths = [];
                $processor = new ExtensionProcessor();
+               $versionChecker = new VersionChecker();
+               $versionChecker->setCoreVersion( $wgVersion );
+               $extDependencies = [];
                $incompatible = [];
-               $versionParser = new VersionChecker();
-               $versionParser->setCoreVersion( $wgVersion );
                foreach ( $queue as $path => $mtime ) {
                        $json = file_get_contents( $path );
                        if ( $json === false ) {
@@ -216,25 +217,13 @@ class ExtensionRegistry {
                                throw new Exception( "$path is not a valid JSON file." );
                        }
 
-                       // Check any constraints against MediaWiki core
-                       $requires = $processor->getRequirements( $info );
-                       if ( $requires ) {
-                               $versionCheck = $versionParser->checkArray(
-                                       [ $info['name'] => $requires ]
-                               );
-                               $incompatible = array_merge( $incompatible, $versionCheck );
-                               if ( $versionCheck ) {
-                                       continue;
-                               }
-                       }
-
                        if ( !isset( $info['manifest_version'] ) ) {
                                // For backwards-compatability, assume a version of 1
                                $info['manifest_version'] = 1;
                        }
                        $version = $info['manifest_version'];
                        if ( $version < self::OLDEST_MANIFEST_VERSION || $version > self::MANIFEST_VERSION ) {
-                               throw new Exception( "$path: unsupported manifest_version: {$version}" );
+                               $incompatible[] = "$path: unsupported manifest_version: {$version}";
                        }
 
                        $autoload = $this->processAutoLoader( dirname( $path ), $info );
@@ -242,12 +231,30 @@ class ExtensionRegistry {
                        $GLOBALS['wgAutoloadClasses'] += $autoload;
                        $autoloadClasses += $autoload;
 
+                       // get all requirements/dependencies for this extension
+                       $requires = $processor->getRequirements( $info );
+
+                       // validate the information needed and add the requirements
+                       if ( is_array( $requires ) && $requires && isset( $info['name'] ) ) {
+                               $extDependencies[$info['name']] = $requires;
+                       }
+
                        // Get extra paths for later inclusion
                        $autoloaderPaths = array_merge( $autoloaderPaths,
                                $processor->getExtraAutoloaderPaths( dirname( $path ), $info ) );
                        // Compatible, read and extract info
                        $processor->extractInfo( $path, $info, $version );
                }
+               $data = $processor->getExtractedInfo();
+
+               // check for incompatible extensions
+               $incompatible = array_merge(
+                       $incompatible,
+                       $versionChecker
+                               ->setLoadedExtensionsAndSkins( $data['credits'] )
+                               ->checkArray( $extDependencies )
+               );
+
                if ( $incompatible ) {
                        if ( count( $incompatible ) === 1 ) {
                                throw new Exception( $incompatible[0] );
@@ -255,7 +262,7 @@ class ExtensionRegistry {
                                throw new Exception( implode( "\n", $incompatible ) );
                        }
                }
-               $data = $processor->getExtractedInfo();
+
                // Need to set this so we can += to it later
                $data['globals']['wgAutoloadClasses'] = [];
                $data['autoload'] = $autoloadClasses;
index b61a10e..2a9401e 100644 (file)
@@ -35,6 +35,11 @@ class VersionChecker {
         */
        private $coreVersion = false;
 
+       /**
+        * @var array Loaded extensions
+        */
+       private $loaded = [];
+
        /**
         * @var VersionParser
         */
@@ -44,6 +49,18 @@ class VersionChecker {
                $this->versionParser = new VersionParser();
        }
 
+       /**
+        * 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.
         *
@@ -71,7 +88,13 @@ class VersionChecker {
         * Example $extDependencies:
         *      {
         *              'GoogleAPIClient' => {
-        *                      'MediaWiki' => '>= 1.25.0'
+        *                      'MediaWiki' => '>= 1.25.0',
+        *                      'extensions' => {
+        *                              'FakeExtension' => '>= 1.25.0'
+        *                      },
+        *          'skins' => {
+        *              'FakeSkin' => '>= 1.0.0'
+        *          }
         *              }
         *      }
         *
@@ -89,6 +112,15 @@ class VersionChecker {
                                                        $this->handleMediaWikiDependency( $values, $extension )
                                                );
                                                break;
+                                       case 'extensions':
+                                       case 'skin':
+                                               foreach ( $values as $dependency => $constraint ) {
+                                                       $errors = array_merge(
+                                                               $errors,
+                                                               $this->handleExtensionDependency( $dependency, $constraint, $extension )
+                                                       );
+                                               }
+                                               break;
                                        default:
                                                throw new UnexpectedValueException( 'Dependency type ' . $dependencyType .
                                                        ' unknown in ' . $extension );
@@ -125,4 +157,58 @@ class VersionChecker {
                         . "MediaWiki core (version {$this->coreVersion->getPrettyString()}), it requires: "
                         . $constraint . '.' ];
        }
+
+       /**
+        * 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
+        * @return array An empty array, if installed version is compatible with $constraint, an array
+        *  with an error message, otherwise.
+        */
+       private function handleExtensionDependency( $dependencyName, $constraint, $checkedExt ) {
+               $incompatible = [];
+               // Check if the dependency is even installed
+               if ( !isset( $this->loaded[$dependencyName] ) ) {
+                       $incompatible[] = "{$checkedExt} requires {$dependencyName} to be installed.";
+                       return $incompatible;
+               }
+               // Check if the dependency has specified a version
+               if ( !isset( $this->loaded[$dependencyName]['version'] ) ) {
+                       // If we depend upon any version, and none is set, that's fine.
+                       if ( $constraint === '*' ) {
+                               wfDebug( "{$dependencyName} does not expose it's version, but {$checkedExt}
+                                       mentions it with constraint '*'. Assume it's ok so." );
+                       } else {
+                               // Otherwise, mark it as incompatible.
+                               $incompatible[] = "{$dependencyName} does not expose it's version, but {$checkedExt}
+                                       requires: {$constraint}.";
+                       }
+               } 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, don't fatal, output an error message that the version
+                               // string is invalid
+                               return [ "Dependency $dependencyName provides an invalid version string." ];
+                       }
+                       // Check if the constraint actually matches...
+                       if (
+                               isset( $installedVersion ) &&
+                               !$this->versionParser->parseConstraints( $constraint )->matches( $installedVersion )
+                       ) {
+                               $incompatible[] = "{$checkedExt} is not compatible with the current "
+                                       . "installed version of {$dependencyName} "
+                                       . "({$this->loaded[$dependencyName]['version']}), "
+                                       . "it requires: " . $constraint . '.';
+                       }
+               }
+
+               return $incompatible;
+       }
 }
index daa407f..2bb1fe4 100644 (file)
@@ -41,4 +41,82 @@ class CoreVersionCheckerTest extends PHPUnit_Framework_TestCase {
                        [ 'totallyinvalid', '== 1.0', true ],
                ];
        }
+
+       /**
+        * @dataProvider provideType
+        */
+       public function testType( $given, $expected ) {
+               $checker = new VersionChecker();
+               $checker
+                       ->setCoreVersion( '1.0.0' )
+                       ->setLoadedExtensionsAndSkins( [
+                               'FakeDependency' => [
+                                       'version' => '1.0.0',
+                               ],
+                       ] );
+               $this->assertEquals( $expected, $checker->checkArray( [
+                       'FakeExtension' => $given,
+               ] )
+               );
+       }
+
+       public static function provideType() {
+               return [
+                       // valid type
+                       [
+                               [
+                                       'extensions' => [
+                                               'FakeDependency' => '1.0.0'
+                                       ]
+                               ],
+                               []
+                       ],
+                       [
+                               [
+                                       'MediaWiki' => '1.0.0'
+                               ],
+                               []
+                       ],
+               ];
+       }
+
+       /**
+        * Check, if a non-parsable version constraint does not throw an exception or
+        * returns any error message.
+        */
+       public function testInvalidConstraint() {
+               $checker = new VersionChecker();
+               $checker
+                       ->setCoreVersion( '1.0.0' )
+                       ->setLoadedExtensionsAndSkins( [
+                               'FakeDependency' => [
+                                       'version' => 'not really valid',
+                               ],
+                       ] );
+               $this->assertEquals( [ "Dependency FakeDependency provides an invalid version string." ],
+                       $checker->checkArray( [
+                               'FakeExtension' => [
+                                       'extensions' => [
+                                               'FakeDependency' => '1.24.3',
+                                       ],
+                               ],
+                       ] )
+               );
+
+               $checker = new VersionChecker();
+               $checker
+                       ->setCoreVersion( '1.0.0' )
+                       ->setLoadedExtensionsAndSkins( [
+                               'FakeDependency' => [
+                                       'version' => '1.24.3',
+                               ],
+                       ] );
+
+               $this->setExpectedException( 'UnexpectedValueException' );
+               $checker->checkArray( [
+                       'FakeExtension' => [
+                               'FakeDependency' => 'not really valid',
+                       ]
+               ] );
+       }
 }