VersionChecker.php 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. <?php
  2. /**
  3. * This program is free software; you can redistribute it and/or modify
  4. * it under the terms of the GNU General Public License as published by
  5. * the Free Software Foundation; either version 2 of the License, or
  6. * (at your option) any later version.
  7. *
  8. * This program is distributed in the hope that it will be useful,
  9. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. * GNU General Public License for more details.
  12. *
  13. * You should have received a copy of the GNU General Public License along
  14. * with this program; if not, write to the Free Software Foundation, Inc.,
  15. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  16. * http://www.gnu.org/copyleft/gpl.html
  17. *
  18. * @author Legoktm
  19. * @author Florian Schmidt
  20. */
  21. use Composer\Semver\VersionParser;
  22. use Composer\Semver\Constraint\Constraint;
  23. /**
  24. * Provides functions to check a set of extensions with dependencies against
  25. * a set of loaded extensions and given version information.
  26. *
  27. * @since 1.29
  28. */
  29. class VersionChecker {
  30. /**
  31. * @var Constraint|bool representing $wgVersion
  32. */
  33. private $coreVersion = false;
  34. /**
  35. * @var array Loaded extensions
  36. */
  37. private $loaded = [];
  38. /**
  39. * @var VersionParser
  40. */
  41. private $versionParser;
  42. /**
  43. * @param string $coreVersion Current version of core
  44. */
  45. public function __construct( $coreVersion ) {
  46. $this->versionParser = new VersionParser();
  47. $this->setCoreVersion( $coreVersion );
  48. }
  49. /**
  50. * Set an array with credits of all loaded extensions and skins.
  51. *
  52. * @param array $credits An array of installed extensions with credits of them
  53. * @return VersionChecker $this
  54. */
  55. public function setLoadedExtensionsAndSkins( array $credits ) {
  56. $this->loaded = $credits;
  57. return $this;
  58. }
  59. /**
  60. * Set MediaWiki core version.
  61. *
  62. * @param string $coreVersion Current version of core
  63. */
  64. private function setCoreVersion( $coreVersion ) {
  65. try {
  66. $this->coreVersion = new Constraint(
  67. '==',
  68. $this->versionParser->normalize( $coreVersion )
  69. );
  70. $this->coreVersion->setPrettyString( $coreVersion );
  71. } catch ( UnexpectedValueException $e ) {
  72. // Non-parsable version, don't fatal.
  73. }
  74. }
  75. /**
  76. * Check all given dependencies if they are compatible with the named
  77. * installed extensions in the $credits array.
  78. *
  79. * Example $extDependencies:
  80. * {
  81. * 'FooBar' => {
  82. * 'MediaWiki' => '>= 1.25.0',
  83. * 'extensions' => {
  84. * 'FooBaz' => '>= 1.25.0'
  85. * },
  86. * 'skins' => {
  87. * 'BazBar' => '>= 1.0.0'
  88. * }
  89. * }
  90. * }
  91. *
  92. * @param array $extDependencies All extensions that depend on other ones
  93. * @return array
  94. */
  95. public function checkArray( array $extDependencies ) {
  96. $errors = [];
  97. foreach ( $extDependencies as $extension => $dependencies ) {
  98. foreach ( $dependencies as $dependencyType => $values ) {
  99. switch ( $dependencyType ) {
  100. case ExtensionRegistry::MEDIAWIKI_CORE:
  101. $mwError = $this->handleMediaWikiDependency( $values, $extension );
  102. if ( $mwError !== false ) {
  103. $errors[] = [
  104. 'msg' => $mwError,
  105. 'type' => 'incompatible-core',
  106. ];
  107. }
  108. break;
  109. case 'extensions':
  110. case 'skins':
  111. foreach ( $values as $dependency => $constraint ) {
  112. $extError = $this->handleExtensionDependency(
  113. $dependency, $constraint, $extension, $dependencyType
  114. );
  115. if ( $extError !== false ) {
  116. $errors[] = $extError;
  117. }
  118. }
  119. break;
  120. default:
  121. throw new UnexpectedValueException( 'Dependency type ' . $dependencyType .
  122. ' unknown in ' . $extension );
  123. }
  124. }
  125. }
  126. return $errors;
  127. }
  128. /**
  129. * Handle a dependency to MediaWiki core. It will check, if a MediaWiki version constraint was
  130. * set with self::setCoreVersion before this call (if not, it will return an empty array) and
  131. * checks the version constraint given against it.
  132. *
  133. * @param string $constraint The required version constraint for this dependency
  134. * @param string $checkedExt The Extension, which depends on this dependency
  135. * @return bool|string false if no error, or a string with the message
  136. */
  137. private function handleMediaWikiDependency( $constraint, $checkedExt ) {
  138. if ( $this->coreVersion === false ) {
  139. // Couldn't parse the core version, so we can't check anything
  140. return false;
  141. }
  142. // if the installed and required version are compatible, return an empty array
  143. if ( $this->versionParser->parseConstraints( $constraint )
  144. ->matches( $this->coreVersion ) ) {
  145. return false;
  146. }
  147. // otherwise mark this as incompatible.
  148. return "{$checkedExt} is not compatible with the current "
  149. . "MediaWiki core (version {$this->coreVersion->getPrettyString()}), it requires: "
  150. . "$constraint.";
  151. }
  152. /**
  153. * Handle a dependency to another extension.
  154. *
  155. * @param string $dependencyName The name of the dependency
  156. * @param string $constraint The required version constraint for this dependency
  157. * @param string $checkedExt The Extension, which depends on this dependency
  158. * @param string $type Either 'extensions' or 'skins'
  159. * @return bool|array false for no errors, or an array of info
  160. */
  161. private function handleExtensionDependency( $dependencyName, $constraint, $checkedExt,
  162. $type
  163. ) {
  164. // Check if the dependency is even installed
  165. if ( !isset( $this->loaded[$dependencyName] ) ) {
  166. return [
  167. 'msg' => "{$checkedExt} requires {$dependencyName} to be installed.",
  168. 'type' => "missing-$type",
  169. 'missing' => $dependencyName,
  170. ];
  171. }
  172. // Check if the dependency has specified a version
  173. if ( !isset( $this->loaded[$dependencyName]['version'] ) ) {
  174. // If we depend upon any version, and none is set, that's fine.
  175. if ( $constraint === '*' ) {
  176. wfDebug( "{$dependencyName} does not expose its version, but {$checkedExt}"
  177. . " mentions it with constraint '*'. Assume it's ok so." );
  178. return false;
  179. } else {
  180. // Otherwise, mark it as incompatible.
  181. $msg = "{$dependencyName} does not expose its version, but {$checkedExt}"
  182. . " requires: {$constraint}.";
  183. return [
  184. 'msg' => $msg,
  185. 'type' => "incompatible-$type",
  186. 'incompatible' => $checkedExt,
  187. ];
  188. }
  189. } else {
  190. // Try to get a constraint for the dependency version
  191. try {
  192. $installedVersion = new Constraint(
  193. '==',
  194. $this->versionParser->normalize( $this->loaded[$dependencyName]['version'] )
  195. );
  196. } catch ( UnexpectedValueException $e ) {
  197. // Non-parsable version, output an error message that the version
  198. // string is invalid
  199. return [
  200. 'msg' => "$dependencyName does not have a valid version string.",
  201. 'type' => 'invalid-version',
  202. ];
  203. }
  204. // Check if the constraint actually matches...
  205. if (
  206. !$this->versionParser->parseConstraints( $constraint )->matches( $installedVersion )
  207. ) {
  208. $msg = "{$checkedExt} is not compatible with the current "
  209. . "installed version of {$dependencyName} "
  210. . "({$this->loaded[$dependencyName]['version']}), "
  211. . "it requires: " . $constraint . '.';
  212. return [
  213. 'msg' => $msg,
  214. 'type' => "incompatible-$type",
  215. 'incompatible' => $checkedExt,
  216. ];
  217. }
  218. }
  219. return false;
  220. }
  221. }