Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 101
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 / 95
0.00% covered (danger)
0.00%
0 / 8
506
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 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 checkExtensionExistence
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
30
 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 * 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// phpcs:disable Generic.Arrays.DisallowLongArraySyntax,PSR2.Classes.PropertyDeclaration,MediaWiki.Usage.DirUsage
22// phpcs:disable Squiz.Scope.MemberVarScope.Missing,Squiz.Scope.MethodScope.Missing
23/**
24 * Check PHP Version, as well as for composer dependencies in entry points,
25 * and display something vaguely comprehensible in the event of a totally
26 * unrecoverable error.
27 *
28 * @note Since we can't rely on anything external, the minimum PHP versions
29 * and MW current version are hardcoded in this class.
30 *
31 * @note This class uses setter methods instead of a constructor so that
32 * it can be compatible with PHP 4 through PHP 8 (without warnings).
33 */
34class PHPVersionCheck {
35    /** @var string The number of the MediaWiki version used. If you're updating MW_VERSION in Defines.php, you must also update this value. */
36    var $mwVersion = '1.43';
37
38    /** @var string[] A mapping of PHP functions to PHP extensions. */
39    var $functionsExtensionsMapping = array(
40        'mb_substr'   => 'mbstring',
41        'xml_parser_create' => 'xml',
42        'ctype_digit' => 'ctype',
43        'json_decode' => 'json',
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    var $format = 'text';
53
54    /**
55     * @var string
56     */
57    var $scriptPath = '/';
58
59    /**
60     * Set the format used for errors.
61     *
62     * @param string $format One of "text" or "html"
63     */
64    function setFormat( $format ) {
65        $this->format = $format;
66    }
67
68    /**
69     * Set the script path used for images in HTML-formatted errors.
70     *
71     * @param string $scriptPath
72     */
73    function setScriptPath( $scriptPath ) {
74        $this->scriptPath = $scriptPath;
75    }
76
77    /**
78     * Displays an error, if the installed PHP version does not meet the minimum requirement.
79     */
80    function checkRequiredPHPVersion() {
81        $minimumVersion = '7.4.3';
82
83        /**
84         * This is a list of known-bad ranges of PHP versions. Syntax is like SemVer – either:
85         *
86         *  - '1.2.3' to prohibit a single version of PHP, or
87         *  - '1.2.3 – 1.2.5' to block a range, inclusive.
88         *
89         * Whitespace will be ignored.
90         *
91         * The key is not shown to users; use it to prompt future developers as to why this was
92         * chosen, ideally one or more Phabricator task references.
93         *
94         * Remember to drop irrelevant ranges when bumping $minimumVersion.
95         */
96        $knownBad = array(
97        );
98
99        $passes = version_compare( PHP_VERSION, $minimumVersion, '>=' );
100
101        $versionString = "PHP $minimumVersion or higher";
102
103        // Left as a programmatic check to make it easier to update.
104        if ( count( $knownBad ) ) {
105            $versionString .= ' (and not ' . implode( ', ', array_values( $knownBad ) ) . ')';
106
107            foreach ( $knownBad as $range ) {
108                // As we don't have composer at this point, we have to do our own version range checking.
109                if ( strpos( $range, '-' ) ) {
110                    $passes = $passes && !(
111                        version_compare( PHP_VERSION, trim( strstr( $range, '-', true ) ), '>=' )
112                        && version_compare( PHP_VERSION, trim( substr( strstr( $range, '-', false ), 1 ) ), '<' )
113                    );
114                } else {
115                    $passes = $passes && version_compare( PHP_VERSION, trim( $range ), '<>' );
116                }
117            }
118        }
119
120        if ( !$passes ) {
121            $cliText = "Error: You are using an unsupported PHP version (PHP " . PHP_VERSION . ").\n"
122            . "MediaWiki $this->mwVersion needs $versionString.\n\nCheck if you might have a newer "
123            . "PHP executable with a different name.\n\n";
124
125            $web = array();
126            $web['intro'] = "MediaWiki $this->mwVersion requires $versionString; you are using PHP "
127                . PHP_VERSION . ".";
128
129            $web['longTitle'] = "Supported PHP versions";
130            // phpcs:disable Generic.Files.LineLength
131            $web['longHtml'] = <<<HTML
132        <p>
133            Please consider <a href="https://www.php.net/downloads.php">upgrading your copy of PHP</a>.
134            PHP versions less than v8.1.0 are no longer <a href="https://www.php.net/supported-versions.php">supported</a>
135            by the PHP Group and will not receive security or bugfix updates.
136        </p>
137        <p>
138            If for some reason you are unable to upgrade your PHP version, you will need to
139            <a href="https://www.mediawiki.org/wiki/Download">download</a> an older version of
140            MediaWiki from our website. See our
141            <a href="https://www.mediawiki.org/wiki/Compatibility#PHP">compatibility page</a>
142            for details of which versions are compatible with prior versions of PHP.
143        </p>
144HTML;
145            // phpcs:enable Generic.Files.LineLength
146            $this->triggerError(
147                $web,
148                $cliText
149            );
150        }
151    }
152
153    /**
154     * Displays an error, if the vendor/autoload.php file could not be found.
155     */
156    function checkVendorExistence() {
157        if ( !file_exists( dirname( __FILE__ ) . '/../vendor/autoload.php' ) ) {
158            $cliText = "Error: You are missing some external dependencies. \n"
159                . "MediaWiki has external dependencies that need to be installed via Composer\n"
160                . "or from a separate repository. Please see\n"
161                . "https://www.mediawiki.org/wiki/Manual:Installation_requirements#PHP and\n"
162                . "https://www.mediawiki.org/wiki/Download_from_Git#Fetch_external_libraries\n"
163                . "for help on installing the required components.";
164
165            $web = array();
166            $web['intro'] = "Installing some external dependencies (e.g. via composer) is required.";
167            $web['longTitle'] = 'External dependencies';
168            // phpcs:disable Generic.Files.LineLength
169            $web['longHtml'] = <<<HTML
170        <p>
171        MediaWiki has external dependencies that need to be installed via Composer
172        or from a separate repository. Please see the
173        <a href="https://www.mediawiki.org/wiki/Manual:Installation_requirements#PHP">PHP
174        installation requirements</a> and the
175        <a href="https://www.mediawiki.org/wiki/Download_from_Git#Fetch_external_libraries">instructions
176        for installing PHP libraries</a> on mediawiki.org for help on installing the required components.
177        </p>
178HTML;
179            // phpcs:enable Generic.Files.LineLength
180
181            $this->triggerError( $web, $cliText );
182        }
183    }
184
185    /**
186     * Displays an error, if a PHP extension does not exist.
187     */
188    function checkExtensionExistence() {
189        $missingExtensions = array();
190        foreach ( $this->functionsExtensionsMapping as $function => $extension ) {
191            if ( !function_exists( $function ) ) {
192                $missingExtensions[] = $extension;
193            }
194        }
195
196        if ( $missingExtensions ) {
197            $missingExtText = '';
198            $missingExtHtml = '';
199            $baseUrl = 'https://www.php.net';
200            foreach ( $missingExtensions as $ext ) {
201                $missingExtText .= " * $ext <$baseUrl/$ext>\n";
202                $missingExtHtml .= "<li><b>$ext</b> "
203                    . "(<a href=\"$baseUrl/$ext\">more information</a>)</li>";
204            }
205
206            $cliText = "Error: Missing one or more required components of PHP.\n"
207                . "You are missing a required extension to PHP that MediaWiki needs.\n"
208                . "Please install:\n" . $missingExtText;
209
210            $web = array();
211            $web['intro'] = "Installing some PHP extensions is required.";
212            $web['longTitle'] = 'Required components';
213            $web['longHtml'] = <<<HTML
214        <p>
215        You are missing a required extension to PHP that MediaWiki
216        requires to run. Please install:
217        </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}