Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
34.41% covered (danger)
34.41%
32 / 93
35.71% covered (danger)
35.71%
5 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
MWException
34.41% covered (danger)
34.41%
32 / 93
35.71% covered (danger)
35.71%
5 / 14
360.21
0.00% covered (danger)
0.00%
0 / 1
 useOutputPage
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
5
 isLoggable
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 useMessageCache
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
4.25
 msg
50.00% covered (danger)
50.00%
5 / 10
0.00% covered (danger)
0.00%
0 / 1
6.00
 getHTML
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
6
 getText
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getPageTitle
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 reportHTML
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
6
 report
57.14% covered (warning)
57.14%
4 / 7
0.00% covered (danger)
0.00%
0 / 1
3.71
 hasOverriddenHandler
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 writeToCommandLine
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
 isCommandLine
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 header
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 statusHeader
0.00% covered (danger)
0.00%
0 / 2
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
21use MediaWiki\Html\Html;
22use MediaWiki\Request\WebRequest;
23
24/**
25 * MediaWiki exception
26 *
27 * @newable
28 * @stable to extend
29 *
30 * @ingroup Exception
31 * @deprecated since 1.40, use native exceptions instead (either directly, or defining subclasses when appropriate)
32 */
33class MWException extends Exception {
34    /**
35     * Should the exception use $wgOut to output the error?
36     *
37     * @return bool
38     */
39    private function useOutputPage() {
40        // NOTE: keep in sync with MWExceptionRenderer::useOutputPage
41        return $this->useMessageCache() &&
42        !empty( $GLOBALS['wgFullyInitialised'] ) &&
43        !empty( $GLOBALS['wgOut'] ) &&
44        !defined( 'MEDIAWIKI_INSTALL' ) &&
45        // Don't send a skinned HTTP 500 page to API clients.
46        !defined( 'MW_API' );
47    }
48
49    /**
50     * Whether to log this exception in the exception debug log.
51     *
52     * @stable to override
53     *
54     * @since 1.23
55     * @return bool
56     */
57    public function isLoggable() {
58        return true;
59    }
60
61    /**
62     * Can the extension use the Message class/wfMessage to get i18n-ed messages?
63     *
64     * @stable to override
65     *
66     * @return bool
67     */
68    public function useMessageCache() {
69        foreach ( $this->getTrace() as $frame ) {
70            if ( isset( $frame['class'] ) && $frame['class'] === LocalisationCache::class ) {
71                return false;
72            }
73        }
74        return true;
75    }
76
77    /**
78     * Get a message from i18n
79     *
80     * @param string $key Message name
81     * @param string $fallback Default message if the message cache can't be
82     *                  called by the exception
83     * @param mixed ...$params To pass to wfMessage()
84     * @return string Message with arguments replaced
85     */
86    public function msg( $key, $fallback, ...$params ) {
87        // NOTE: Keep logic in sync with MWExceptionRenderer::msg.
88        $res = false;
89        if ( $this->useMessageCache() ) {
90            try {
91                $res = wfMessage( $key, ...$params )->text();
92            } catch ( Exception $e ) {
93            }
94        }
95        if ( $res === false ) {
96            // Fallback to static message text and generic sitename.
97            // Avoid live config as this must work before Setup/MediaWikiServices finish.
98            $res = wfMsgReplaceArgs( $fallback, $params );
99            $res = strtr( $res, [
100                '{{SITENAME}}' => 'MediaWiki',
101            ] );
102        }
103        return $res;
104    }
105
106    /**
107     * Format an HTML message for the current exception object.
108     *
109     * @deprecated since 1.42 Provide the error message when constructing the Exception instead.
110     *   If you need a whole custom error page, use ErrorPageError instead.
111     * @return string HTML to output
112     */
113    public function getHTML() {
114        wfDeprecated( __METHOD__, '1.42' );
115        if ( MWExceptionRenderer::shouldShowExceptionDetails() ) {
116            return '<p>' . nl2br( htmlspecialchars( MWExceptionHandler::getLogMessage( $this ) ) ) .
117            '</p><p>Backtrace:</p><p>' .
118            nl2br( htmlspecialchars( MWExceptionHandler::getRedactedTraceAsString( $this ) ) ) .
119            "</p>\n";
120        } else {
121            $logId = WebRequest::getRequestId();
122            $type = static::class;
123            return Html::errorBox(
124            htmlspecialchars(
125                '[' . $logId . '] ' .
126                gmdate( 'Y-m-d H:i:s' ) . ": " .
127                $this->msg( "internalerror-fatal-exception",
128                    "Fatal exception of type $1",
129                    $type,
130                    $logId,
131                    MWExceptionHandler::getURL()
132                )
133            ) ) .
134            "<!-- Set \$wgShowExceptionDetails = true; " .
135            "at the bottom of LocalSettings.php to show detailed " .
136            "debugging information. -->";
137        }
138    }
139
140    /**
141     * Format plain text message for the current exception object.
142     *
143     * @deprecated since 1.42 Provide the error message when constructing the Exception instead.
144     *   If you need a whole custom error page, use ErrorPageError instead.
145     * @return string
146     */
147    public function getText() {
148        wfDeprecated( __METHOD__, '1.42' );
149        if ( MWExceptionRenderer::shouldShowExceptionDetails() ) {
150            return MWExceptionHandler::getLogMessage( $this ) .
151            "\nBacktrace:\n" . MWExceptionHandler::getRedactedTraceAsString( $this ) . "\n";
152        } else {
153            return "Set \$wgShowExceptionDetails = true; " .
154            "in LocalSettings.php to show detailed debugging information.\n";
155        }
156    }
157
158    /**
159     * Return the title of the page when reporting this error in a HTTP response.
160     *
161     * @deprecated since 1.42 Provide the error message when constructing the Exception instead.
162     *   If you need a whole custom error page, use ErrorPageError instead.
163     * @return string
164     */
165    public function getPageTitle() {
166        wfDeprecated( __METHOD__, '1.42' );
167        return $this->msg( 'internalerror', 'Internal error' );
168    }
169
170    /**
171     * Output the exception report using HTML.
172     * @deprecated since 1.42 Provide the error message when constructing the Exception instead.
173     *   If you need a whole custom error page, use ErrorPageError instead.
174     */
175    public function reportHTML() {
176        wfDeprecated( __METHOD__, '1.42' );
177        global $wgOut;
178
179        if ( $this->useOutputPage() ) {
180            $wgOut->prepareErrorPage();
181            $wgOut->setPageTitle( $this->getPageTitle() );
182            // Manually set the html title, since sometimes
183            // {{SITENAME}} does not get replaced for exceptions
184            // happening inside message rendering.
185            $wgOut->setHTMLTitle(
186                $this->msg( 'pagetitle', '$1 - MediaWiki', $this->getPageTitle() )
187            );
188
189            $wgOut->addHTML( $this->getHTML() );
190            // Content-Type is set by OutputPage::output
191            $wgOut->output();
192        } else {
193            self::header( 'Content-Type: text/html; charset=UTF-8' );
194            echo "<!DOCTYPE html>\n" .
195                '<html><head>' .
196                // Mimic OutputPage::setPageTitle behaviour
197                '<title>' .
198                htmlspecialchars( $this->msg( 'pagetitle', '$1 - MediaWiki', $this->getPageTitle() ) ) .
199                '</title>' .
200                '<style>body { font-family: sans-serif; margin: 0; padding: 0.5em 2em; }</style>' .
201                "</head><body>\n";
202
203            echo $this->getHTML();
204
205            echo "</body></html>\n";
206        }
207    }
208
209    /**
210     * Output a report about the exception and takes care of formatting.
211     * It will be either HTML or plain text based on isCommandLine().
212     *
213     * @stable to override
214     */
215    public function report() {
216        if ( defined( 'MW_API' ) ) {
217            self::header( 'MediaWiki-API-Error: internal_api_error_' . static::class );
218        }
219
220        if ( self::isCommandLine() ) {
221            $message = $this->getText();
222            $this->writeToCommandLine( $message );
223        } else {
224            self::statusHeader( 500 );
225            $this->reportHTML();
226        }
227    }
228
229    /**
230     * @internal
231     */
232    final public function hasOverriddenHandler(): bool {
233        // No deprecation warning - report() is not deprecated, only the other methods
234        if ( MWDebug::detectDeprecatedOverride( $this, __CLASS__, 'report' ) ) {
235            return true;
236        }
237
238        // Check them all here to avoid short-circuiting and report all deprecations,
239        // even if the function is not called in this request
240        $detectedOverrides = [
241            'getHTML' => MWDebug::detectDeprecatedOverride( $this, __CLASS__, 'getHTML', '1.42' ),
242            'getText' => MWDebug::detectDeprecatedOverride( $this, __CLASS__, 'getText', '1.42' ),
243            'getPageTitle' => MWDebug::detectDeprecatedOverride( $this, __CLASS__, 'getPageTitle', '1.42' ),
244            'reportHTML' => MWDebug::detectDeprecatedOverride( $this, __CLASS__, 'reportHTML', '1.42' ),
245        ];
246
247        return (bool)array_filter( $detectedOverrides );
248    }
249
250    /**
251     * Write a message to stderr falling back to stdout if stderr unavailable
252     *
253     * @param string $message
254     * @suppress SecurityCheck-XSS
255     */
256    private function writeToCommandLine( $message ) {
257        // T17602: STDERR may not be available
258        if ( !defined( 'MW_PHPUNIT_TEST' ) && defined( 'STDERR' ) ) {
259            fwrite( STDERR, $message );
260        } else {
261            echo $message;
262        }
263    }
264
265    /**
266     * Check whether we are in command line mode or not to report the exception
267     * in the correct format.
268     *
269     * @return bool
270     */
271    public static function isCommandLine() {
272        return MW_ENTRY_POINT === 'cli';
273    }
274
275    /**
276     * Send a header, if we haven't already sent them. We shouldn't,
277     * but sometimes we might in a weird case like Export
278     * @param string $header
279     */
280    private static function header( $header ) {
281        if ( !headers_sent() ) {
282            header( $header );
283        }
284    }
285
286    private static function statusHeader( $code ) {
287        if ( !headers_sent() ) {
288            HttpStatus::header( $code );
289        }
290    }
291}