Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
99.30% covered (success)
99.30%
141 / 142
100.00% covered (success)
100.00%
7 / 7
CRAP
100.00% covered (success)
100.00%
1 / 1
VersionChecker
100.00% covered (success)
100.00%
141 / 141
100.00% covered (success)
100.00%
7 / 7
37
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 setLoadedExtensionsAndSkins
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setCoreVersion
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 setPhpVersion
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 checkArray
100.00% covered (success)
100.00%
80 / 80
100.00% covered (success)
100.00%
1 / 1
23
 handleDependency
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 handleExtensionDependency
100.00% covered (success)
100.00%
36 / 36
100.00% covered (success)
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
21namespace MediaWiki\Registration;
22
23use Composer\Semver\Constraint\Constraint;
24use Composer\Semver\VersionParser;
25use 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 */
35class 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 */
365class_alias( VersionChecker::class, 'VersionChecker' );