Completed
Push — master ( 46a77a...a5d155 )
by Fabio
53:55
created

TErrorHandler::hidePrivatePathParts()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 3
nop 1
dl 0
loc 16
rs 9.7333
c 0
b 0
f 0
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
14
use \Prado\TApplicationMode;
15
use \Prado\Prado;
16
17
/**
18
 * TErrorHandler class
19
 *
20
 * TErrorHandler handles all PHP user errors and exceptions generated during
21
 * servicing user requests. It displays these errors using different templates
22
 * and if possible, using languages preferred by the client user.
23
 * Note, PHP parsing errors cannot be caught and handled by TErrorHandler.
24
 *
25
 * The templates used to format the error output are stored under System.Exceptions.
26
 * You may choose to use your own templates, should you not like the templates
27
 * provided by Prado. Simply set {@link setErrorTemplatePath ErrorTemplatePath}
28
 * to the path (in namespace format) storing your own templates.
29
 *
30
 * There are two sets of templates, one for errors to be displayed to client users
31
 * (called external errors), one for errors to be displayed to system developers
32
 * (called internal errors). The template file name for the former is
33
 * <b>error[StatusCode][-LanguageCode].html</b>, and for the latter it is
34
 * <b>exception[-LanguageCode].html</b>, where StatusCode refers to response status
35
 * code (e.g. 404, 500) specified when {@link THttpException} is thrown,
36
 * and LanguageCode is the client user preferred language code (e.g. en, zh, de).
37
 * The templates <b>error.html</b> and <b>exception.html</b> are default ones
38
 * that are used if no other appropriate templates are available.
39
 * Note, these templates are not Prado control templates. They are simply
40
 * html files with keywords (e.g. %%ErrorMessage%%, %%Version%%)
41
 * to be replaced with the corresponding information.
42
 *
43
 * By default, TErrorHandler is registered with {@link TApplication} as the
44
 * error handler module. It can be accessed via {@link TApplication::getErrorHandler()}.
45
 * You seldom need to deal with the error handler directly. It is mainly used
46
 * by the application object to handle errors.
47
 *
48
 * TErrorHandler may be configured in application configuration file as follows
49
 * <module id="error" class="TErrorHandler" ErrorTemplatePath="System.Exceptions" />
50
 *
51
 * @author Qiang Xue <[email protected]>
52
 * @package Prado\Exceptions
53
 * @since 3.0
54
 */
