1<?php 2/** 3 * This file contains the error handler application component. 4 * 5 * @author Qiang Xue <qiang.xue@gmail.com> 6 * @link http://www.yiiframework.com/ 7 * @copyright 2008-2013 Yii Software LLC 8 * @license http://www.yiiframework.com/license/ 9 */ 10 11Yii::import('CHtml',true); 12 13/** 14 * CErrorHandler handles uncaught PHP errors and exceptions. 15 * 16 * It displays these errors using appropriate views based on the 17 * nature of the error and the mode the application runs at. 18 * It also chooses the most preferred language for displaying the error. 19 * 20 * CErrorHandler uses two sets of views: 21 * <ul> 22 * <li>development views, named as <code>exception.php</code>; 23 * <li>production views, named as <code>error<StatusCode>.php</code>; 24 * </ul> 25 * where <StatusCode> stands for the HTTP error code (e.g. error500.php). 26 * Localized views are named similarly but located under a subdirectory 27 * whose name is the language code (e.g. zh_cn/error500.php). 28 * 29 * Development views are displayed when the application is in debug mode 30 * (i.e. YII_DEBUG is defined as true). Detailed error information with source code 31 * are displayed in these views. Production views are meant to be shown 32 * to end-users and are used when the application is in production mode. 33 * For security reasons, they only display the error message without any 34 * sensitive information. 35 * 36 * CErrorHandler looks for the view templates from the following locations in order: 37 * <ol> 38 * <li><code>themes/ThemeName/views/system</code>: when a theme is active.</li> 39 * <li><code>protected/views/system</code></li> 40 * <li><code>framework/views</code></li> 41 * </ol> 42 * If the view is not found in a directory, it will be looked for in the next directory. 43 * 44 * The property {@link maxSourceLines} can be changed to specify the number 45 * of source code lines to be displayed in development views. 46 * 47 * CErrorHandler is a core application component that can be accessed via 48 * {@link CApplication::getErrorHandler()}. 49 * 50 * @property array $error The error details. Null if there is no error. 51 * @property Exception|null $exception exception instance. Null if there is no exception. 52 * 53 * @author Qiang Xue <qiang.xue@gmail.com> 54 * @package system.base 55 * @since 1.0 56 */ 57class CErrorHandler extends CApplicationComponent 58{ 59 /** 60 * @var integer maximum number of source code lines to be displayed. Defaults to 25. 61 */ 62 public $maxSourceLines=25; 63 64 /** 65 * @var integer maximum number of trace source code lines to be displayed. Defaults to 10. 66 * @since 1.1.6 67 */ 68 public $maxTraceSourceLines = 10; 69 70 /** 71 * @var string the application administrator information (could be a name or email link). It is displayed in error pages to end users. Defaults to 'the webmaster'. 72 */ 73 public $adminInfo='the webmaster'; 74 /** 75 * @var boolean whether to discard any existing page output before error display. Defaults to true. 76 */ 77 public $discardOutput=true; 78 /** 79 * @var string the route (eg 'site/error') to the controller action that will be used to display external errors. 80 * Inside the action, it can retrieve the error information by Yii::app()->errorHandler->error. 81 * This property defaults to null, meaning CErrorHandler will handle the error display. 82 */ 83 public $errorAction; 84 85 private $_error; 86 private $_exception; 87 88 /** 89 * Handles the exception/error event. 90 * This method is invoked by the application whenever it captures 91 * an exception or PHP error. 92 * @param CEvent $event the event containing the exception/error information 93 */ 94 public function handle($event) 95 { 96 // set event as handled to prevent it from being handled by other event handlers 97 $event->handled=true; 98 99 if($this->discardOutput) 100 { 101 $gzHandler=false; 102 foreach(ob_list_handlers() as $h) 103 { 104 if(strpos($h,'gzhandler')!==false) 105 $gzHandler=true; 106 } 107 // the following manual level counting is to deal with zlib.output_compression set to On 108 // for an output buffer created by zlib.output_compression set to On ob_end_clean will fail 109 for($level=ob_get_level();$level>0;--$level) 110 { 111 if(!@ob_end_clean()) 112 ob_clean(); 113 } 114 // reset headers in case there was an ob_start("ob_gzhandler") before 115 if($gzHandler && !headers_sent() && ob_list_handlers()===array()) 116 { 117 if(function_exists('header_remove')) // php >= 5.3 118 { 119 header_remove('Vary'); 120 header_remove('Content-Encoding'); 121 } 122 else 123 { 124 header('Vary:'); 125 header('Content-Encoding:'); 126 } 127 } 128 } 129 130 if($event instanceof CExceptionEvent) 131 $this->handleException($event->exception); 132 else // CErrorEvent 133 $this->handleError($event); 134 } 135 136 /** 137 * Returns the details about the error that is currently being handled. 138 * The error is returned in terms of an array, with the following information: 139 * <ul> 140 * <li>code - the HTTP status code (e.g. 403, 500)</li> 141 * <li>type - the error type (e.g. 'CHttpException', 'PHP Error')</li> 142 * <li>message - the error message</li> 143 * <li>file - the name of the PHP script file where the error occurs</li> 144 * <li>line - the line number of the code where the error occurs</li> 145 * <li>trace - the call stack of the error</li> 146 * <li>source - the context source code where the error occurs</li> 147 * </ul> 148 * @return array the error details. Null if there is no error. 149 */ 150 public function getError() 151 { 152 return $this->_error; 153 } 154 155 /** 156 * Returns the instance of the exception that is currently being handled. 157 * @return Exception|null exception instance. Null if there is no exception. 158 */ 159 public function getException() 160 { 161 return $this->_exception; 162 } 163 164 /** 165 * Handles the exception. 166 * @param Exception $exception the exception captured 167 */ 168 protected function handleException($exception) 169 { 170 $app=Yii::app(); 171 if($app instanceof CWebApplication) 172 { 173 if(($trace=$this->getExactTrace($exception))===null) 174 { 175 $fileName=$exception->getFile(); 176 $errorLine=$exception->getLine(); 177 } 178 else 179 { 180 $fileName=$trace['file']; 181 $errorLine=$trace['line']; 182 } 183 184 $trace = $exception->getTrace(); 185 186 foreach($trace as $i=>$t) 187 { 188 if(!isset($t['file'])) 189 $trace[$i]['file']='unknown'; 190 191 if(!isset($t['line'])) 192 $trace[$i]['line']=0; 193 194 if(!isset($t['function'])) 195 $trace[$i]['function']='unknown'; 196 197 unset($trace[$i]['object']); 198 } 199 200 $this->_exception=$exception; 201 $this->_error=$data=array( 202 'code'=>($exception instanceof CHttpException)?$exception->statusCode:500, 203 'type'=>get_class($exception), 204 'errorCode'=>$exception->getCode(), 205 'message'=>$exception->getMessage(), 206 'file'=>$fileName, 207 'line'=>$errorLine, 208 'trace'=>$exception->getTraceAsString(), 209 'traces'=>$trace, 210 ); 211 212 if(!headers_sent()) 213 { 214 $httpVersion=Yii::app()->request->getHttpVersion(); 215 header("HTTP/$httpVersion {$data['code']} ".$this->getHttpHeader($data['code'], get_class($exception))); 216 } 217 218 $this->renderException(); 219 } 220 else 221 $app->displayException($exception); 222 } 223 224 /** 225 * Handles the PHP error. 226 * @param CErrorEvent $event the PHP error event 227 */ 228 protected function handleError($event) 229 { 230 $trace=debug_backtrace(); 231 // skip the first 3 stacks as they do not tell the error position 232 if(count($trace)>3) 233 $trace=array_slice($trace,3); 234 $traceString=''; 235 foreach($trace as $i=>$t) 236 { 237 if(!isset($t['file'])) 238 $trace[$i]['file']='unknown'; 239 240 if(!isset($t['line'])) 241 $trace[$i]['line']=0; 242 243 if(!isset($t['function'])) 244 $trace[$i]['function']='unknown'; 245 246 $traceString.="#$i {$trace[$i]['file']}({$trace[$i]['line']}): "; 247 if(isset($t['object']) && is_object($t['object'])) 248 $traceString.=get_class($t['object']).'->'; 249 $traceString.="{$trace[$i]['function']}()\n"; 250 251 unset($trace[$i]['object']); 252 } 253 254 $app=Yii::app(); 255 if($app instanceof CWebApplication) 256 { 257 switch($event->code) 258 { 259 case E_WARNING: 260 $type = 'PHP warning'; 261 break; 262 case E_NOTICE: 263 $type = 'PHP notice'; 264 break; 265 case E_USER_ERROR: 266 $type = 'User error'; 267 break; 268 case E_USER_WARNING: 269 $type = 'User warning'; 270 break; 271 case E_USER_NOTICE: 272 $type = 'User notice'; 273 break; 274 case E_RECOVERABLE_ERROR: 275 $type = 'Recoverable error'; 276 break; 277 default: 278 $type = 'PHP error'; 279 } 280 $this->_exception=null; 281 $this->_error=array( 282 'code'=>500, 283 'type'=>$type, 284 'message'=>$event->message, 285 'file'=>$event->file, 286 'line'=>$event->line, 287 'trace'=>$traceString, 288 'traces'=>$trace, 289 ); 290 if(!headers_sent()) 291 { 292 $httpVersion=Yii::app()->request->getHttpVersion(); 293 header("HTTP/$httpVersion 500 Internal Server Error"); 294 } 295 296 $this->renderError(); 297 } 298 else 299 $app->displayError($event->code,$event->message,$event->file,$event->line); 300 } 301 302 /** 303 * whether the current request is an AJAX (XMLHttpRequest) request. 304 * @return boolean whether the current request is an AJAX request. 305 */ 306 protected function isAjaxRequest() 307 { 308 return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH']==='XMLHttpRequest'; 309 } 310 311 /** 312 * Returns the exact trace where the problem occurs. 313 * @param Exception $exception the uncaught exception 314 * @return array the exact trace where the problem occurs 315 */ 316 protected function getExactTrace($exception) 317 { 318 $traces=$exception->getTrace(); 319 320 foreach($traces as $trace) 321 { 322 // property access exception 323 if(isset($trace['function']) && ($trace['function']==='__get' || $trace['function']==='__set')) 324 return $trace; 325 } 326 return null; 327 } 328 329 /** 330 * Renders the view. 331 * @param string $view the view name (file name without extension). 332 * See {@link getViewFile} for how a view file is located given its name. 333 * @param array $data data to be passed to the view 334 */ 335 protected function render($view,$data) 336 { 337 $data['version']=$this->getVersionInfo(); 338 $data['time']=time(); 339 $data['admin']=$this->adminInfo; 340 include($this->getViewFile($view,$data['code'])); 341 } 342 343 /** 344 * Renders the exception information. 345 * This method will display information from current {@link error} value. 346 */ 347 protected function renderException() 348 { 349 $exception=$this->getException(); 350 if($exception instanceof CHttpException || !YII_DEBUG) 351 $this->renderError(); 352 else 353 { 354 if($this->isAjaxRequest()) 355 Yii::app()->displayException($exception); 356 else 357 $this->render('exception',$this->getError()); 358 } 359 } 360 361 /** 362 * Renders the current error information. 363 * This method will display information from current {@link error} value. 364 */ 365 protected function renderError() 366 { 367 if($this->errorAction!==null) 368 Yii::app()->runController($this->errorAction); 369 else 370 { 371 $data=$this->getError(); 372 if($this->isAjaxRequest()) 373 Yii::app()->displayError($data['code'],$data['message'],$data['file'],$data['line']); 374 elseif(YII_DEBUG) 375 $this->render('exception',$data); 376 else 377 $this->render('error',$data); 378 } 379 } 380 381 /** 382 * Determines which view file should be used. 383 * @param string $view view name (either 'exception' or 'error') 384 * @param integer $code HTTP status code 385 * @return string view file path 386 */ 387 protected function getViewFile($view,$code) 388 { 389 $viewPaths=array( 390 Yii::app()->getTheme()===null ? null : Yii::app()->getTheme()->getSystemViewPath(), 391 Yii::app() instanceof CWebApplication ? Yii::app()->getSystemViewPath() : null, 392 YII_PATH.DIRECTORY_SEPARATOR.'views', 393 ); 394 395 foreach($viewPaths as $i=>$viewPath) 396 { 397 if($viewPath!==null) 398 { 399 $viewFile=$this->getViewFileInternal($viewPath,$view,$code,$i===2?'en_us':null); 400 if(is_file($viewFile)) 401 return $viewFile; 402 } 403 } 404 } 405 406 /** 407 * Looks for the view under the specified directory. 408 * @param string $viewPath the directory containing the views 409 * @param string $view view name (either 'exception' or 'error') 410 * @param integer $code HTTP status code 411 * @param string $srcLanguage the language that the view file is in 412 * @return string view file path 413 */ 414 protected function getViewFileInternal($viewPath,$view,$code,$srcLanguage=null) 415 { 416 $app=Yii::app(); 417 if($view==='error') 418 { 419 $viewFile=$app->findLocalizedFile($viewPath.DIRECTORY_SEPARATOR."error{$code}.php",$srcLanguage); 420 if(!is_file($viewFile)) 421 $viewFile=$app->findLocalizedFile($viewPath.DIRECTORY_SEPARATOR.'error.php',$srcLanguage); 422 } 423 else 424 $viewFile=$viewPath.DIRECTORY_SEPARATOR."exception.php"; 425 return $viewFile; 426 } 427 428 /** 429 * Returns server version information. 430 * If the application is in production mode, empty string is returned. 431 * @return string server version information. Empty if in production mode. 432 */ 433 protected function getVersionInfo() 434 { 435 if(YII_DEBUG) 436 { 437 $version='<a href="http://www.yiiframework.com/">Yii Framework</a>/'.Yii::getVersion(); 438 if(isset($_SERVER['SERVER_SOFTWARE'])) 439 $version=$_SERVER['SERVER_SOFTWARE'].' '.$version; 440 } 441 else 442 $version=''; 443 return $version; 444 } 445 446 /** 447 * Converts arguments array to its string representation 448 * 449 * @param array $args arguments array to be converted 450 * @return string string representation of the arguments array 451 */ 452 protected function argumentsToString($args) 453 { 454 $count=0; 455 456 $isAssoc=$args!==array_values($args); 457 458 foreach($args as $key => $value) 459 { 460 $count++; 461 if($count>=5) 462 { 463 if($count>5) 464 unset($args[$key]); 465 else 466 $args[$key]='...'; 467 continue; 468 } 469 470 if(is_object($value)) 471 $args[$key] = get_class($value); 472 elseif(is_bool($value)) 473 $args[$key] = $value ? 'true' : 'false'; 474 elseif(is_string($value)) 475 { 476 if(strlen($value)>64) 477 $args[$key] = '"'.substr($value,0,64).'..."'; 478 else 479 $args[$key] = '"'.$value.'"'; 480 } 481 elseif(is_array($value)) 482 $args[$key] = 'array('.$this->argumentsToString($value).')'; 483 elseif($value===null) 484 $args[$key] = 'null'; 485 elseif(is_resource($value)) 486 $args[$key] = 'resource'; 487 488 if(is_string($key)) 489 { 490 $args[$key] = '"'.$key.'" => '.$args[$key]; 491 } 492 elseif($isAssoc) 493 { 494 $args[$key] = $key.' => '.$args[$key]; 495 } 496 } 497 $out = implode(", ", $args); 498 499 return $out; 500 } 501 502 /** 503 * Returns a value indicating whether the call stack is from application code. 504 * @param array $trace the trace data 505 * @return boolean whether the call stack is from application code. 506 */ 507 protected function isCoreCode($trace) 508 { 509 if(isset($trace['file'])) 510 { 511 $systemPath=realpath(dirname(__FILE__).'/..'); 512 return $trace['file']==='unknown' || strpos(realpath($trace['file']),$systemPath.DIRECTORY_SEPARATOR)===0; 513 } 514 return false; 515 } 516 517 /** 518 * Renders the source code around the error line. 519 * @param string $file source file path 520 * @param integer $errorLine the error line number 521 * @param integer $maxLines maximum number of lines to display 522 * @return string the rendering result 523 */ 524 protected function renderSourceCode($file,$errorLine,$maxLines) 525 { 526 $errorLine--; // adjust line number to 0-based from 1-based 527 if($errorLine<0 || ($lines=@file($file))===false || ($lineCount=count($lines))<=$errorLine) 528 return ''; 529 530 $halfLines=(int)($maxLines/2); 531 $beginLine=$errorLine-$halfLines>0 ? $errorLine-$halfLines:0; 532 $endLine=$errorLine+$halfLines<$lineCount?$errorLine+$halfLines:$lineCount-1; 533 $lineNumberWidth=strlen($endLine+1); 534 535 $output=''; 536 for($i=$beginLine;$i<=$endLine;++$i) 537 { 538 $isErrorLine = $i===$errorLine; 539 $code=sprintf("<span class=\"ln".($isErrorLine?' error-ln':'')."\">%0{$lineNumberWidth}d</span> %s",$i+1,CHtml::encode(str_replace("\t",' ',$lines[$i]))); 540 if(!$isErrorLine) 541 $output.=$code; 542 else 543 $output.='<span class="error">'.$code.'</span>'; 544 } 545 return '<div class="code"><pre>'.$output.'</pre></div>'; 546 } 547 /** 548 * Return correct message for each known http error code 549 * @param integer $httpCode error code to map 550 * @param string $replacement replacement error string that is returned if code is unknown 551 * @return string the textual representation of the given error code or the replacement string if the error code is unknown 552 */ 553 protected function getHttpHeader($httpCode, $replacement='') 554 { 555 $httpCodes = array( 556 100 => 'Continue', 557 101 => 'Switching Protocols', 558 102 => 'Processing', 559 118 => 'Connection Timed Out', 560 200 => 'OK', 561 201 => 'Created', 562 202 => 'Accepted', 563 203 => 'Non-Authoritative', 564 204 => 'No Content', 565 205 => 'Reset Content', 566 206 => 'Partial Content', 567 207 => 'Multi-Status', 568 210 => 'Content Different', 569 300 => 'Multiple Choices', 570 301 => 'Moved Permanently', 571 302 => 'Found', 572 303 => 'See Other', 573 304 => 'Not Modified', 574 305 => 'Use Proxy', 575 307 => 'Temporary Redirect', 576 310 => 'Too Many Redirect', 577 400 => 'Bad Request', 578 401 => 'Unauthorized', 579 402 => 'Payment Required', 580 403 => 'Forbidden', 581 404 => 'Not Found', 582 405 => 'Method Not Allowed', 583 406 => 'Not Acceptable', 584 407 => 'Proxy Authentication Required', 585 408 => 'Request Timeout', 586 409 => 'Conflict', 587 410 => 'Gone', 588 411 => 'Length Required', 589 412 => 'Precondition Failed', 590 413 => 'Request Entity Too Large', 591 414 => 'Request-URI Too Long', 592 415 => 'Unsupported Media Type', 593 416 => 'Requested Range Not Satisfiable', 594 417 => 'Expectation Failed', 595 418 => 'I’m a teapot', 596 422 => 'Unprocessable entity', 597 423 => 'Locked', 598 424 => 'Method failure', 599 425 => 'Unordered Collection', 600 426 => 'Upgrade Required', 601 428 => 'Precondition Required', 602 429 => 'Too Many Requests', 603 431 => 'Request Header Fields Too Large', 604 449 => 'Retry With', 605 450 => 'Blocked by Windows Parental Controls', 606 451 => 'Unavailable For Legal Reasons', 607 500 => 'Internal Server Error', 608 501 => 'Not Implemented', 609 502 => 'Bad Gateway', 610 503 => 'Service Unavailable', 611 504 => 'Gateway Timeout', 612 505 => 'HTTP Version Not Supported', 613 507 => 'Insufficient Storage', 614 509 => 'Bandwidth Limit Exceeded', 615 510 => 'Not Extended', 616 511 => 'Network Authentication Required', 617 ); 618 if(isset($httpCodes[$httpCode])) 619 return $httpCodes[$httpCode]; 620 else 621 return $replacement; 622 } 623} 624