Completed
Push — master ( 61fbd5...6809a3 )
by Fabio
07:14
created

TErrorHandler::displayException()   B

Complexity

Conditions 7
Paths 13

Size

Total Lines 52
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 34
nc 13
nop 1
dl 0
loc 52
rs 7.2396
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * TErrorHandler class file
4
 *
5
 * @author Qiang Xue <[email protected]>
6
 * @link https://github.com/pradosoft/prado
7
 * @copyright Copyright &copy; 2005-2016 The PRADO Group
8
 * @license https://github.com/pradosoft/prado/blob/master/LICENSE
9
 * @package Prado\Exceptions
10
 */
11
12
namespace Prado\Exceptions;
13
use \Prado\TApplicationMode;
14
use \Prado\Prado;
15
16
/**
17
 * TErrorHandler class
18
 *
19
 * TErrorHandler handles all PHP user errors and exceptions generated during
20
 * servicing user requests. It displays these errors using different templates
21
 * and if possible, using languages preferred by the client user.
22
 * Note, PHP parsing errors cannot be caught and handled by TErrorHandler.
23
 *
24
 * The templates used to format the error output are stored under System.Exceptions.
25
 * You may choose to use your own templates, should you not like the templates
26
 * provided by Prado. Simply set {@link setErrorTemplatePath ErrorTemplatePath}
27
 * to the path (in namespace format) storing your own templates.
28
 *
29
 * There are two sets of templates, one for errors to be displayed to client users
30
 * (called external errors), one for errors to be displayed to system developers
31
 * (called internal errors). The template file name for the former is
32
 * <b>error[StatusCode][-LanguageCode].html</b>, and for the latter it is
33
 * <b>exception[-LanguageCode].html</b>, where StatusCode refers to response status
34
 * code (e.g. 404, 500) specified when {@link THttpException} is thrown,
35
 * and LanguageCode is the client user preferred language code (e.g. en, zh, de).
36
 * The templates <b>error.html</b> and <b>exception.html</b> are default ones
37
 * that are used if no other appropriate templates are available.
38
 * Note, these templates are not Prado control templates. They are simply
39
 * html files with keywords (e.g. %%ErrorMessage%%, %%Version%%)
40
 * to be replaced with the corresponding information.
41
 *
42
 * By default, TErrorHandler is registered with {@link TApplication} as the
43
 * error handler module. It can be accessed via {@link TApplication::getErrorHandler()}.
44
 * You seldom need to deal with the error handler directly. It is mainly used
45
 * by the application object to handle errors.
46
 *
47
 * TErrorHandler may be configured in application configuration file as follows
48
 * <module id="error" class="TErrorHandler" ErrorTemplatePath="System.Exceptions" />
49
 *
50
 * @author Qiang Xue <[email protected]>
51
 * @package Prado\Exceptions
52
 * @since 3.0
53
 */
