MediaWiki  REL1_22
Exception.php
Go to the documentation of this file.
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 }