VersionChecker.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  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 Constraint|bool representing PHP version
  36. */
  37. private $phpVersion = false;
  38. /**
  39. * @var string[] List of installed PHP extensions
  40. */
  41. private $phpExtensions = [];
  42. /**
  43. * @var bool[] List of provided abilities
  44. */
  45. private $abilities = [];
  46. /**
  47. * @var string[] List of provided ability errors
  48. */
  49. private $abilityErrors = [];
  50. /**
  51. * @var array Loaded extensions
  52. */
  53. private $loaded = [];
  54. /**
  55. * @var VersionParser
  56. */
  57. private $versionParser;
  58. /**
  59. * @param string $coreVersion Current version of core
  60. * @param string $phpVersion Current PHP version
  61. * @param string[] $phpExtensions List of installed PHP extensions
  62. * @param bool[] $abilities List of provided abilities
  63. * @param string[] $abilityErrors Error messages for the abilities
  64. */
  65. public function __construct(
  66. $coreVersion, $phpVersion, array $phpExtensions,
  67. array $abilities = [], array $abilityErrors = []
  68. ) {
  69. $this->versionParser = new VersionParser();
  70. $this->setCoreVersion( $coreVersion );
  71. $this->setPhpVersion( $phpVersion );
  72. $this->phpExtensions = $phpExtensions;
  73. $this->abilities = $abilities;
  74. $this->abilityErrors = $abilityErrors;
  75. }
  76. /**
  77. * Set an array with credits of all loaded extensions and skins.
  78. *
  79. * @param array $credits An array of installed extensions with credits of them
  80. * @return VersionChecker $this
  81. */
  82. public function setLoadedExtensionsAndSkins( array $credits ) {
  83. $this->loaded = $credits;
  84. return $this;
  85. }
  86. /**
  87. * Set MediaWiki core version.
  88. *
  89. * @param string $coreVersion Current version of core
  90. */
  91. private function setCoreVersion( $coreVersion ) {
  92. try {
  93. $this->coreVersion = new Constraint(
  94. '==',
  95. $this->versionParser->normalize( $coreVersion )
  96. );
  97. $this->coreVersion->setPrettyString( $coreVersion );
  98. } catch ( UnexpectedValueException $e ) {
  99. // Non-parsable version, don't fatal.
  100. }
  101. }
  102. /**
  103. * Set PHP version.
  104. *
  105. * @param string $phpVersion Current PHP version. Must be well-formed.
  106. * @throws UnexpectedValueException
  107. */
  108. private function setPhpVersion( $phpVersion ) {
  109. // normalize to make this throw an exception if the version is invalid
  110. $this->phpVersion = new Constraint(
  111. '==',
  112. $this->versionParser->normalize( $phpVersion )
  113. );
  114. $this->phpVersion->setPrettyString( $phpVersion );
  115. }
  116. /**
  117. * Check all given dependencies if they are compatible with the named
  118. * installed extensions in the $credits array.
  119. *
  120. * Example $extDependencies:
  121. * {
  122. * 'FooBar' => {
  123. * 'MediaWiki' => '>= 1.25.0',
  124. * 'platform': {
  125. * 'php': '>= 7.0.0',
  126. * 'ext-foo': '*',
  127. * 'ability-bar': true
  128. * },
  129. * 'extensions' => {
  130. * 'FooBaz' => '>= 1.25.0'
  131. * },
  132. * 'skins' => {
  133. * 'BazBar' => '>= 1.0.0'
  134. * }
  135. * }
  136. * }
  137. *
  138. * @param array $extDependencies All extensions that depend on other ones
  139. * @return array
  140. */
  141. public function checkArray( array $extDependencies ) {
  142. $errors = [];
  143. foreach ( $extDependencies as $extension => $dependencies ) {
  144. foreach ( $dependencies as $dependencyType => $values ) {
  145. switch ( $dependencyType ) {
  146. case ExtensionRegistry::MEDIAWIKI_CORE:
  147. $mwError = $this->handleDependency(
  148. $this->coreVersion,
  149. $values,
  150. $extension
  151. );
  152. if ( $mwError !== false ) {
  153. $errors[] = [
  154. 'msg' =>
  155. "{$extension} is not compatible with the current MediaWiki "
  156. . "core (version {$this->coreVersion->getPrettyString()}), "
  157. . "it requires: $values."
  158. ,
  159. 'type' => 'incompatible-core',
  160. ];
  161. }
  162. break;
  163. case 'platform':
  164. foreach ( $values as $dependency => $constraint ) {
  165. if ( $dependency === 'php' ) {
  166. // PHP version
  167. $phpError = $this->handleDependency(
  168. $this->phpVersion,
  169. $constraint,
  170. $extension
  171. );
  172. if ( $phpError !== false ) {
  173. $errors[] = [
  174. 'msg' =>
  175. "{$extension} is not compatible with the current PHP "
  176. . "version {$this->phpVersion->getPrettyString()}), "
  177. . "it requires: $constraint."
  178. ,
  179. 'type' => 'incompatible-php',
  180. ];
  181. }
  182. } elseif ( substr( $dependency, 0, 4 ) === 'ext-' ) {
  183. // PHP extensions
  184. $phpExtension = substr( $dependency, 4 );
  185. if ( $constraint !== '*' ) {
  186. throw new UnexpectedValueException( 'Version constraints for '
  187. . 'PHP extensions are not supported in ' . $extension );
  188. }
  189. if ( !in_array( $phpExtension, $this->phpExtensions, true ) ) {
  190. $errors[] = [
  191. 'msg' =>
  192. "{$extension} requires {$phpExtension} PHP extension "
  193. . "to be installed."
  194. ,
  195. 'type' => 'missing-phpExtension',
  196. 'missing' => $phpExtension,
  197. ];
  198. }
  199. } elseif ( substr( $dependency, 0, 8 ) === 'ability-' ) {
  200. // Other abilities the environment might provide.
  201. $ability = substr( $dependency, 8 );
  202. if ( !isset( $this->abilities[$ability] ) ) {
  203. throw new UnexpectedValueException( 'Dependency type '
  204. . $dependency . ' unknown in ' . $extension );
  205. }
  206. if ( !is_bool( $constraint ) ) {
  207. throw new UnexpectedValueException( 'Only booleans are '
  208. . 'allowed to to indicate the presence of abilities '
  209. . 'in ' . $extension );
  210. }
  211. if ( $constraint === true &&
  212. $this->abilities[$ability] !== true
  213. ) {
  214. // add custom error message for missing ability if specified
  215. $customMessage = '';
  216. if ( isset( $this->abilityErrors[$ability] ) ) {
  217. $customMessage = ': ' . $this->abilityErrors[$ability];
  218. }
  219. $errors[] = [
  220. 'msg' =>
  221. "{$extension} requires \"{$ability}\" ability"
  222. . $customMessage
  223. ,
  224. 'type' => 'missing-ability',
  225. 'missing' => $ability,
  226. ];
  227. }
  228. } else {
  229. // add other platform dependencies here
  230. throw new UnexpectedValueException( 'Dependency type ' . $dependency .
  231. ' unknown in ' . $extension );
  232. }
  233. }
  234. break;
  235. case 'extensions':
  236. case 'skins':
  237. foreach ( $values as $dependency => $constraint ) {
  238. $extError = $this->handleExtensionDependency(
  239. $dependency, $constraint, $extension, $dependencyType
  240. );
  241. if ( $extError !== false ) {
  242. $errors[] = $extError;
  243. }
  244. }
  245. break;
  246. default:
  247. throw new UnexpectedValueException( 'Dependency type ' . $dependencyType .
  248. ' unknown in ' . $extension );
  249. }
  250. }
  251. }
  252. return $errors;
  253. }
  254. /**
  255. * Handle a simple dependency to MediaWiki core or PHP. See handleMediaWikiDependency and
  256. * handlePhpDependency for details.
  257. *
  258. * @param Constraint|bool $version The version installed
  259. * @param string $constraint The required version constraint for this dependency
  260. * @param string $checkedExt The Extension, which depends on this dependency
  261. * @return bool false if no error, true else
  262. */
  263. private function handleDependency( $version, $constraint, $checkedExt ) {
  264. if ( $version === false ) {
  265. // Couldn't parse the version, so we can't check anything
  266. return false;
  267. }
  268. // if the installed and required version are compatible, return an empty array
  269. if ( $this->versionParser->parseConstraints( $constraint )
  270. ->matches( $version ) ) {
  271. return false;
  272. }
  273. return true;
  274. }
  275. /**
  276. * Handle a dependency to another extension.
  277. *
  278. * @param string $dependencyName The name of the dependency
  279. * @param string $constraint The required version constraint for this dependency
  280. * @param string $checkedExt The Extension, which depends on this dependency
  281. * @param string $type Either 'extensions' or 'skins'
  282. * @return bool|array false for no errors, or an array of info
  283. */
  284. private function handleExtensionDependency( $dependencyName, $constraint, $checkedExt,
  285. $type
  286. ) {
  287. // Check if the dependency is even installed
  288. if ( !isset( $this->loaded[$dependencyName] ) ) {
  289. return [
  290. 'msg' => "{$checkedExt} requires {$dependencyName} to be installed.",
  291. 'type' => "missing-$type",
  292. 'missing' => $dependencyName,
  293. ];
  294. }
  295. if ( $constraint === '*' ) {
  296. // short-circuit since any version is OK.
  297. return false;
  298. }
  299. // Check if the dependency has specified a version
  300. if ( !isset( $this->loaded[$dependencyName]['version'] ) ) {
  301. $msg = "{$dependencyName} does not expose its version, but {$checkedExt}"
  302. . " requires: {$constraint}.";
  303. return [
  304. 'msg' => $msg,
  305. 'type' => "incompatible-$type",
  306. 'incompatible' => $checkedExt,
  307. ];
  308. } else {
  309. // Try to get a constraint for the dependency version
  310. try {
  311. $installedVersion = new Constraint(
  312. '==',
  313. $this->versionParser->normalize( $this->loaded[$dependencyName]['version'] )
  314. );
  315. } catch ( UnexpectedValueException $e ) {
  316. // Non-parsable version, output an error message that the version
  317. // string is invalid
  318. return [
  319. 'msg' => "$dependencyName does not have a valid version string.",
  320. 'type' => 'invalid-version',
  321. ];
  322. }
  323. // Check if the constraint actually matches...
  324. if (
  325. !$this->versionParser->parseConstraints( $constraint )->matches( $installedVersion )
  326. ) {
  327. $msg = "{$checkedExt} is not compatible with the current "
  328. . "installed version of {$dependencyName} "
  329. . "({$this->loaded[$dependencyName]['version']}), "
  330. . "it requires: " . $constraint . '.';
  331. return [
  332. 'msg' => $msg,
  333. 'type' => "incompatible-$type",
  334. 'incompatible' => $checkedExt,
  335. ];
  336. }
  337. }
  338. return false;
  339. }
  340. }