54
class TErrorHandler extends \Prado\TModule
55
{
56
	/**
57
	 * error template file basename
58
	 */
59
	const ERROR_FILE_NAME='error';
60
	/**
61
	 * exception template file basename
62
	 */
63
	const EXCEPTION_FILE_NAME='exception';
64
	/**
65
	 * number of lines before and after the error line to be displayed in case of an exception
66
	 */
67
	const SOURCE_LINES=12;
68
	/**
69
	 * number of prado internal function calls to be dropped from stack traces on fatal errors
70
	 */
71
	const FATAL_ERROR_TRACE_DROP_LINES=5;
72
73
	/**
74
	 * @var string error template directory
75
	 */
76
	private $_templatePath=null;
77
78
	/**
79
	 * Initializes the module.
80
	 * This method is required by IModule and is invoked by application.
81
	 * @param TXmlElement module configuration
82
	 */
83
	public function init($config)
84
	{
85
		$this->getApplication()->setErrorHandler($this);
86
	}
87
88
	/**
89
	 * @return string the directory containing error template files.
90
	 */
91
	public function getErrorTemplatePath()
92
	{
93
		if($this->_templatePath===null)
94
			$this->_templatePath=Prado::getFrameworkPath().'/Exceptions/templates';
95
		return $this->_templatePath;
96
	}
97
98
	/**
99
	 * Sets the path storing all error and exception template files.
100
	 * The path must be in namespace format, such as System.Exceptions (which is the default).
101
	 * @param string template path in namespace format
102
	 * @throws TConfigurationException if the template path is invalid
103
	 */
104
	public function setErrorTemplatePath($value)
105
	{
106
		if(($templatePath=Prado::getPathOfNamespace($value))!==null && is_dir($templatePath))
107
			$this->_templatePath=$templatePath;
108
		else
109
			throw new TConfigurationException('errorhandler_errortemplatepath_invalid',$value);
110
	}
111
112
	/**
113
	 * Handles PHP user errors and exceptions.
114
	 * This is the event handler responding to the <b>Error</b> event
115
	 * raised in {@link TApplication}.
116
	 * The method mainly uses appropriate template to display the error/exception.
117
	 * It terminates the application immediately after the error is displayed.
118
	 * @param mixed sender of the event
119
	 * @param mixed event parameter (if the event is raised by TApplication, it refers to the exception instance)
120
	 */
121
	public function handleError($sender,$param)
122
	{
123
		static $handling=false;
124
		// We need to restore error and exception handlers,
125
		// because within error and exception handlers, new errors and exceptions
126
		// cannot be handled properly by PHP
127
		restore_error_handler();
128
		restore_exception_handler();
129
		// ensure that we do not enter infinite loop of error handling
130
		if($handling)
131
			$this->handleRecursiveError($param);
132
		else
133
		{
134
			$handling=true;
135
			if(($response=$this->getResponse())!==null)
136
				$response->clear();
137
			if(!headers_sent())
138
				header('Content-Type: text/html; charset=UTF-8');
139
			if($param instanceof THttpException)
140
				$this->handleExternalError($param->getStatusCode(),$param);
141
			else if($this->getApplication()->getMode()===TApplicationMode::Debug)
142
				$this->displayException($param);
143
			else
144
				$this->handleExternalError(500,$param);
145
		}
146
	}
147
148
149
	/**
150
	 * @param string $value
151
	 * @param Exception|null$exception
152
	 * @return string
153
	 * @since 3.1.6
154
	 */
155
	protected static function hideSecurityRelated($value, $exception=null)
0 ignored issues
show
Coding Style introduced by
hideSecurityRelated uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
156
	{
157
		$aRpl = array();
158
		if($exception !== null && $exception instanceof \Exception)
159
		{
160
			if($exception instanceof TPhpFatalErrorException && 
161
				function_exists('xdebug_get_function_stack'))
162
			{
163
				$aTrace = array_slice(array_reverse(xdebug_get_function_stack()), self::FATAL_ERROR_TRACE_DROP_LINES, -1);
164
			} else {
165
				$aTrace = $exception->getTrace();
166
			}
167
168
			foreach($aTrace as $item)
169
			{
170
				if(isset($item['file']))
171
					$aRpl[dirname($item['file']) . DIRECTORY_SEPARATOR] = '<hidden>' . DIRECTORY_SEPARATOR;
172
			}
173
		}
174
		$aRpl[$_SERVER['DOCUMENT_ROOT']] = '${DocumentRoot}';
175
		$aRpl[str_replace('/', DIRECTORY_SEPARATOR, $_SERVER['DOCUMENT_ROOT'])] = '${DocumentRoot}';
176
		$aRpl[PRADO_DIR . DIRECTORY_SEPARATOR] = '${PradoFramework}' . DIRECTORY_SEPARATOR;
177
		if(isset($aRpl[DIRECTORY_SEPARATOR])) unset($aRpl[DIRECTORY_SEPARATOR]);
178
		$aRpl = array_reverse($aRpl, true);
179
180
		return str_replace(array_keys($aRpl), $aRpl, $value);
181
	}
182
183
	/**
184
	 * Displays error to the client user.
185
	 * THttpException and errors happened when the application is in <b>Debug</b>
186
	 * mode will be displayed to the client user.
187
	 * @param integer response status code
188
	 * @param Exception exception instance
189
	 */
190
	protected function handleExternalError($statusCode,$exception)
0 ignored issues
show
Coding Style introduced by
handleExternalError uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
191
	{
192
		if(!($exception instanceof THttpException))
193
			error_log($exception->__toString());
194
195
		$content=$this->getErrorTemplate($statusCode,$exception);
196
197
		$serverAdmin=isset($_SERVER['SERVER_ADMIN'])?$_SERVER['SERVER_ADMIN']:'';
198
199
		$isDebug = $this->getApplication()->getMode()===TApplicationMode::Debug;
200
201
		$errorMessage = $exception->getMessage();
202
		if($isDebug)
203
			$version=$_SERVER['SERVER_SOFTWARE'].' <a href="https://github.com/pradosoft/prado">PRADO</a>/'.Prado::getVersion();
204
		else
205
		{
206
			$version='';
207
			$errorMessage = self::hideSecurityRelated($errorMessage, $exception);
208
		}
209
		$tokens=array(
210
			'%%StatusCode%%' => "$statusCode",
211
			'%%ErrorMessage%%' => htmlspecialchars($errorMessage),
212
			'%%ServerAdmin%%' => $serverAdmin,
213
			'%%Version%%' => $version,
214
			'%%Time%%' => @strftime('%Y-%m-%d %H:%M',time())
215
		);
216
217
		$this->getApplication()->getResponse()->setStatusCode($statusCode, $isDebug ? $exception->getMessage() : null);
218
219
		echo strtr($content,$tokens);
220
	}
221
222
	/**
223
	 * Handles error occurs during error handling (called recursive error).
224
	 * THttpException and errors happened when the application is in <b>Debug</b>
225
	 * mode will be displayed to the client user.
226
	 * Error is displayed without using existing template to prevent further errors.
227
	 * @param Exception exception instance
228
	 */
229
	protected function handleRecursiveError($exception)
230
	{
231
		if($this->getApplication()->getMode()===TApplicationMode::Debug)
232
		{
233
			echo "<html><head><title>Recursive Error</title></head>\n";
234
			echo "<body><h1>Recursive Error</h1>\n";
235
			echo "<pre>".$exception->__toString()."</pre>\n";
236
			echo "</body></html>";
237
		}
238
		else
239
		{
240
			error_log("Error happened while processing an existing error:\n".$exception->__toString());
241
			header('HTTP/1.0 500 Internal Error');
242
		}
243
	}
244
245
	/**
246
	 * Displays exception information.
247
	 * Exceptions are displayed with rich context information, including
248
	 * the call stack and the context source code.
249
	 * This method is only invoked when application is in <b>Debug</b> mode.
250
	 * @param Exception exception instance
251
	 */
252
	protected function displayException($exception)
0 ignored issues
show
Coding Style introduced by
displayException uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
253
	{
254
		if(php_sapi_name()==='cli')
255
		{
256
			echo $exception->getMessage()."\n";
257
			echo $this->getExactTraceAsString($exception);
258
			return;
259
		}
260
261
		if($exception instanceof TTemplateException)
262
		{
263
			$fileName=$exception->getTemplateFile();
264
			$lines=empty($fileName)?explode("\n",$exception->getTemplateSource()):@file($fileName);
265
			$source=$this->getSourceCode($lines,$exception->getLineNumber());
266
			if($fileName==='')
267
				$fileName='---embedded template---';
268
			$errorLine=$exception->getLineNumber();
269
		}
270
		else
271
		{
272
			if(($trace=$this->getExactTrace($exception))!==null)
273
			{
274
				$fileName=$trace['file'];
275
				$errorLine=$trace['line'];
276
			}
277
			else
278
			{
279
				$fileName=$exception->getFile();
280
				$errorLine=$exception->getLine();
281
			}
282
			$source=$this->getSourceCode(@file($fileName),$errorLine);
283
		}
284
285
		if($this->getApplication()->getMode()===TApplicationMode::Debug)
286
			$version=$_SERVER['SERVER_SOFTWARE'].' <a href="https://github.com/pradosoft/prado">PRADO</a>/'.Prado::getVersion();
287
		else
288
			$version='';
289
290
		$tokens=array(
291
			'%%ErrorType%%' => get_class($exception),
292
			'%%ErrorMessage%%' => $this->addLink(htmlspecialchars($exception->getMessage())),
293
			'%%SourceFile%%' => htmlspecialchars($fileName).' ('.$errorLine.')',
294
			'%%SourceCode%%' => $source,
295
			'%%StackTrace%%' => htmlspecialchars($this->getExactTraceAsString($exception)),
296
			'%%Version%%' => $version,
297
			'%%Time%%' => @strftime('%Y-%m-%d %H:%M',time())
298
		);
299
300
		$content=$this->getExceptionTemplate($exception);
301
302
		echo strtr($content,$tokens);
303
	}
304
305
	/**
306
	 * Retrieves the template used for displaying internal exceptions.
307
	 * Internal exceptions will be displayed with source code causing the exception.
308
	 * This occurs when the application is in debug mode.
309
	 * @param Exception the exception to be displayed
310
	 * @return string the template content
311
	 */
312
	protected function getExceptionTemplate($exception)
313
	{
314
		$lang=Prado::getPreferredLanguage();
315
		$exceptionFile=Prado::getFrameworkPath().'/Exceptions/templates/'.self::EXCEPTION_FILE_NAME.'-'.$lang.'.html';
316
		if(!is_file($exceptionFile))
317
			$exceptionFile=Prado::getFrameworkPath().'/Exceptions/templates/'.self::EXCEPTION_FILE_NAME.'.html';
318
		if(($content=@file_get_contents($exceptionFile))===false)
319
			die("Unable to open exception template file '$exceptionFile'.");
0 ignored issues
show
Coding Style Compatibility introduced by
The method getExceptionTemplate() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
320
		return $content;
321
	}
322
323
	/**
324
	 * Retrieves the template used for displaying external exceptions.
325
	 * External exceptions are those displayed to end-users. They do not contain
326
	 * error source code. Therefore, you might want to override this method
327
	 * to provide your own error template for displaying certain external exceptions.
328
	 * The following tokens in the template will be replaced with corresponding content:
329
	 * %%StatusCode%% : the status code of the exception
330
	 * %%ErrorMessage%% : the error message (HTML encoded).
331
	 * %%ServerAdmin%% : the server admin information (retrieved from Web server configuration)
332
	 * %%Version%% : the version information of the Web server.
333
	 * %%Time%% : the time the exception occurs at
334
	 *
335
	 * @param integer status code (such as 404, 500, etc.)
336
	 * @param Exception the exception to be displayed
337
	 * @return string the template content
338
	 */
339
	protected function getErrorTemplate($statusCode,$exception)
340
	{
341
		$base=$this->getErrorTemplatePath().DIRECTORY_SEPARATOR.self::ERROR_FILE_NAME;
342
		$lang=Prado::getPreferredLanguage();
343
		if(is_file("$base$statusCode-$lang.html"))
344
			$errorFile="$base$statusCode-$lang.html";
345
		else if(is_file("$base$statusCode.html"))
346
			$errorFile="$base$statusCode.html";
347
		else if(is_file("$base-$lang.html"))
348
			$errorFile="$base-$lang.html";
349
		else
350
			$errorFile="$base.html";
351
		if(($content=@file_get_contents($errorFile))===false)
352
			die("Unable to open error template file '$errorFile'.");
0 ignored issues
show
Coding Style Compatibility introduced by
The method getErrorTemplate() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
353
		return $content;
354
	}
355
356
	private function getExactTrace($exception)
357
	{
358
		$result=null;
359
		if($exception instanceof TPhpFatalErrorException && 
360
			function_exists('xdebug_get_function_stack'))
361
		{
362
			$trace = array_slice(array_reverse(xdebug_get_function_stack()), self::FATAL_ERROR_TRACE_DROP_LINES, -1);
363
		} else {
364
			$trace=$exception->getTrace();
365
		}
366
		
367
		// if PHP exception, we want to show the 2nd stack level context
368
		// because the 1st stack level is of little use (it's in error handler)
369
		if($exception instanceof TPhpErrorException) {
370
			if(isset($trace[0]['file']))
371
				$result=$trace[0];
372
			elseif(isset($trace[1]))
373
				$result=$trace[1];
374
		} elseif($exception instanceof TInvalidOperationException) {
375
			// in case of getter or setter error, find out the exact file and row
376
			if(($result=$this->getPropertyAccessTrace($trace,'__get'))===null)
377
				$result=$this->getPropertyAccessTrace($trace,'__set');
378
		}
379
		if($result!==null && strpos($result['file'],': eval()\'d code')!==false)
380
			return null;
381
382
		return $result;
383
	}
384
385
	private function getExactTraceAsString($exception)
386
	{
387
		if($exception instanceof TPhpFatalErrorException && 
388
			function_exists('xdebug_get_function_stack'))
389
		{
390
			$trace = array_slice(array_reverse(xdebug_get_function_stack()), self::FATAL_ERROR_TRACE_DROP_LINES, -1);
391
			$txt = '';
392
			$row = 0;
393
394
			// try to mimic Exception::getTraceAsString()
395
			foreach($trace as $line)
396
			{
397
				if(array_key_exists('function', $line))
398
					$func = $line['function'] . '(' . implode(',', $line['params']) . ')';
399
				else
400
					$func = 'unknown';
401
402
				$txt .= '#' . $row . ' ' . $line['file'] . '(' . $line['line'] . '): ' . $func . "\n";
403
				$row++;
404
			}
405
406
			return $txt;
407
		}
408
409
		return $exception->getTraceAsString();
410
	}
411
412
	private function getPropertyAccessTrace($trace,$pattern)
413
	{
414
		$result=null;
415
		foreach($trace as $t)
416
		{
417
			if(isset($t['function']) && $t['function']===$pattern)
418
				$result=$t;
419
			else
420
				break;
421
		}
422
		return $result;
423
	}
424
425
	private function getSourceCode($lines,$errorLine)
426
	{
427
		$beginLine=$errorLine-self::SOURCE_LINES>=0?$errorLine-self::SOURCE_LINES:0;
428
		$endLine=$errorLine+self::SOURCE_LINES<=count($lines)?$errorLine+self::SOURCE_LINES:count($lines);
429
430
		$source='';
431
		for($i=$beginLine;$i<$endLine;++$i)
432
		{
433
			if($i===$errorLine-1)
434
			{
435
				$line=htmlspecialchars(sprintf("%04d: %s",$i+1,str_replace("\t",'    ',$lines[$i])));
436
				$source.="<div class=\"error\">".$line."</div>";
437
			}
438
			else
439
				$source.=htmlspecialchars(sprintf("%04d: %s",$i+1,str_replace("\t",'    ',$lines[$i])));
440
		}
441
		return $source;
442
	}
443
444
	private function addLink($message) {
445
		if (!is_null($class = $this->getErrorClassNameSpace($message))) {
446
			return str_replace($class['name'], '<a href="' . $class['url'] . '" target="_blank">' . $class['name'] . '</a>', $message);
447
		}
448
		return $message;
449
	}
450
451
	private function getErrorClassNameSpace($message) {
452
		$matches = [];
453
		preg_match('/\b(T[A-Z]\w+)\b/', $message, $matches);
454
		if (is_array($matches) && count($matches) > 0) {
455
			$class = $matches[0];
456
			try {
457
				$function = new \ReflectionClass($class);
458
			}
459
			catch (\Exception $e) {
460
				return null;
461
			}
462
			$classname = $function->getNamespaceName();
463
			return [
464
			    'url' => 'http://pradosoft.github.io/docs/manual/class-' . str_replace('\\', '.', (string) $classname) . '.' . $class . '.html',
465
			    'name' => $class,
466
			];
467
		}
468
		return null;
469
	}
470
471
}
472
473