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