55
class TErrorHandler extends \Prado\TModule
56
{
57
	/**
58
	 * error template file basename
59
	 */
60
	const ERROR_FILE_NAME = 'error';
61
	/**
62
	 * exception template file basename
63
	 */
64
	const EXCEPTION_FILE_NAME = 'exception';
65
	/**
66
	 * number of lines before and after the error line to be displayed in case of an exception
67
	 */
68
	const SOURCE_LINES = 12;
69
	/**
70
	 * number of prado internal function calls to be dropped from stack traces on fatal errors
71
	 */
72
	const FATAL_ERROR_TRACE_DROP_LINES = 5;
73
74
	/**
75
	 * @var string error template directory
76
	 */
77
	private $_templatePath;
78
79
	/**
80
	 * Initializes the module.
81
	 * This method is required by IModule and is invoked by application.
82
	 * @param TXmlElement $config module configuration
83
	 */
84
	public function init($config)
85
	{
86
		$this->getApplication()->setErrorHandler($this);
87
	}
88
89
	/**
90
	 * @return string the directory containing error template files.
91
	 */
92
	public function getErrorTemplatePath()
93
	{
94
		if ($this->_templatePath === null) {
95
			$this->_templatePath = Prado::getFrameworkPath() . '/Exceptions/templates';
96
		}
97
		return $this->_templatePath;
98
	}
99
100
	/**
101
	 * Sets the path storing all error and exception template files.
102
	 * The path must be in namespace format, such as System.Exceptions (which is the default).
103
	 * @param string $value template path in namespace format
104
	 * @throws TConfigurationException if the template path is invalid
105
	 */
106
	public function setErrorTemplatePath($value)
107
	{
108 View Code Duplication
		if (($templatePath = Prado::getPathOfNamespace($value)) !== null && is_dir($templatePath)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
109
			$this->_templatePath = $templatePath;
110
		} else {
111
			throw new TConfigurationException('errorhandler_errortemplatepath_invalid', $value);
112
		}
113
	}
114
115
	/**
116
	 * Handles PHP user errors and exceptions.
117
	 * This is the event handler responding to the <b>Error</b> event
118
	 * raised in {@link TApplication}.
119
	 * The method mainly uses appropriate template to display the error/exception.
120
	 * It terminates the application immediately after the error is displayed.
121
	 * @param mixed $sender sender of the event
122
	 * @param mixed $param event parameter (if the event is raised by TApplication, it refers to the exception instance)
123
	 */
124
	public function handleError($sender, $param)
125
	{
126
		static $handling = false;
127
		// We need to restore error and exception handlers,
128
		// because within error and exception handlers, new errors and exceptions
129
		// cannot be handled properly by PHP
130
		restore_error_handler();
131
		restore_exception_handler();
132
		// ensure that we do not enter infinite loop of error handling
133
		if ($handling) {
134
			$this->handleRecursiveError($param);
135
		} else {
136
			$handling = true;
137
			if (($response = $this->getResponse()) !== null) {
138
				$response->clear();
139
			}
140
			if (!headers_sent()) {
141
				header('Content-Type: text/html; charset=UTF-8');
142
			}
143
			if ($param instanceof THttpException) {
144
				$this->handleExternalError($param->getStatusCode(), $param);
145
			} elseif ($this->getApplication()->getMode() === TApplicationMode::Debug) {
146
				$this->displayException($param);
147
			} else {
148
				$this->handleExternalError(500, $param);
149
			}
150
		}
151
	}
152
153
154
	/**
155
	 * @param string $value
156
	 * @param Exception|null$exception
157
	 * @return string
158
	 * @since 3.1.6
159
	 */
160
	protected static function hideSecurityRelated($value, $exception = null)
161
	{
162
		$aRpl = [];
163
		if ($exception !== null && $exception instanceof \Exception) {
164 View Code Duplication
			if ($exception instanceof TPhpFatalErrorException &&
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
165
				function_exists('xdebug_get_function_stack')) {
166
				$aTrace = array_slice(array_reverse(xdebug_get_function_stack()), self::FATAL_ERROR_TRACE_DROP_LINES, -1);
167
			} else {
168
				$aTrace = $exception->getTrace();
169
			}
170
171
			foreach ($aTrace as $item) {
172
				if (isset($item['file'])) {
173
					$aRpl[dirname($item['file']) . DIRECTORY_SEPARATOR] = '<hidden>' . DIRECTORY_SEPARATOR;
174
				}
175
			}
176
		}
177
		$aRpl[$_SERVER['DOCUMENT_ROOT']] = '${DocumentRoot}';
178
		$aRpl[str_replace('/', DIRECTORY_SEPARATOR, $_SERVER['DOCUMENT_ROOT'])] = '${DocumentRoot}';
179
		$aRpl[PRADO_DIR . DIRECTORY_SEPARATOR] = '${PradoFramework}' . DIRECTORY_SEPARATOR;
180
		if (isset($aRpl[DIRECTORY_SEPARATOR])) {
181
			unset($aRpl[DIRECTORY_SEPARATOR]);
182
		}
183
		$aRpl = array_reverse($aRpl, true);
184
185
		return str_replace(array_keys($aRpl), $aRpl, $value);
186
	}
187
188
	/**
189
	 * Displays error to the client user.
190
	 * THttpException and errors happened when the application is in <b>Debug</b>
191
	 * mode will be displayed to the client user.
192
	 * @param int $statusCode response status code
193
	 * @param Exception $exception exception instance
194
	 */
195
	protected function handleExternalError($statusCode, $exception)
196
	{
197
		if (!($exception instanceof THttpException)) {
198
			error_log($exception->__toString());
199
		}
200
201
		$content = $this->getErrorTemplate($statusCode, $exception);
202
203
		$serverAdmin = isset($_SERVER['SERVER_ADMIN']) ? $_SERVER['SERVER_ADMIN'] : '';
204
205
		$isDebug = $this->getApplication()->getMode() === TApplicationMode::Debug;
206
207
		$errorMessage = $exception->getMessage();
208
		if ($isDebug) {
209
			$version = $_SERVER['SERVER_SOFTWARE'] . ' <a href="https://github.com/pradosoft/prado">PRADO</a>/' . Prado::getVersion();
210
		} else {
211
			$version = '';
212
			$errorMessage = self::hideSecurityRelated($errorMessage, $exception);
213
		}
214
		$tokens = [
215
			'%%StatusCode%%' => "$statusCode",
216
			'%%ErrorMessage%%' => htmlspecialchars($errorMessage),
217
			'%%ServerAdmin%%' => $serverAdmin,
218
			'%%Version%%' => $version,
219
			'%%Time%%' => @strftime('%Y-%m-%d %H:%M', time())
220
		];
221
222
		$this->getApplication()->getResponse()->setStatusCode($statusCode, $isDebug ? $exception->getMessage() : null);
223
224
		echo strtr($content, $tokens);
225
	}
226
227
	/**
228
	 * Handles error occurs during error handling (called recursive error).
229
	 * THttpException and errors happened when the application is in <b>Debug</b>
230
	 * mode will be displayed to the client user.
231
	 * Error is displayed without using existing template to prevent further errors.
232
	 * @param Exception $exception exception instance
233
	 */
234
	protected function handleRecursiveError($exception)
235
	{
236
		if ($this->getApplication()->getMode() === TApplicationMode::Debug) {
237
			echo "<html><head><title>Recursive Error</title></head>\n";
238
			echo "<body><h1>Recursive Error</h1>\n";
239
			echo "<pre>" . $exception->__toString() . "</pre>\n";
240
			echo "</body></html>";
241
		} else {
242
			error_log("Error happened while processing an existing error:\n" . $exception->__toString());
243
			header('HTTP/1.0 500 Internal Error');
244
		}
245
	}
246
247
	protected function hidePrivatePathParts($value)
248
	{
249
		static $aRpl;
250
		if($aRpl === null)
251
		{
252
			$aRpl[$_SERVER['DOCUMENT_ROOT']] = '${DocumentRoot}';
253
			$aRpl[str_replace('/', DIRECTORY_SEPARATOR, $_SERVER['DOCUMENT_ROOT'])] = '${DocumentRoot}';
254
			$aRpl[PRADO_DIR . DIRECTORY_SEPARATOR] = '${PradoFramework}' . DIRECTORY_SEPARATOR;
255
			if (isset($aRpl[DIRECTORY_SEPARATOR])) {
256
				unset($aRpl[DIRECTORY_SEPARATOR]);
257
			}
258
			$aRpl = array_reverse($aRpl, true);
259
		}
260
261
		return str_replace(array_keys($aRpl), $aRpl, $value);
262
	}
263
	/**
264
	 * Displays exception information.
265
	 * Exceptions are displayed with rich context information, including
266
	 * the call stack and the context source code.
267
	 * This method is only invoked when application is in <b>Debug</b> mode.
268
	 * @param Exception $exception exception instance
269
	 */
270
	protected function displayException($exception)
271
	{
272
		if (php_sapi_name() === 'cli') {
273
			echo $exception->getMessage() . "\n";
274
			echo $this->getExactTraceAsString($exception);
275
			return;
276
		}
277
278
		if ($exception instanceof TTemplateException) {
279
			$fileName = $exception->getTemplateFile();
280
			$lines = empty($fileName) ? explode("\n", $exception->getTemplateSource()) : @file($fileName);
281
			$source = $this->getSourceCode($lines, $exception->getLineNumber());
282
			if ($fileName === '') {
283
				$fileName = '---embedded template---';
284
			}
285
			$errorLine = $exception->getLineNumber();
286
		} else {
287
			if (($trace = $this->getExactTrace($exception)) !== null) {
288
				$fileName = $trace['file'];
289
				$errorLine = $trace['line'];
290
			} else {
291
				$fileName = $exception->getFile();
292
				$errorLine = $exception->getLine();
293
			}
294
			$source = $this->getSourceCode(@file($fileName), $errorLine);
295
		}
296
297
		if ($this->getApplication()->getMode() === TApplicationMode::Debug) {
298
			$version = $_SERVER['SERVER_SOFTWARE'] . ' <a href="https://github.com/pradosoft/prado">PRADO</a>/' . Prado::getVersion();
299
		} else {
300
			$version = '';
301
		}
302
303
		$tokens = [
304
			'%%ErrorType%%' => get_class($exception),
305
			'%%ErrorMessage%%' => $this->addLink(htmlspecialchars($exception->getMessage())),
306
			'%%SourceFile%%' => htmlspecialchars($this->hidePrivatePathParts($fileName)) . ' (' . $errorLine . ')',
307
			'%%SourceCode%%' => $source,
308
			'%%StackTrace%%' => htmlspecialchars($this->getExactTraceAsString($exception)),
309
			'%%Version%%' => $version,
310
			'%%Time%%' => @strftime('%Y-%m-%d %H:%M', time())
311
		];
312
313
		$content = $this->getExceptionTemplate($exception);
314
315
		echo strtr($content, $tokens);
316
	}
317
318
	/**
319
	 * Retrieves the template used for displaying internal exceptions.
320
	 * Internal exceptions will be displayed with source code causing the exception.
321
	 * This occurs when the application is in debug mode.
322
	 * @param Exception $exception the exception to be displayed
323
	 * @return string the template content
324
	 */
325
	protected function getExceptionTemplate($exception)
326
	{
327
		$lang = Prado::getPreferredLanguage();
328
		$exceptionFile = Prado::getFrameworkPath() . '/Exceptions/templates/' . self::EXCEPTION_FILE_NAME . '-' . $lang . '.html';
329
		if (!is_file($exceptionFile)) {
330
			$exceptionFile = Prado::getFrameworkPath() . '/Exceptions/templates/' . self::EXCEPTION_FILE_NAME . '.html';
331
		}
332
		if (($content = @file_get_contents($exceptionFile)) === false) {
333
			die("Unable to open exception template file '$exceptionFile'.");
334
		}
335
		return $content;
336
	}
337
338
	/**
339
	 * Retrieves the template used for displaying external exceptions.
340
	 * External exceptions are those displayed to end-users. They do not contain
341
	 * error source code. Therefore, you might want to override this method
342
	 * to provide your own error template for displaying certain external exceptions.
343
	 * The following tokens in the template will be replaced with corresponding content:
344
	 * %%StatusCode%% : the status code of the exception
345
	 * %%ErrorMessage%% : the error message (HTML encoded).
346
	 * %%ServerAdmin%% : the server admin information (retrieved from Web server configuration)
347
	 * %%Version%% : the version information of the Web server.
348
	 * %%Time%% : the time the exception occurs at
349
	 *
350
	 * @param int $statusCode status code (such as 404, 500, etc.)
351
	 * @param Exception $exception the exception to be displayed
352
	 * @return string the template content
353
	 */
354
	protected function getErrorTemplate($statusCode, $exception)
355
	{
356
		$base = $this->getErrorTemplatePath() . DIRECTORY_SEPARATOR . self::ERROR_FILE_NAME;
357
		$lang = Prado::getPreferredLanguage();
358
		if (is_file("$base$statusCode-$lang.html")) {
359
			$errorFile = "$base$statusCode-$lang.html";
360
		} elseif (is_file("$base$statusCode.html")) {
361
			$errorFile = "$base$statusCode.html";
362
		} elseif (is_file("$base-$lang.html")) {
363
			$errorFile = "$base-$lang.html";
364
		} else {
365
			$errorFile = "$base.html";
366
		}
367
		if (($content = @file_get_contents($errorFile)) === false) {
368
			die("Unable to open error template file '$errorFile'.");
369
		}
370
		return $content;
371
	}
372
373
	private function getExactTrace($exception)
374
	{
375
		$result = null;
376 View Code Duplication
		if ($exception instanceof TPhpFatalErrorException &&
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
377
			function_exists('xdebug_get_function_stack')) {
378
			$trace = array_slice(array_reverse(xdebug_get_function_stack()), self::FATAL_ERROR_TRACE_DROP_LINES, -1);
379
		} else {
380
			$trace = $exception->getTrace();
381
		}
382
383
		// if PHP exception, we want to show the 2nd stack level context
384
		// because the 1st stack level is of little use (it's in error handler)
385
		if ($exception instanceof TPhpErrorException) {
386
			if (isset($trace[0]['file'])) {
387
				$result = $trace[0];
388
			} elseif (isset($trace[1])) {
389
				$result = $trace[1];
390
			}
391
		} elseif ($exception instanceof TInvalidOperationException) {
392
			// in case of getter or setter error, find out the exact file and row
393
			if (($result = $this->getPropertyAccessTrace($trace, '__get')) === null) {
394
				$result = $this->getPropertyAccessTrace($trace, '__set');
395
			}
396
		}
397 View Code Duplication
		if ($result !== null && strpos($result['file'], ': eval()\'d code') !== false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
398
			return null;
399
		}
400
401
		return $result;
402
	}
403
404
	private function getExactTraceAsString($exception)
405
	{
406
		if ($exception instanceof TPhpFatalErrorException &&
407
			function_exists('xdebug_get_function_stack')) {
408
			$trace = array_slice(array_reverse(xdebug_get_function_stack()), self::FATAL_ERROR_TRACE_DROP_LINES, -1);
409
			$txt = '';
410
			$row = 0;
411
412
			// try to mimic Exception::getTraceAsString()
413
			foreach ($trace as $line) {
414
				if (array_key_exists('function', $line)) {
415
					$func = $line['function'] . '(' . implode(',', $line['params']) . ')';
416
				} else {
417
					$func = 'unknown';
418
				}
419
420
				$txt .= '#' . $row . ' ' . $this->hidePrivatePathParts($line['file']) . '(' . $line['line'] . '): ' . $func . "\n";
421
				$row++;
422
			}
423
424
			return $txt;
425
		}
426
427
		return $this->hidePrivatePathParts($exception->getTraceAsString());
428
	}
429
430
	private function getPropertyAccessTrace($trace, $pattern)
431
	{
432
		$result = null;
433
		foreach ($trace as $t) {
434
			if (isset($t['function']) && $t['function'] === $pattern) {
435
				$result = $t;
436
			} else {
437
				break;
438
			}
439
		}
440
		return $result;
441
	}
442
443
	private function getSourceCode($lines, $errorLine)
444
	{
445
		$beginLine = $errorLine - self::SOURCE_LINES >= 0 ? $errorLine - self::SOURCE_LINES : 0;
446
		$endLine = $errorLine + self::SOURCE_LINES <= count($lines) ? $errorLine + self::SOURCE_LINES : count($lines);
447
448
		$source = '';
449
		for ($i = $beginLine; $i < $endLine; ++$i) {
450
			if ($i === $errorLine - 1) {
451
				$line = htmlspecialchars(sprintf("%04d: %s", $i + 1, str_replace("\t", '    ', $lines[$i])));
452
				$source .= "<div class=\"error\">" . $line . "</div>";
453
			} else {
454
				$source .= htmlspecialchars(sprintf("%04d: %s", $i + 1, str_replace("\t", '    ', $lines[$i])));
455
			}
456
		}
457
		return $source;
458
	}
459
460
	private function addLink($message)
461
	{
462
		if (null !== ($class = $this->getErrorClassNameSpace($message))) {
463
			return str_replace($class['name'], '<a href="' . $class['url'] . '" target="_blank">' . $class['name'] . '</a>', $message);
464
		}
465
		return $message;
466
	}
467
468
	private function getErrorClassNameSpace($message)
469
	{
470
		$matches = [];
471
		preg_match('/\b(T[A-Z]\w+)\b/', $message, $matches);
472
		if (is_array($matches) && count($matches) > 0) {
473
			$class = $matches[0];
474
			try {
475
				$function = new \ReflectionClass($class);
476
			} catch (\Exception $e) {
477
				return null;
478
			}
479
			$classname = $function->getNamespaceName();
480
			return [
481
				'url' => 'http://pradosoft.github.io/docs/manual/class-' . str_replace('\\', '.', (string) $classname) . '.' . $class . '.html',
482
				'name' => $class,
483
			];
484
		}
485
		return null;
486
	}
487
}
488