Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 106
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
PHPVersionCheck
0.00% covered (danger)
0.00%
0 / 106
0.00% covered (danger)
0.00%
0 / 8
702
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 checkRequiredPHPVersion
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
72
 checkVendorExistence
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 checkExtensionExistence
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
90
 outputHTMLHeader
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getIndexErrorOutput
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
2
 triggerError
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 run
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7// phpcs:disable PSR12.Properties.ConstantVisibility.NotFound
8
9namespace MediaWiki;
10
11/**
12 * Check PHP Version, as well as for composer dependencies in entry points,
13 * and display something vaguely comprehensible in the event of a totally
14 * unrecoverable error.
15 *
16 * @note Since we can't rely on anything external, the minimum PHP versions
17 * and MW current version are hardcoded in this class.
18 *
19 * @note This class should be compatible with PHP 5.4 through PHP 8
20 * (without warnings). It is no longer compatible with PHP 4.
21 *
22 * @internal
23 */
24class PHPVersionCheck {
25    /**
26     * The number of the MediaWiki version used.
27     *
28     * If you're updating MW_VERSION in Defines.php, you must also update this value.
29     *
30     * @note For PHP 7.0 compatibility, this constant has no visibility keyword.
31     *
32     * @internal
33     */
34    const MW_VERSION = '1.47';
35
36    /**
37     * @var string[] A mapping of PHP functions to PHP extensions.
38     * @note For PHP 5.5 compatibility, this is a property rather than a constant.
39     */
40    private static $functionsExtensionsMapping = [
41        'mb_substr'   => 'mbstring',
42        'xml_parser_create' => 'xml',
43        'ctype_digit' => 'ctype',
44        'iconv'       => 'iconv',
45        'mime_content_type' => 'fileinfo',
46        'intl_is_failure' => 'intl',
47    ];
48
49    /**
50     * @var string The format used for errors. One of "text" or "html"
51     */
52    private $format;
53
54    /**
55     * @var string
56     */
57    private $scriptPath;
58
59    /**
60     * @param string $format The format used for errors. One of "text" or "html"
61     * @param string $scriptPath Used when an error is formatted as HTML.
62     */
63    public function __construct( $format = 'text', $scriptPath = '/' ) {
64        $this->format = $format;
65        $this->scriptPath = $scriptPath;
66    }
67
68    /**
69     * Displays an error, if the installed PHP version does not meet the minimum requirement.
70     */
71    private function checkRequiredPHPVersion() {
72        // NOTE: Keep this in sync with composer.json and ScopeStructureTest.php
73        $minimumVersion = '8.3.0';
74
75        /**
76         * This is a list of known-bad ranges of PHP versions. Syntax is like SemVer – either:
77         *
78         *  - '1.2.3' to prohibit a single version of PHP, or
79         *  - '1.2.3 – 1.2.5' to block a range, inclusive.
80         *
81         * Whitespace will be ignored.
82         *
83         * The key is not shown to users; use it to prompt future developers as to why this was
84         * chosen, ideally one or more Phabricator task references.
85         *
86         * Remember to drop irrelevant ranges when bumping $minimumVersion.
87         */
88        $knownBad = [
89        ];
90
91        $passes = version_compare( PHP_VERSION, $minimumVersion, '>=' );
92
93        $versionString = "PHP $minimumVersion or higher";
94
95        // Left as a programmatic check to make it easier to update.
96        if ( count( $knownBad ) ) {
97            $versionString .= ' (and not ' . implode( ', ', array_values( $knownBad ) ) . ')';
98
99            foreach ( $knownBad as $range ) {
100                // As we don't have composer at this point, we have to do our own version range checking.
101                if ( strpos( $range, '-' ) ) {
102                    $passes = $passes && !(
103                        version_compare( PHP_VERSION, trim( strstr( $range, '-', true ) ), '>=' )
104                        && version_compare( PHP_VERSION, trim( substr( strstr( $range, '-', false ), 1 ) ), '<' )
105                    );
106                } else {
107                    $passes = $passes && version_compare( PHP_VERSION, trim( $range ), '<>' );
108                }
109            }
110        }
111
112        if ( !$passes ) {
113            $mwVersion = self::MW_VERSION;
114            $cliText = "Error: You are using an unsupported PHP version (PHP " . PHP_VERSION . ").\n"
115            . "MediaWiki $mwVersion needs $versionString.\n\nCheck if you might have a newer "
116            . "PHP executable with a different name.\n\n";
117
118            $web = [];
119            $web['intro'] = "MediaWiki $mwVersion requires $versionString; you are using PHP "
120                . PHP_VERSION . ".";
121
122            $web['longTitle'] = "Supported PHP versions";
123            // phpcs:disable Generic.Files.LineLength
124            $web['longHtml'] = <<<HTML
125        <p>
126            Please consider <a href="https://www.php.net/downloads.php">upgrading your copy of PHP</a>.
127            PHP versions less than v8.2.0 are no longer <a href="https://www.php.net/supported-versions.php">supported</a>
128            by the PHP Group and will not receive security or bugfix updates.
129        </p>
130        <p>
131            If for some reason you are unable to upgrade your PHP version, you will need to
132            <a href="https://www.mediawiki.org/wiki/Download">download</a> an older version of
133            MediaWiki from our website. See our
134            <a href="https://www.mediawiki.org/wiki/Compatibility#PHP">compatibility page</a>
135            for details of which versions are compatible with prior versions of PHP.
136        </p>
137HTML;
138            // phpcs:enable Generic.Files.LineLength
139            $this->triggerError(
140                $web,
141                $cliText
142            );
143        }
144    }
145
146    /**
147     * Displays an error, if the vendor/autoload.php file could not be found.
148     */
149    private function checkVendorExistence() {
150        if ( !file_exists( __DIR__ . '/../vendor/autoload.php' ) ) {
151            $cliText = "Error: You are missing some dependencies. \n"
152                . "MediaWiki has dependencies that need to be installed via Composer\n"
153                . "or from a separate repository. Please see\n"
154                . "https://www.mediawiki.org/wiki/Download_from_Git#Fetch_external_libraries\n"
155                . "for help with installing them.";
156
157            $web = [];
158            $web['intro'] = "Installing some dependencies is required.";
159            $web['longTitle'] = 'Dependencies';
160            // phpcs:disable Generic.Files.LineLength
161            $web['longHtml'] = <<<HTML
162        <p>
163        MediaWiki has dependencies that need to be installed via Composer
164        or from a separate repository. Please see the
165        <a href="https://www.mediawiki.org/wiki/Download_from_Git#Fetch_external_libraries">instructions
166        for installing external libraries</a> on MediaWiki.org.
167        </p>
168HTML;
169            // phpcs:enable Generic.Files.LineLength
170
171            $this->triggerError( $web, $cliText );
172        }
173    }
174
175    /**
176     * Displays an error, if a PHP extension does not exist.
177     */
178    private function checkExtensionExistence() {
179        $missingExtensions = [];
180        foreach ( self::$functionsExtensionsMapping as $function => $extension ) {
181            if ( !function_exists( $function ) ) {
182                $missingExtensions[] = [ $extension ];
183            }
184        }
185
186        // Special case: either of those is required, but only on 32-bit systems (T391169)
187        if ( PHP_INT_SIZE < 8 && !extension_loaded( 'gmp' ) && !extension_loaded( 'bcmath' ) ) {
188            $missingExtensions[] = [ 'bcmath', 'gmp' ];
189        }
190
191        if ( $missingExtensions ) {
192            $missingExtText = '';
193            $missingExtHtml = '';
194            $baseUrl = 'https://www.php.net';
195            foreach ( $missingExtensions as $extNames ) {
196                $plaintextLinks = [];
197                $htmlLinks = [];
198                foreach ( $extNames as $ext ) {
199                    $plaintextLinks[] = "$ext <$baseUrl/$ext>";
200                    $htmlLinks[] = "<b>$ext</b> (<a href=\"$baseUrl/$ext\">more information</a>)";
201                }
202
203                $missingExtText .= ' * ' . implode( ' or ', $plaintextLinks ) . "\n";
204                $missingExtHtml .= "<li>" . implode( ' or ', $htmlLinks ) . "</li>";
205            }
206
207            $cliText = "Error: Missing one or more required PHP extensions. Please see\n"
208                . "https://www.mediawiki.org/wiki/Manual:Installation_requirements#PHP\n"
209                . "for help with installing them.\n"
210                . "Please install or enable:\n" . $missingExtText;
211
212            $web = [];
213            $web['intro'] = "Installing some PHP extensions is required.";
214            $web['longTitle'] = 'Required PHP extensions';
215            $web['longHtml'] = <<<HTML
216        <p>
217        You are missing one or more extensions to PHP that MediaWiki requires to run. Please see the
218        <a href="https://www.mediawiki.org/wiki/Manual:Installation_requirements#PHP">PHP
219        installation requirements</a> on MediaWiki.org.
220        </p>
221        <p>Please install or enable:</p>
222        <ul>
223        $missingExtHtml
224        </ul>
225HTML;
226
227            $this->triggerError( $web, $cliText );
228        }
229    }
230
231    /**
232     * Output headers that prevents error pages to be cached.
233     */
234    private function outputHTMLHeader() {
235        $protocol = isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0';
236
237        header( "$protocol 500 MediaWiki configuration Error" );
238        // Don't cache error pages! They cause no end of trouble...
239        header( 'Cache-Control: no-cache' );
240    }
241
242    /**
243     * Returns an error page, which is suitable for output to the end user via a web browser.
244     *
245     * @param string $introText
246     * @param string $longTitle
247     * @param string $longHtml
248     * @return string
249     */
250    private function getIndexErrorOutput( $introText, $longTitle, $longHtml ) {
251        $encLogo =
252            htmlspecialchars( str_replace( '//', '/', $this->scriptPath . '/' ) .
253                'resources/assets/mediawiki.png' );
254
255        $introHtml = htmlspecialchars( $introText );
256        $longTitleHtml = htmlspecialchars( $longTitle );
257
258        header( 'Content-type: text/html; charset=UTF-8' );
259
260        $mwVersion = self::MW_VERSION;
261        $finalOutput = <<<HTML
262<!DOCTYPE html>
263<html lang="en" dir="ltr">
264    <head>
265        <meta charset="UTF-8" />
266        <title>MediaWiki {$mwVersion}</title>
267        <style media="screen">
268            body {
269                color: #000;
270                background-color: #fff;
271                font-family: sans-serif;
272                padding: 2em;
273                text-align: center;
274            }
275            p, img, h1, h2, ul {
276                text-align: left;
277                margin: 0.5em 0 1em;
278            }
279            h1 {
280                font-size: 120%;
281            }
282            h2 {
283                font-size: 110%;
284            }
285        </style>
286    </head>
287    <body>
288        <img src="{$encLogo}" alt="The MediaWiki logo" />
289        <h1>MediaWiki {$mwVersion} internal error</h1>
290        <p>
291            {$introHtml}
292        </p>
293        <h2>{$longTitleHtml}</h2>
294        {$longHtml}
295    </body>
296</html>
297HTML;
298
299        return $finalOutput;
300    }
301
302    /**
303     * Display something vaguely comprehensible in the event of a totally unrecoverable error.
304     * Does not assume access to *anything*; no globals, no autoloader, no database, no localisation.
305     *
306     * This function immediately terminates the PHP process.
307     *
308     * @param string[] $web
309     *  - (string) intro: Short error message, displayed on top.
310     *  - (string) longTitle: Title for the longer message.
311     *  - (string) longHtml: The longer message, as raw HTML.
312     * @param string $cliText
313     */
314    private function triggerError( array $web, $cliText ) {
315        if ( $this->format === 'html' ) {
316            // Used by index.php and mw-config/index.php
317            $this->outputHTMLHeader();
318            $finalOutput = $this->getIndexErrorOutput(
319                $web['intro'],
320                $web['longTitle'],
321                $web['longHtml']
322            );
323        } else {
324            // Used by Maintenance.php (CLI)
325            $finalOutput = $cliText;
326        }
327
328        echo "$finalOutput\n";
329        die( 1 );
330    }
331
332    /**
333     * Check PHP version and that external dependencies are installed, and
334     * display an informative error if either condition is not satisfied.
335     *
336     * @internal
337     */
338    public function run() {
339        $this->checkRequiredPHPVersion();
340        $this->checkVendorExistence();
341        $this->checkExtensionExistence();
342    }
343}