MediaWiki
REL1_22
|
00001 <?php 00032 class MWException extends Exception { 00033 00039 function useOutputPage() { 00040 return $this->useMessageCache() && 00041 !empty( $GLOBALS['wgFullyInitialised'] ) && 00042 !empty( $GLOBALS['wgOut'] ) && 00043 !empty( $GLOBALS['wgTitle'] ); 00044 } 00045 00052 function isLoggable() { 00053 return true; 00054 } 00055 00061 function useMessageCache() { 00062 global $wgLang; 00063 00064 foreach ( $this->getTrace() as $frame ) { 00065 if ( isset( $frame['class'] ) && $frame['class'] === 'LocalisationCache' ) { 00066 return false; 00067 } 00068 } 00069 00070 return $wgLang instanceof Language; 00071 } 00072 00080 function runHooks( $name, $args = array() ) { 00081 global $wgExceptionHooks; 00082 00083 if ( !isset( $wgExceptionHooks ) || !is_array( $wgExceptionHooks ) ) { 00084 return null; // Just silently ignore 00085 } 00086 00087 if ( !array_key_exists( $name, $wgExceptionHooks ) || !is_array( $wgExceptionHooks[$name] ) ) { 00088 return null; 00089 } 00090 00091 $hooks = $wgExceptionHooks[$name]; 00092 $callargs = array_merge( array( $this ), $args ); 00093 00094 foreach ( $hooks as $hook ) { 00095 if ( is_string( $hook ) || ( is_array( $hook ) && count( $hook ) >= 2 && is_string( $hook[0] ) ) ) { // 'function' or array( 'class', hook' ) 00096 $result = call_user_func_array( $hook, $callargs ); 00097 } else { 00098 $result = null; 00099 } 00100 00101 if ( is_string( $result ) ) { 00102 return $result; 00103 } 00104 } 00105 return null; 00106 } 00107 00117 function msg( $key, $fallback /*[, params...] */ ) { 00118 $args = array_slice( func_get_args(), 2 ); 00119 00120 if ( $this->useMessageCache() ) { 00121 return wfMessage( $key, $args )->plain(); 00122 } else { 00123 return wfMsgReplaceArgs( $fallback, $args ); 00124 } 00125 } 00126 00134 function getHTML() { 00135 global $wgShowExceptionDetails; 00136 00137 if ( $wgShowExceptionDetails ) { 00138 return '<p>' . nl2br( htmlspecialchars( MWExceptionHandler::getLogMessage( $this ) ) ) . 00139 '</p><p>Backtrace:</p><p>' . nl2br( htmlspecialchars( MWExceptionHandler::getRedactedTraceAsString( $this ) ) ) . 00140 "</p>\n"; 00141 } else { 00142 return "<div class=\"errorbox\">" . 00143 '[' . MWExceptionHandler::getLogId( $this ) . '] ' . 00144 gmdate( 'Y-m-d H:i:s' ) . 00145 ": Fatal exception of type " . get_class( $this ) . "</div>\n" . 00146 "<!-- Set \$wgShowExceptionDetails = true; " . 00147 "at the bottom of LocalSettings.php to show detailed " . 00148 "debugging information. -->"; 00149 } 00150 } 00151 00159 function getText() { 00160 global $wgShowExceptionDetails; 00161 00162 if ( $wgShowExceptionDetails ) { 00163 return MWExceptionHandler::getLogMessage( $this ) . 00164 "\nBacktrace:\n" . MWExceptionHandler::getRedactedTraceAsString( $this ) . "\n"; 00165 } else { 00166 return "Set \$wgShowExceptionDetails = true; " . 00167 "in LocalSettings.php to show detailed debugging information.\n"; 00168 } 00169 } 00170 00176 function getPageTitle() { 00177 return $this->msg( 'internalerror', "Internal error" ); 00178 } 00179 00187 function getLogId() { 00188 wfDeprecated( __METHOD__, '1.22' ); 00189 return MWExceptionHandler::getLogId( $this ); 00190 } 00191 00200 function getLogMessage() { 00201 wfDeprecated( __METHOD__, '1.22' ); 00202 return MWExceptionHandler::getLogMessage( $this ); 00203 } 00204 00208 function reportHTML() { 00209 global $wgOut; 00210 if ( $this->useOutputPage() ) { 00211 $wgOut->prepareErrorPage( $this->getPageTitle() ); 00212 00213 $hookResult = $this->runHooks( get_class( $this ) ); 00214 if ( $hookResult ) { 00215 $wgOut->addHTML( $hookResult ); 00216 } else { 00217 $wgOut->addHTML( $this->getHTML() ); 00218 } 00219 00220 $wgOut->output(); 00221 } else { 00222 header( "Content-Type: text/html; charset=utf-8" ); 00223 echo "<!doctype html>\n" . 00224 '<html><head>' . 00225 '<title>' . htmlspecialchars( $this->getPageTitle() ) . '</title>' . 00226 "</head><body>\n"; 00227 00228 $hookResult = $this->runHooks( get_class( $this ) . "Raw" ); 00229 if ( $hookResult ) { 00230 echo $hookResult; 00231 } else { 00232 echo $this->getHTML(); 00233 } 00234 00235 echo "</body></html>\n"; 00236 } 00237 } 00238 00243 function report() { 00244 global $wgMimeType; 00245 00246 MWExceptionHandler::logException( $this ); 00247 00248 if ( defined( 'MW_API' ) ) { 00249 // Unhandled API exception, we can't be sure that format printer is alive 00250 header( 'MediaWiki-API-Error: internal_api_error_' . get_class( $this ) ); 00251 wfHttpError( 500, 'Internal Server Error', $this->getText() ); 00252 } elseif ( self::isCommandLine() ) { 00253 MWExceptionHandler::printError( $this->getText() ); 00254 } else { 00255 header( "HTTP/1.1 500 MediaWiki exception" ); 00256 header( "Status: 500 MediaWiki exception", true ); 00257 header( "Content-Type: $wgMimeType; charset=utf-8", true ); 00258 00259 $this->reportHTML(); 00260 } 00261 } 00262 00269 static function isCommandLine() { 00270 return !empty( $GLOBALS['wgCommandLineMode'] ); 00271 } 00272 } 00273 00281 class FatalError extends MWException { 00282 00286 function getHTML() { 00287 return $this->getMessage(); 00288 } 00289 00293 function getText() { 00294 return $this->getMessage(); 00295 } 00296 } 00297 00304 class ErrorPageError extends MWException { 00305 public $title, $msg, $params; 00306 00314 function __construct( $title, $msg, $params = null ) { 00315 $this->title = $title; 00316 $this->msg = $msg; 00317 $this->params = $params; 00318 00319 // Bug 44111: Messages in the log files should be in English and not 00320 // customized by the local wiki. So get the default English version for 00321 // passing to the parent constructor. Our overridden report() below 00322 // makes sure that the page shown to the user is not forced to English. 00323 if ( $msg instanceof Message ) { 00324 $enMsg = clone( $msg ); 00325 } else { 00326 $enMsg = wfMessage( $msg, $params ); 00327 } 00328 $enMsg->inLanguage( 'en' )->useDatabase( false ); 00329 parent::__construct( $enMsg->text() ); 00330 } 00331 00332 function report() { 00333 global $wgOut; 00334 00335 $wgOut->showErrorPage( $this->title, $this->msg, $this->params ); 00336 $wgOut->output(); 00337 } 00338 } 00339 00348 class BadTitleError extends ErrorPageError { 00353 function __construct( $msg = 'badtitletext', $params = null ) { 00354 parent::__construct( 'badtitle', $msg, $params ); 00355 } 00356 00361 function report() { 00362 global $wgOut; 00363 00364 // bug 33646: a badtitle error page need to return an error code 00365 // to let mobile browser now that it is not a normal page. 00366 $wgOut->setStatusCode( 400 ); 00367 parent::report(); 00368 } 00369 00370 } 00371 00379 class PermissionsError extends ErrorPageError { 00380 public $permission, $errors; 00381 00382 function __construct( $permission, $errors = array() ) { 00383 global $wgLang; 00384 00385 $this->permission = $permission; 00386 00387 if ( !count( $errors ) ) { 00388 $groups = array_map( 00389 array( 'User', 'makeGroupLinkWiki' ), 00390 User::getGroupsWithPermission( $this->permission ) 00391 ); 00392 00393 if ( $groups ) { 00394 $errors[] = array( 'badaccess-groups', $wgLang->commaList( $groups ), count( $groups ) ); 00395 } else { 00396 $errors[] = array( 'badaccess-group0' ); 00397 } 00398 } 00399 00400 $this->errors = $errors; 00401 } 00402 00403 function report() { 00404 global $wgOut; 00405 00406 $wgOut->showPermissionsErrorPage( $this->errors, $this->permission ); 00407 $wgOut->output(); 00408 } 00409 } 00410 00418 class ReadOnlyError extends ErrorPageError { 00419 public function __construct() { 00420 parent::__construct( 00421 'readonly', 00422 'readonlytext', 00423 wfReadOnlyReason() 00424 ); 00425 } 00426 } 00427 00434 class ThrottledError extends ErrorPageError { 00435 public function __construct() { 00436 parent::__construct( 00437 'actionthrottled', 00438 'actionthrottledtext' 00439 ); 00440 } 00441 00442 public function report() { 00443 global $wgOut; 00444 $wgOut->setStatusCode( 503 ); 00445 parent::report(); 00446 } 00447 } 00448 00455 class UserBlockedError extends ErrorPageError { 00456 public function __construct( Block $block ) { 00457 // @todo FIXME: Implement a more proper way to get context here. 00458 $params = $block->getPermissionsError( RequestContext::getMain() ); 00459 parent::__construct( 'blockedtitle', array_shift( $params ), $params ); 00460 } 00461 } 00462 00490 class UserNotLoggedIn extends ErrorPageError { 00491 00500 public function __construct( 00501 $reasonMsg = 'exception-nologin-text', 00502 $titleMsg = 'exception-nologin', 00503 $params = null 00504 ) { 00505 parent::__construct( $titleMsg, $reasonMsg, $params ); 00506 } 00507 } 00508 00516 class HttpError extends MWException { 00517 private $httpCode, $header, $content; 00518 00526 public function __construct( $httpCode, $content, $header = null ) { 00527 parent::__construct( $content ); 00528 $this->httpCode = (int)$httpCode; 00529 $this->header = $header; 00530 $this->content = $content; 00531 } 00532 00538 public function getStatusCode() { 00539 return $this->httpCode; 00540 } 00541 00547 public function report() { 00548 $httpMessage = HttpStatus::getMessage( $this->httpCode ); 00549 00550 header( "Status: {$this->httpCode} {$httpMessage}", true, $this->httpCode ); 00551 header( 'Content-type: text/html; charset=utf-8' ); 00552 00553 print $this->getHTML(); 00554 } 00555 00562 public function getHTML() { 00563 if ( $this->header === null ) { 00564 $header = HttpStatus::getMessage( $this->httpCode ); 00565 } elseif ( $this->header instanceof Message ) { 00566 $header = $this->header->escaped(); 00567 } else { 00568 $header = htmlspecialchars( $this->header ); 00569 } 00570 00571 if ( $this->content instanceof Message ) { 00572 $content = $this->content->escaped(); 00573 } else { 00574 $content = htmlspecialchars( $this->content ); 00575 } 00576 00577 return "<!DOCTYPE html>\n" . 00578 "<html><head><title>$header</title></head>\n" . 00579 "<body><h1>$header</h1><p>$content</p></body></html>\n"; 00580 } 00581 } 00582 00587 class MWExceptionHandler { 00591 public static function installHandler() { 00592 set_exception_handler( array( 'MWExceptionHandler', 'handle' ) ); 00593 } 00594 00598 protected static function report( Exception $e ) { 00599 global $wgShowExceptionDetails; 00600 00601 $cmdLine = MWException::isCommandLine(); 00602 00603 if ( $e instanceof MWException ) { 00604 try { 00605 // Try and show the exception prettily, with the normal skin infrastructure 00606 $e->report(); 00607 } catch ( Exception $e2 ) { 00608 // Exception occurred from within exception handler 00609 // Show a simpler error message for the original exception, 00610 // don't try to invoke report() 00611 $message = "MediaWiki internal error.\n\n"; 00612 00613 if ( $wgShowExceptionDetails ) { 00614 $message .= 'Original exception: ' . self::getLogMessage( $e ) . 00615 "\nBacktrace:\n" . self::getRedactedTraceAsString( $e ) . 00616 "\n\nException caught inside exception handler: " . self::getLogMessage( $e2 ) . 00617 "\nBacktrace:\n" . self::getRedactedTraceAsString( $e2 ); 00618 } else { 00619 $message .= "Exception caught inside exception handler.\n\n" . 00620 "Set \$wgShowExceptionDetails = true; at the bottom of LocalSettings.php " . 00621 "to show detailed debugging information."; 00622 } 00623 00624 $message .= "\n"; 00625 00626 if ( $cmdLine ) { 00627 self::printError( $message ); 00628 } else { 00629 echo nl2br( htmlspecialchars( $message ) ) . "\n"; 00630 } 00631 } 00632 } else { 00633 $message = "Unexpected non-MediaWiki exception encountered, of type \"" . get_class( $e ) . "\""; 00634 00635 if ( $wgShowExceptionDetails ) { 00636 $message .= "\n" . MWExceptionHandler::getLogMessage( $e ) . "\nBacktrace:\n" . 00637 self::getRedactedTraceAsString( $e ) . "\n"; 00638 } 00639 00640 if ( $cmdLine ) { 00641 self::printError( $message ); 00642 } else { 00643 echo nl2br( htmlspecialchars( $message ) ) . "\n"; 00644 } 00645 } 00646 } 00647 00654 public static function printError( $message ) { 00655 # NOTE: STDERR may not be available, especially if php-cgi is used from the command line (bug #15602). 00656 # Try to produce meaningful output anyway. Using echo may corrupt output to STDOUT though. 00657 if ( defined( 'STDERR' ) ) { 00658 fwrite( STDERR, $message ); 00659 } else { 00660 echo $message; 00661 } 00662 } 00663 00675 public static function handle( $e ) { 00676 global $wgFullyInitialised; 00677 00678 self::report( $e ); 00679 00680 // Final cleanup 00681 if ( $wgFullyInitialised ) { 00682 try { 00683 // uses $wgRequest, hence the $wgFullyInitialised condition 00684 wfLogProfilingData(); 00685 } catch ( Exception $e ) { 00686 } 00687 } 00688 00689 // Exit value should be nonzero for the benefit of shell jobs 00690 exit( 1 ); 00691 } 00692 00702 public static function getRedactedTraceAsString( Exception $e ) { 00703 $text = ''; 00704 00705 foreach ( self::getRedactedTrace( $e ) as $level => $frame ) { 00706 if ( isset( $frame['file'] ) && isset( $frame['line'] ) ) { 00707 $text .= "#{$level} {$frame['file']}({$frame['line']}): "; 00708 } else { 00709 // 'file' and 'line' are unset for calls via call_user_func (bug 55634) 00710 // This matches behaviour of Exception::getTraceAsString to instead 00711 // display "[internal function]". 00712 $text .= "#{$level} [internal function]: "; 00713 } 00714 00715 if ( isset( $frame['class'] ) ) { 00716 $text .= $frame['class'] . $frame['type'] . $frame['function']; 00717 } else { 00718 $text .= $frame['function']; 00719 } 00720 00721 if ( isset( $frame['args'] ) ) { 00722 $text .= '(' . implode( ', ', $frame['args'] ) . ")\n"; 00723 } else { 00724 $text .= "()\n"; 00725 } 00726 } 00727 00728 $level = $level + 1; 00729 $text .= "#{$level} {main}"; 00730 00731 return $text; 00732 } 00733 00745 public static function getRedactedTrace( Exception $e ) { 00746 return array_map( function ( $frame ) { 00747 if ( isset( $frame['args'] ) ) { 00748 $frame['args'] = array_map( function ( $arg ) { 00749 return is_object( $arg ) ? get_class( $arg ) : gettype( $arg ); 00750 }, $frame['args'] ); 00751 } 00752 return $frame; 00753 }, $e->getTrace() ); 00754 } 00755 00756 00767 public static function getLogId( Exception $e ) { 00768 if ( !isset( $e->_mwLogId ) ) { 00769 $e->_mwLogId = wfRandomString( 8 ); 00770 } 00771 return $e->_mwLogId; 00772 } 00773 00782 public static function getLogMessage( Exception $e ) { 00783 global $wgRequest; 00784 00785 $id = self::getLogId( $e ); 00786 $file = $e->getFile(); 00787 $line = $e->getLine(); 00788 $message = $e->getMessage(); 00789 00790 if ( isset( $wgRequest ) && !$wgRequest instanceof FauxRequest ) { 00791 $url = $wgRequest->getRequestURL(); 00792 if ( !$url ) { 00793 $url = '[no URL]'; 00794 } 00795 } else { 00796 $url = '[no req]'; 00797 } 00798 00799 return "[$id] $url Exception from line $line of $file: $message"; 00800 } 00801 00811 public static function logException( Exception $e ) { 00812 global $wgLogExceptionBacktrace; 00813 00814 if ( !( $e instanceof MWException ) || $e->isLoggable() ) { 00815 $log = self::getLogMessage( $e ); 00816 if ( $wgLogExceptionBacktrace ) { 00817 wfDebugLog( 'exception', $log . "\n" . $e->getTraceAsString() . "\n" ); 00818 } else { 00819 wfDebugLog( 'exception', $log ); 00820 } 00821 } 00822 } 00823 00824 }