Subversion-Projekte lars-tiefland.prado

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 1
<?php
2
/**
3
 * TErrorHandler class file
4
 *
5
 * @author Qiang Xue <qiang.xue@gmail.com>
6
 * @link http://www.pradosoft.com/
7
 * @copyright Copyright &copy; 2005-2008 PradoSoft
8
 * @license http://www.pradosoft.com/license/
9
 * @version $Id: TErrorHandler.php 2541 2008-10-21 15:05:13Z qiang.xue $
10
 * @package System.Exceptions
11
 */
12
 
13
/**
14
 * TErrorHandler class
15
 *
16
 * TErrorHandler handles all PHP user errors and exceptions generated during
17
 * servicing user requests. It displays these errors using different templates
18
 * and if possible, using languages preferred by the client user.
19
 * Note, PHP parsing errors cannot be caught and handled by TErrorHandler.
20
 *
21
 * The templates used to format the error output are stored under System.Exceptions.
22
 * You may choose to use your own templates, should you not like the templates
23
 * provided by Prado. Simply set {@link setErrorTemplatePath ErrorTemplatePath}
24
 * to the path (in namespace format) storing your own templates.
25
 *
26
 * There are two sets of templates, one for errors to be displayed to client users
27
 * (called external errors), one for errors to be displayed to system developers
28
 * (called internal errors). The template file name for the former is
29
 * <b>error[StatusCode][-LanguageCode].html</b>, and for the latter it is
30
 * <b>exception[-LanguageCode].html</b>, where StatusCode refers to response status
31
 * code (e.g. 404, 500) specified when {@link THttpException} is thrown,
32
 * and LanguageCode is the client user preferred language code (e.g. en, zh, de).
33
 * The templates <b>error.html</b> and <b>exception.html</b> are default ones
34
 * that are used if no other appropriate templates are available.
35
 * Note, these templates are not Prado control templates. They are simply
36
 * html files with keywords (e.g. %%ErrorMessage%%, %%Version%%)
37
 * to be replaced with the corresponding information.
38
 *
39
 * By default, TErrorHandler is registered with {@link TApplication} as the
40
 * error handler module. It can be accessed via {@link TApplication::getErrorHandler()}.
41
 * You seldom need to deal with the error handler directly. It is mainly used
42
 * by the application object to handle errors.
43
 *
44
 * TErrorHandler may be configured in application configuration file as follows
45
 * <module id="error" class="TErrorHandler" ErrorTemplatePath="System.Exceptions" />
46
 *
47
 * @author Qiang Xue <qiang.xue@gmail.com>
48
 * @version $Id: TErrorHandler.php 2541 2008-10-21 15:05:13Z qiang.xue $
49
 * @package System.Exceptions
50
 * @since 3.0
51
 */
