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