1 <?php
// @codingStandardsIgnoreFile
4 * @copyright 2001-2019 WoltLab GmbH
5 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
6 * @package WoltLabSuite\Core
11 // set exception handler
12 set_exception_handler([WCF
::class, 'handleException']);
13 // set php error handler
14 set_error_handler([WCF
::class, 'handleError'], E_ALL
);
15 // set shutdown function
16 register_shutdown_function([WCF
::class, 'destruct']);
17 // set autoload function
18 spl_autoload_register([WCF
::class, 'autoload']);
20 spl_autoload_register(function ($className) {
22 * @deprecated 5.3 This file is a compatibility layer mapping from Leafo\\ to ScssPhp\\
25 if (substr($className, 0, strlen($leafo)) === $leafo) {
26 class_alias('ScssPhp\\'.substr($className, strlen($leafo)), $className, true);
31 * Escapes a string for use in sql query.
33 * @see \wcf\system\database\Database::escapeString()
34 * @param string $string
37 function escapeString($string) {
38 return WCF
::getDB()->escapeString($string);
42 * Helper method to output debug data for all passed variables,
43 * uses `print_r()` for arrays and objects, `var_dump()` otherwise.
48 $args = func_get_args();
49 $length = count($args);
51 echo "ERROR: No arguments provided.<hr>";
54 for ($i = 0; $i < $length; $i++
) {
57 echo "<h2>Argument {$i} (" . gettype($arg) . ")</h2>";
59 if (is_array($arg) ||
is_object($arg)) {
70 $backtrace = debug_backtrace();
72 // output call location to help finding these debug outputs again
73 echo "wcfDebug() called in {$backtrace[0]['file']} on line {$backtrace[0]['line']}";
80 // define DOCUMENT_ROOT on IIS if not set
81 if (PHP_EOL
== "\r\n") {
82 if (!isset($_SERVER['DOCUMENT_ROOT']) && isset($_SERVER['SCRIPT_FILENAME'])) {
83 $_SERVER['DOCUMENT_ROOT'] = str_replace('\\', '/', substr($_SERVER['SCRIPT_FILENAME'], 0, 0 - strlen($_SERVER['PHP_SELF'])));
85 if (!isset($_SERVER['DOCUMENT_ROOT']) && isset($_SERVER['PATH_TRANSLATED'])) {
86 $_SERVER['DOCUMENT_ROOT'] = str_replace('\\', '/', substr(str_replace('\\\\', '\\', $_SERVER['PATH_TRANSLATED']), 0, 0 - strlen($_SERVER['PHP_SELF'])));
89 if (!isset($_SERVER['REQUEST_URI'])) {
90 $_SERVER['REQUEST_URI'] = substr($_SERVER['PHP_SELF'], 1);
91 if (isset($_SERVER['QUERY_STRING'])) {
92 $_SERVER['REQUEST_URI'] .= '?' . $_SERVER['QUERY_STRING'];
97 // setting global gzip compression breaks output buffering
98 if (@ini_get
('zlib.output_compression')) {
99 @ini_set
('zlib.output_compression', '0');
102 if (!function_exists('is_countable')) {
103 function is_countable($var) {
104 return is_array($var) ||
$var instanceof Countable ||
$var instanceof ResourceBundle ||
$var instanceof SimpleXmlElement
;
110 function getRequestId(): string {
111 if (!defined('WCF_REQUEST_ID_HEADER') ||
!WCF_REQUEST_ID_HEADER
) return '';
113 return $_SERVER[WCF_REQUEST_ID_HEADER
] ??
'';
116 function getMinorVersion(): string {
117 return preg_replace('/^(\d+\.\d+)\..*$/', '\\1', WCF_VERSION
);
120 #[Attribute(\Attribute::TARGET_PARAMETER)]
121 class SensitiveArgument
126 namespace wcf\functions\exception
{
128 use wcf\system\exception\IExtraInformationException
;
129 use wcf\system\exception\ILoggingAwareException
;
130 use wcf\system\exception\SystemException
;
131 use wcf\util\FileUtil
;
132 use wcf\util\StringUtil
;
135 * If the stacktrace contains a compiled template, the context of the relevant template line
136 * is returned, otherwise an empty array is returned.
138 function getTemplateContextLines(\Throwable
$e): array {
140 $contextLineCount = 5;
141 foreach ($e->getTrace() as $traceEntry) {
142 if (isset($traceEntry['file']) && \
preg_match('~/templates/compiled/.+\.php$~',
143 $traceEntry['file'])) {
144 $startLine = $traceEntry['line'] - $contextLineCount;
145 $relativeErrorLine = $contextLineCount;
146 if ($startLine < 0) {
148 $relativeErrorLine = $traceEntry['line'] - 1;
151 $file = \fopen
($traceEntry['file'], 'r');
156 for ($line = 0; $line < $startLine; $line++
) {
157 if (\
substr(\fgets
($file, 1024), -1) !== "\n") {
158 // We don't want to handle a file where lines exceed 1024 Bytes.
163 $maxLineCount = 2 * $contextLineCount +
1;
165 while (!\feof
($file) && \
count($lines) < $maxLineCount) {
166 $line = \fgets
($file, 1024);
167 if (\
substr($line, -1) !== "\n" && !\feof
($file)) {
168 // We don't want to handle a file where lines exceed 1024 Bytes.
172 if (count($lines) === $relativeErrorLine - 1) {
173 $line = "====> {$line}";
183 catch (\Throwable
$e) {
184 // Ignore errors while extracting the template context to be saved in the exception log.
191 * Logs the given Throwable.
193 * @param string $logFile The log file to use. If set to `null` the default log file will be used and the variable contents will be replaced by the actual path.
194 * @return string The ID of the log entry.
196 function logThrowable(\Throwable
$e, &$logFile = null): string {
197 if ($logFile === null) $logFile = WCF_DIR
. 'log/' . gmdate('Y-m-d', TIME_NOW
) . '.txt';
200 $stripNewlines = function ($item) {
201 return str_replace("\n", ' ', $item);
204 $getExtraInformation = function (\Throwable
$e) {
205 $extraInformation = [];
207 if ($e instanceof IExtraInformationException
) {
208 $extraInformation = $e->getExtraInformation();
211 $templateContextLines = getTemplateContextLines($e);
212 if (!empty($templateContextLines)) {
213 $extraInformation[] = [
215 \
implode("", $templateContextLines),
219 return !empty($extraInformation) ?
base64_encode(serialize($extraInformation)) : "-";
222 // don't forget to update ExceptionLogUtil / ExceptionLogViewPage, when changing the log file format
223 $message = gmdate('r', TIME_NOW
)."\n".
224 'Message: '.$stripNewlines($e->getMessage())."\n".
225 'PHP version: '.phpversion()."\n".
226 'WoltLab Suite version: '.WCF_VERSION
."\n".
227 'Request URI: '.$stripNewlines(($_SERVER['REQUEST_METHOD'] ??
'').' '.($_SERVER['REQUEST_URI'] ??
'')).(\wcf\
getRequestId() ?
' ('.\wcf\
getRequestId().')' : '')."\n".
228 'Referrer: '.$stripNewlines($_SERVER['HTTP_REFERER'] ??
'')."\n".
229 'User Agent: '.$stripNewlines($_SERVER['HTTP_USER_AGENT'] ??
'')."\n".
230 'Peak Memory Usage: '.memory_get_peak_usage().'/'.FileUtil
::getMemoryLimit()."\n";
233 $message .= "======\n".
234 'Error Class: '.get_class($prev)."\n".
235 'Error Message: '.$stripNewlines($prev->getMessage())."\n".
236 'Error Code: '.$stripNewlines($prev->getCode())."\n".
237 'File: '.$stripNewlines($prev->getFile()).' ('.$prev->getLine().')'."\n".
238 'Extra Information: ' . $getExtraInformation($prev) . "\n".
239 'Stack Trace: '.json_encode(array_map(function ($item) {
240 $item['args'] = array_map(function ($item) {
241 switch (gettype($item)) {
243 return get_class($item);
245 return array_map(function () {
249 return 'resource('.get_resource_type($item).')';
256 }, sanitizeStacktrace($prev, true)))."\n";
258 while ($prev = $prev->getPrevious());
260 // calculate Exception-ID
261 $exceptionID = sha1($message);
262 $entry = "<<<<<<<<".$exceptionID."<<<<\n".$message."<<<<\n\n";
264 file_put_contents($logFile, $entry, FILE_APPEND
);
266 // let the Exception know it has been logged
268 $e instanceof ILoggingAwareException
269 ||
(method_exists($e, 'finalizeLog') && is_callable([$e, 'finalizeLog']))
271 /** @var ILoggingAwareException $e */
272 $e->finalizeLog($exceptionID, $logFile);
279 * Pretty prints the given Throwable. It is recommended to `exit;`
280 * the request after calling this function.
284 function printThrowable(\Throwable
$e) {
285 $exceptionID = logThrowable($e, $logFile);
286 if (\wcf\
getRequestId()) $exceptionID .= '/'.\wcf\
getRequestId();
288 $exceptionTitle = $exceptionSubtitle = $exceptionExplanation = '';
289 $logFile = sanitizePath($logFile);
291 if (WCF
::getLanguage() !== null) {
292 $exceptionTitle = WCF
::getLanguage()->get('wcf.global.exception.title', true);
293 $exceptionSubtitle = str_replace('{$exceptionID}', $exceptionID, WCF
::getLanguage()->get('wcf.global.exception.subtitle', true));
294 $exceptionExplanation = str_replace('{$logFile}', $logFile, WCF
::getLanguage()->get('wcf.global.exception.explanation', true));
297 catch (\Throwable
$e) {
301 if (!$exceptionTitle ||
!$exceptionSubtitle ||
!$exceptionExplanation) {
302 // one or more failed, fallback to english
303 $exceptionTitle = 'An error has occurred';
304 $exceptionSubtitle = 'Internal error code: <span class="exceptionInlineCodeWrapper"><span class="exceptionInlineCode">'.$exceptionID.'</span></span>';
305 $exceptionExplanation = <<<EXPLANATION
306 <p class="exceptionSubtitle">What happened?</p>
307 <p class="exceptionText">An error has occured while trying to handle your request and execution has been terminated. Please forward the above error code to the site administrator.</p>
308 <p class="exceptionText"> </p> <!-- required to ensure spacing after copy & paste -->
309 <p class="exceptionText">
310 The error code can be used by an administrator to lookup the full error message in the Administration Control Panel via “Logs » Errors”.
311 In addition the error has been written to the log file located at <span class="exceptionInlineCodeWrapper"><span class="exceptionInlineCode">{$logFile}</span></span> and can be accessed with a FTP program or similar.
313 <p class="exceptionText"> </p> <!-- required to ensure spacing after copy & paste -->
314 <p class="exceptionText">Notice: The error code was randomly generated and has no use beyond looking up the full message.</p>
320 * A notice on the HTML used below:
322 * It might appear a bit weird to use <p> all over the place where semantically
323 * other elements would fit in way better. The reason behind this is that we avoid
324 * inheriting unwanted styles (e.g. exception displayed in an overlay) and that
325 * the output needs to be properly readable when copied & pasted somewhere.
327 * Besides the visual appearance, the output was built to provide a maximum of
328 * compatibility and readability when pasted somewhere else, e.g. a WYSIWYG editor
329 * without the potential of messing up the formatting and thus harming the readability.
334 <meta charset
="utf-8">
335 <?php
if (!defined('EXCEPTION_PRIVACY') || EXCEPTION_PRIVACY
!== 'private') { ?
>
336 <title
>Fatal Error
: <?php
echo StringUtil
::encodeHTML($e->getMessage()); ?
></title
>
338 <title
>Fatal Error
</title
>
340 <meta name
="viewport" content
="width=device-width, initial-scale=1">
343 background
-color
: rgb(250, 250, 250);
344 color
: rgb(44, 62, 80);
349 .exceptionContainer
{
350 box
-sizing
: border
-box
;
351 font
-family
: 'Segoe UI', 'Lucida Grande', 'Helvetica Neue', Helvetica
, Arial
, sans
-serif
;
353 padding
-bottom
: 20px
;
356 .exceptionContainer
* {
364 background
-color
: rgb(58, 109, 156);
374 .exceptionErrorCode
{
379 .exceptionErrorCode
.exceptionInlineCode
{
380 background
-color
: rgb(43, 79, 113);
383 font
-family
: monospace
;
389 border
-bottom
: 1px solid
rgb(238, 238, 238);
393 padding
-bottom
: 10px
;
396 .exceptionContainer
> .exceptionBoundary
{
400 .exceptionText
.exceptionInlineCodeWrapper
{
401 border
: 1px solid
rgb(169, 169, 169);
406 .exceptionText
.exceptionInlineCode
{
407 font
-family
: monospace
;
411 .exceptionFieldTitle
{
412 color
: rgb(59, 109, 169);
415 .exceptionFieldTitle
.exceptionColon
{
416 /* hide colon in browser, but will be visible after copy & paste */
420 .exceptionFieldValue
{
425 pre
.exceptionFieldValue
{
427 white
-space
: pre
-wrap
;
430 .exceptionSystemInformation
,
431 .exceptionErrorDetails
,
432 .exceptionStacktrace
{
433 list-style
-type
: none
;
436 .exceptionSystemInformation
> li
:not(:first
-child
),
437 .exceptionErrorDetails
> li
:not(:first
-child
) {
441 .exceptionStacktrace
{
445 padding
-bottom
: 20px
;
448 .exceptionStacktraceFile
,
449 .exceptionStacktraceFile span
,
450 .exceptionStacktraceCall
,
451 .exceptionStacktraceCall span
{
452 font
-family
: monospace
!important
;
453 white
-space
: nowrap
!important
;
456 .exceptionStacktraceCall +
.exceptionStacktraceFile
{
460 .exceptionStacktraceCall
{
464 .exceptionStacktraceCall
,
465 .exceptionStacktraceCall span
{
466 color
: rgb(102, 102, 102) !important
;
467 font
-size
: 13px
!important
;
471 @media
(max
-width
: 767px
) {
477 .exceptionText
.exceptionInlineCodeWrapper
{
478 display
: inline
-block
;
482 .exceptionErrorCode
.exceptionInlineCode
{
489 @media
(min
-width
: 768px
) {
497 .exceptionSystemInformation
{
502 .exceptionSystemInformation1
,
503 .exceptionSystemInformation3
,
504 .exceptionSystemInformation5
{
506 margin
: 0 0 10px
0 !important
;
509 .exceptionSystemInformation2
,
510 .exceptionSystemInformation4
,
511 .exceptionSystemInformation6
{
512 flex
: 0 0 calc(100%
- 210px
);
513 margin
: 0 0 10px
10px
!important
;
514 max
-width
: calc(100%
- 210px
);
517 .exceptionSystemInformation1
{ order
: 1; }
518 .exceptionSystemInformation2
{ order
: 2; }
519 .exceptionSystemInformation3
{ order
: 3; }
520 .exceptionSystemInformation4
{ order
: 4; }
521 .exceptionSystemInformation5
{ order
: 5; }
522 .exceptionSystemInformation6
{ order
: 6; }
524 .exceptionSystemInformation
.exceptionFieldValue
{
526 text
-overflow
: ellipsis
;
532 <body
class="exceptionBody">
533 <div
class="exceptionContainer">
534 <div
class="exceptionHeader">
535 <div
class="exceptionBoundary">
536 <p
class="exceptionTitle"><?php
echo $exceptionTitle; ?
></p
>
537 <p
class="exceptionErrorCode"><?php
echo str_replace('{$exceptionID}', $exceptionID, $exceptionSubtitle); ?
></p
>
541 <div
class="exceptionBoundary">
542 <?php
echo $exceptionExplanation; ?
>
544 <?php
if (!defined('EXCEPTION_PRIVACY') || EXCEPTION_PRIVACY
!== 'private') { ?
>
545 <div
class="exceptionBoundary">
546 <p
class="exceptionSubtitle">System Information
</p
>
547 <ul
class="exceptionSystemInformation">
548 <li
class="exceptionSystemInformation1">
549 <p
class="exceptionFieldTitle">PHP Version
<span
class="exceptionColon">:</span
></p
>
550 <p
class="exceptionFieldValue"><?php
echo StringUtil
::encodeHTML(phpversion()); ?
></p
>
552 <li
class="exceptionSystemInformation3">
553 <p
class="exceptionFieldTitle">WoltLab Suite Core
<span
class="exceptionColon">:</span
></p
>
554 <p
class="exceptionFieldValue"><?php
echo StringUtil
::encodeHTML(WCF_VERSION
); ?
></p
>
556 <li
class="exceptionSystemInformation5">
557 <p
class="exceptionFieldTitle">Peak Memory Usage
<span
class="exceptionColon">:</span
></p
>
558 <p
class="exceptionFieldValue"><?php
echo round(memory_get_peak_usage() / 1024 / 1024, 3); ?
>/<?php
echo round(FileUtil
::getMemoryLimit() / 1024 / 1024, 3); ?
> MiB
</p
>
560 <li
class="exceptionSystemInformation2">
561 <p
class="exceptionFieldTitle">Request URI
<span
class="exceptionColon">:</span
></p
>
562 <p
class="exceptionFieldValue"><?php
if (isset($_SERVER['REQUEST_METHOD'])) echo StringUtil
::encodeHTML($_SERVER['REQUEST_METHOD']); ?
> <?php
if (isset($_SERVER['REQUEST_URI'])) echo StringUtil
::encodeHTML($_SERVER['REQUEST_URI']); ?
></p
>
564 <li
class="exceptionSystemInformation4">
565 <p
class="exceptionFieldTitle">Referrer
<span
class="exceptionColon">:</span
></p
>
566 <p
class="exceptionFieldValue"><?php
if (isset($_SERVER['HTTP_REFERER'])) echo StringUtil
::encodeHTML($_SERVER['HTTP_REFERER']); ?
></p
>
568 <li
class="exceptionSystemInformation6">
569 <p
class="exceptionFieldTitle">User Agent
<span
class="exceptionColon">:</span
></p
>
570 <p
class="exceptionFieldValue"><?php
if (isset($_SERVER['HTTP_USER_AGENT'])) echo StringUtil
::encodeHTML($_SERVER['HTTP_USER_AGENT']); ?
></p
>
580 $exceptions[] = $current;
582 while ($current = $current->getPrevious());
584 $e = array_pop($exceptions);
587 <div
class="exceptionBoundary">
588 <p
class="exceptionSubtitle"><?php
if (!empty($exceptions) && $first) { echo "Original "; } else if (empty($exceptions) && !$first) { echo "Final "; } ?
>Error
</p
>
589 <?php
if ($e instanceof SystemException
&& $e->getDescription()) { ?
>
590 <p
class="exceptionText"><?php
echo $e->getDescription(); ?
></p
>
592 <ul
class="exceptionErrorDetails">
594 <p
class="exceptionFieldTitle">Error Type
<span
class="exceptionColon">:</span
></p
>
595 <p
class="exceptionFieldValue"><?php
echo StringUtil
::encodeHTML(get_class($e)); ?
></p
>
598 <p
class="exceptionFieldTitle">Error Message
<span
class="exceptionColon">:</span
></p
>
599 <p
class="exceptionFieldValue"><?php
echo StringUtil
::encodeHTML($e->getMessage()); ?
></p
>
601 <?php
if ($e->getCode()) { ?
>
603 <p
class="exceptionFieldTitle">Error Code
<span
class="exceptionColon">:</span
></p
>
604 <p
class="exceptionFieldValue"><?php
echo StringUtil
::encodeHTML($e->getCode()); ?
></p
>
608 <p
class="exceptionFieldTitle">File
<span
class="exceptionColon">:</span
></p
>
609 <p
class="exceptionFieldValue" style
="word-break: break-all"><?php
echo StringUtil
::encodeHTML(sanitizePath($e->getFile())); ?
> (<?php
echo $e->getLine(); ?
>)</p
>
613 if ($e instanceof SystemException
) {
618 $reflection = new \
ReflectionClass($e);
619 $property = $reflection->getProperty('information');
620 $property->setAccessible(true);
621 if ($property->getValue($e)) {
622 throw new \
Exception("Using the 'information' property of SystemException is not supported any more.");
625 if ($e instanceof IExtraInformationException
) {
626 foreach ($e->getExtraInformation() as list($key, $value)) {
629 <p
class="exceptionFieldTitle"><?php
echo StringUtil
::encodeHTML($key); ?
><span
class="exceptionColon">:</span
></p
>
630 <p
class="exceptionFieldValue"><?php
echo StringUtil
::encodeHTML($value); ?
></p
>
636 $templateContextLines = getTemplateContextLines($e);
637 if (!empty($templateContextLines)) {
640 <p
class="exceptionFieldTitle">Template Context
<span
class="exceptionColon">:</span
></p
>
641 <pre
class="exceptionFieldValue"><?php
echo StringUtil
::encodeHTML(implode("", $templateContextLines));?
></pre
>
647 <p
class="exceptionFieldTitle">Stack Trace
<span
class="exceptionColon">:</span
></p
>
648 <ul
class="exceptionStacktrace">
650 $trace = sanitizeStacktrace($e);
651 for ($i = 0, $max = count($trace); $i < $max; $i++
) {
653 <li
class="exceptionStacktraceFile"><?php
echo '#'.$i.' '.StringUtil
::encodeHTML($trace[$i]['file']).' ('.$trace[$i]['line'].')'.':'; ?
></li
>
654 <li
class="exceptionStacktraceCall">
656 echo $trace[$i]['class'].$trace[$i]['type'].$trace[$i]['function'].'(';
657 echo implode(', ', array_map(function ($item) {
658 switch (gettype($item)) {
665 return "'".addcslashes(StringUtil
::encodeHTML($item), "\\'")."'";
667 return $item ?
'true' : 'false';
669 $keys = array_keys($item);
670 if (count($keys) > 5) return "[ ".count($keys)." items ]";
671 return '[ '.implode(', ', array_map(function ($item) {
675 return get_class($item);
677 return 'resource('.get_resource_type($item).')';
678 case 'resource (closed)':
679 return 'resource (closed)';
682 throw new \
LogicException('Unreachable');
683 }, $trace[$i]['args']));
693 } while ($e = array_pop($exceptions));
703 * Returns the stack trace of the given Throwable with sensitive
704 * information removed.
706 * @param bool $ignorePaths If set to `true`: Don't call `sanitizePath`.
709 function sanitizeStacktrace(\Throwable
$e, bool $ignorePaths = false) {
710 $trace = $e->getTrace();
712 return array_map(function ($item) use ($ignorePaths) {
713 if (!isset($item['file'])) $item['file'] = '[internal function]';
714 if (!isset($item['line'])) $item['line'] = '?';
715 if (!isset($item['class'])) $item['class'] = '';
716 if (!isset($item['type'])) $item['type'] = '';
717 if (!isset($item['args'])) $item['args'] = [];
719 if (!empty($item['args'])) {
720 if ($item['class']) {
721 $function = new \
ReflectionMethod($item['class'], $item['function']);
724 $function = new \
ReflectionFunction($item['function']);
727 $parameters = $function->getParameters();
729 foreach ($parameters as $parameter) {
730 $isSensitive = false;
732 \
method_exists($parameter, 'getAttributes')
733 && !empty($parameter->getAttributes(\wcf\SensitiveArgument
::class))
738 '/(?:^(?:password|passphrase|secret)|(?:Password|Passphrase|Secret))/',
739 $parameter->getName()
744 if ($isSensitive && isset($item['args'][$i])) {
745 $item['args'][$i] = '[redacted]';
750 // strip database credentials
752 preg_match('~\\\\?wcf\\\\system\\\\database\\\\[a-zA-Z]*Database~', $item['class'])
753 ||
$item['class'] === 'PDO'
755 if ($item['function'] === '__construct') {
756 $item['args'] = array_map(function () {
764 $item['args'] = array_map(function ($item) {
765 if (!is_string($item)) return $item;
767 if (preg_match('~^('.preg_quote($_SERVER['DOCUMENT_ROOT'], '~').'|'.preg_quote(WCF_DIR
, '~').')~', $item)) {
768 $item = sanitizePath($item);
774 $item['file'] = sanitizePath($item['file']);
782 * Returns the given path relative to `WCF_DIR`, unless both,
783 * `EXCEPTION_PRIVACY` is `public` and the debug mode is enabled.
785 * @param string $path
788 function sanitizePath(string $path): string {
789 if (WCF
::debugModeIsEnabled() && defined('EXCEPTION_PRIVACY') && EXCEPTION_PRIVACY
=== 'public') {
793 return '*/'.FileUtil
::removeTrailingSlash(FileUtil
::getRelativePath(WCF_DIR
, $path));