52
class TErrorHandler extends TModule
53
{
54
	/**
55
	 * error template file basename
56
	 */
57
	const ERROR_FILE_NAME='error';
58
	/**
59
	 * exception template file basename
60
	 */
61
	const EXCEPTION_FILE_NAME='exception';
62
	/**
63
	 * number of lines before and after the error line to be displayed in case of an exception
64
	 */
65
	const SOURCE_LINES=12;
66
 
67
	/**
68
	 * @var string error template directory
69
	 */
70
	private $_templatePath=null;
71
 
72
	/**
73
	 * Initializes the module.
74
	 * This method is required by IModule and is invoked by application.
75
	 * @param TXmlElement module configuration
76
	 */
77
	public function init($config)
78
	{
79
		$this->getApplication()->setErrorHandler($this);
80
	}
81
 
82
	/**
83
	 * @return string the directory containing error template files.
84
	 */
85
	public function getErrorTemplatePath()
86
	{
87
		if($this->_templatePath===null)
88
			$this->_templatePath=Prado::getFrameworkPath().'/Exceptions/templates';
89
		return $this->_templatePath;
90
	}
91
 
92
	/**
93
	 * Sets the path storing all error and exception template files.
94
	 * The path must be in namespace format, such as System.Exceptions (which is the default).
95
	 * @param string template path in namespace format
96
	 * @throws TConfigurationException if the template path is invalid
97
	 */
98
	public function setErrorTemplatePath($value)
99
	{
100
		if(($templatePath=Prado::getPathOfNamespace($value))!==null && is_dir($templatePath))
101
			$this->_templatePath=$templatePath;
102
		else
103
			throw new TConfigurationException('errorhandler_errortemplatepath_invalid',$value);
104
	}
105
 
106
	/**
107
	 * Handles PHP user errors and exceptions.
108
	 * This is the event handler responding to the <b>Error</b> event
109
	 * raised in {@link TApplication}.
110
	 * The method mainly uses appropriate template to display the error/exception.
111
	 * It terminates the application immediately after the error is displayed.
112
	 * @param mixed sender of the event
113
	 * @param mixed event parameter (if the event is raised by TApplication, it refers to the exception instance)
114
	 */
115
	public function handleError($sender,$param)
116
	{
117
		static $handling=false;
118
		// We need to restore error and exception handlers,
119
		// because within error and exception handlers, new errors and exceptions
120
		// cannot be handled properly by PHP
121
		restore_error_handler();
122
		restore_exception_handler();
123
		// ensure that we do not enter infinite loop of error handling
124
		if($handling)
125
			$this->handleRecursiveError($param);
126
		else
127
		{
128
			$handling=true;
129
			if(($response=$this->getResponse())!==null)
130
				$response->clear();
131
			if(!headers_sent())
132
				header('Content-Type: text/html; charset=UTF-8');
133
			if($param instanceof THttpException)
134
				$this->handleExternalError($param->getStatusCode(),$param);
135
			else if($this->getApplication()->getMode()===TApplicationMode::Debug)
136
				$this->displayException($param);
137
			else
138
				$this->handleExternalError(500,$param);
139
		}
140
	}
141
 
142
	/**
143
	 * Displays error to the client user.
144
	 * THttpException and errors happened when the application is in <b>Debug</b>
145
	 * mode will be displayed to the client user.
146
	 * @param integer response status code
147
	 * @param Exception exception instance
148
	 */
149
	protected function handleExternalError($statusCode,$exception)
150
	{
151
		if(!($exception instanceof THttpException))
152
			error_log($exception->__toString());
153
 
154
		$content=$this->getErrorTemplate($statusCode,$exception);
155
 
156
		$serverAdmin=isset($_SERVER['SERVER_ADMIN'])?$_SERVER['SERVER_ADMIN']:'';
157
		if($this->getApplication()->getMode()===TApplicationMode::Debug)
158
			$version=$_SERVER['SERVER_SOFTWARE'].' <a href="http://www.pradosoft.com/">PRADO</a>/'.Prado::getVersion();
159
		else
160
			$version='';
161
		$tokens=array(
162
			'%%StatusCode%%' => "$statusCode",
163
			'%%ErrorMessage%%' => htmlspecialchars($exception->getMessage()),
164
			'%%ServerAdmin%%' => $serverAdmin,
165
			'%%Version%%' => $version,
166
			'%%Time%%' => @strftime('%Y-%m-%d %H:%M',time())
167
		);
168
		header("HTTP/1.0 $statusCode ".$exception->getMessage());
169
		echo strtr($content,$tokens);
170
	}
171
 
172
	/**
173
	 * Handles error occurs during error handling (called recursive error).
174
	 * THttpException and errors happened when the application is in <b>Debug</b>
175
	 * mode will be displayed to the client user.
176
	 * Error is displayed without using existing template to prevent further errors.
177
	 * @param Exception exception instance
178
	 */
179
	protected function handleRecursiveError($exception)
180
	{
181
		if($this->getApplication()->getMode()===TApplicationMode::Debug)
182
		{
183
			echo "<html><head><title>Recursive Error</title></head>\n";
184
			echo "<body><h1>Recursive Error</h1>\n";
185
			echo "<pre>".$exception->__toString()."</pre>\n";
186
			echo "</body></html>";
187
		}
188
		else
189
		{
190
			error_log("Error happened while processing an existing error:\n".$exception->__toString());
191
			header('HTTP/1.0 500 Internal Error');
192
		}
193
	}
194
 
195
	/**
196
	 * Displays exception information.
197
	 * Exceptions are displayed with rich context information, including
198
	 * the call stack and the context source code.
199
	 * This method is only invoked when application is in <b>Debug</b> mode.
200
	 * @param Exception exception instance
201
	 */
202
	protected function displayException($exception)
203
	{
204
		if(php_sapi_name()==='cli')
205
		{
206
			echo $exception->getMessage()."\n";
207
			echo $exception->getTraceAsString();
208
			return;
209
		}
210
 
211
		if($exception instanceof TTemplateException)
212
		{
213
			$fileName=$exception->getTemplateFile();
214
			$lines=empty($fileName)?explode("\n",$exception->getTemplateSource()):@file($fileName);
215
			$source=$this->getSourceCode($lines,$exception->getLineNumber());
216
			if($fileName==='')
217
				$fileName='---embedded template---';
218
			$errorLine=$exception->getLineNumber();
219
		}
220
		else
221
		{
222
			if(($trace=$this->getExactTrace($exception))!==null)
223
			{
224
				$fileName=$trace['file'];
225
				$errorLine=$trace['line'];
226
			}
227
			else
228
			{
229
				$fileName=$exception->getFile();
230
				$errorLine=$exception->getLine();
231
			}
232
			$source=$this->getSourceCode(@file($fileName),$errorLine);
233
		}
234
 
235
		if($this->getApplication()->getMode()===TApplicationMode::Debug)
236
			$version=$_SERVER['SERVER_SOFTWARE'].' <a href="http://www.pradosoft.com/">PRADO</a>/'.Prado::getVersion();
237
		else
238
			$version='';
239
 
240
		$tokens=array(
241
			'%%ErrorType%%' => get_class($exception),
242
			'%%ErrorMessage%%' => $this->addLink(htmlspecialchars($exception->getMessage())),
243
			'%%SourceFile%%' => htmlspecialchars($fileName).' ('.$errorLine.')',
244
			'%%SourceCode%%' => $source,
245
			'%%StackTrace%%' => htmlspecialchars($exception->getTraceAsString()),
246
			'%%Version%%' => $version,
247
			'%%Time%%' => @strftime('%Y-%m-%d %H:%M',time())
248
		);
249
 
250
		$content=$this->getExceptionTemplate($exception);
251
 
252
		echo strtr($content,$tokens);
253
	}
254
 
255
	/**
256
	 * Retrieves the template used for displaying internal exceptions.
257
	 * Internal exceptions will be displayed with source code causing the exception.
258
	 * This occurs when the application is in debug mode.
259
	 * @param Exception the exception to be displayed
260
	 * @return string the template content
261
	 */
262
	protected function getExceptionTemplate($exception)
263
	{
264
		$lang=Prado::getPreferredLanguage();
265
		$exceptionFile=Prado::getFrameworkPath().'/Exceptions/templates/'.self::EXCEPTION_FILE_NAME.'-'.$lang.'.html';
266
		if(!is_file($exceptionFile))
267
			$exceptionFile=Prado::getFrameworkPath().'/Exceptions/templates/'.self::EXCEPTION_FILE_NAME.'.html';
268
		if(($content=@file_get_contents($exceptionFile))===false)
269
			die("Unable to open exception template file '$exceptionFile'.");
270
		return $content;
271
	}
272
 
273
	/**
274
	 * Retrieves the template used for displaying external exceptions.
275
	 * External exceptions are those displayed to end-users. They do not contain
276
	 * error source code. Therefore, you might want to override this method
277
	 * to provide your own error template for displaying certain external exceptions.
278
	 * The following tokens in the template will be replaced with corresponding content:
279
	 * %%StatusCode%% : the status code of the exception
280
	 * %%ErrorMessage%% : the error message (HTML encoded).
281
	 * %%ServerAdmin%% : the server admin information (retrieved from Web server configuration)
282
	 * %%Version%% : the version information of the Web server.
283
	 * %%Time%% : the time the exception occurs at
284
	 *
285
	 * @param integer status code (such as 404, 500, etc.)
286
	 * @param Exception the exception to be displayed
287
	 * @return string the template content
288
	 */
289
	protected function getErrorTemplate($statusCode,$exception)
290
	{
291
		$base=$this->getErrorTemplatePath().DIRECTORY_SEPARATOR.self::ERROR_FILE_NAME;
292
		$lang=Prado::getPreferredLanguage();
293
		if(is_file("$base$statusCode-$lang.html"))
294
			$errorFile="$base$statusCode-$lang.html";
295
		else if(is_file("$base$statusCode.html"))
296
			$errorFile="$base$statusCode.html";
297
		else if(is_file("$base-$lang.html"))
298
			$errorFile="$base-$lang.html";
299
		else
300
			$errorFile="$base.html";
301
		if(($content=@file_get_contents($errorFile))===false)
302
			die("Unable to open error template file '$errorFile'.");
303
		return $content;
304
	}
305
 
306
	private function getExactTrace($exception)
307
	{
308
		$trace=$exception->getTrace();
309
		$result=null;
310
		// if PHP exception, we want to show the 2nd stack level context
311
		// because the 1st stack level is of little use (it's in error handler)
312
		if($exception instanceof TPhpErrorException)
313
			$result=isset($trace[0]['file'])?$trace[0]:$trace[1];
314
		else if($exception instanceof TInvalidOperationException)
315
		{
316
			// in case of getter or setter error, find out the exact file and row
317
			if(($result=$this->getPropertyAccessTrace($trace,'__get'))===null)
318
				$result=$this->getPropertyAccessTrace($trace,'__set');
319
		}
320
		if($result!==null && strpos($result['file'],': eval()\'d code')!==false)
321
			return null;
322
 
323
		return $result;
324
	}
325
 
326
	private function getPropertyAccessTrace($trace,$pattern)
327
	{
328
		$result=null;
329
		foreach($trace as $t)
330
		{
331
			if(isset($t['function']) && $t['function']===$pattern)
332
				$result=$t;
333
			else
334
				break;
335
		}
336
		return $result;
337
	}
338
 
339
	private function getSourceCode($lines,$errorLine)
340
	{
341
		$beginLine=$errorLine-self::SOURCE_LINES>=0?$errorLine-self::SOURCE_LINES:0;
342
		$endLine=$errorLine+self::SOURCE_LINES<=count($lines)?$errorLine+self::SOURCE_LINES:count($lines);
343
 
344
		$source='';
345
		for($i=$beginLine;$i<$endLine;++$i)
346
		{
347
			if($i===$errorLine-1)
348
			{
349
				$line=htmlspecialchars(sprintf("%04d: %s",$i+1,str_replace("\t",'    ',$lines[$i])));
350
				$source.="<div class=\"error\">".$line."</div>";
351
			}
352
			else
353
				$source.=htmlspecialchars(sprintf("%04d: %s",$i+1,str_replace("\t",'    ',$lines[$i])));
354
		}
355
		return $source;
356
	}
357
 
358
	private function addLink($message)
359
	{
360
		$baseUrl='http://www.pradosoft.com/docs/classdoc';
361
		return preg_replace('/\b(T[A-Z]\w+)\b/',"<a href=\"$baseUrl/\${1}\" target=\"_blank\">\${1}</a>",$message);
362
	}
363
}
364