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