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&lt;StatusCode&gt;.php</code>;
24 * </ul>
25 * where &lt;StatusCode&gt; 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