3cecacd3ba2789d1fbdb2a15aad868f5ef9a2730
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / core.functions.php
1 <?php // @codingStandardsIgnoreFile
2 /**
3 * @author Marcel Werk
4 * @copyright 2001-2019 WoltLab GmbH
5 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
6 * @package WoltLabSuite\Core
7 */
8 namespace {
9 use wcf\system\WCF;
10
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']);
19
20 spl_autoload_register(function ($className) {
21 /**
22 * @deprecated 5.3 This file is a compatibility layer mapping from Leafo\\ to ScssPhp\\
23 */
24 $leafo = 'Leafo\\';
25 if (substr($className, 0, strlen($leafo)) === $leafo) {
26 class_alias('ScssPhp\\'.substr($className, strlen($leafo)), $className, true);
27 }
28 });
29
30 /**
31 * Escapes a string for use in sql query.
32 *
33 * @see \wcf\system\database\Database::escapeString()
34 * @param string $string
35 * @return string
36 */
37 function escapeString($string) {
38 return WCF::getDB()->escapeString($string);
39 }
40
41 /**
42 * Helper method to output debug data for all passed variables,
43 * uses `print_r()` for arrays and objects, `var_dump()` otherwise.
44 */
45 function wcfDebug() {
46 echo "<pre>";
47
48 $args = func_get_args();
49 $length = count($args);
50 if ($length === 0) {
51 echo "ERROR: No arguments provided.<hr>";
52 }
53 else {
54 for ($i = 0; $i < $length; $i++) {
55 $arg = $args[$i];
56
57 echo "<h2>Argument {$i} (" . gettype($arg) . ")</h2>";
58
59 if (is_array($arg) || is_object($arg)) {
60 print_r($arg);
61 }
62 else {
63 var_dump($arg);
64 }
65
66 echo "<hr>";
67 }
68 }
69
70 $backtrace = debug_backtrace();
71
72 // output call location to help finding these debug outputs again
73 echo "wcfDebug() called in {$backtrace[0]['file']} on line {$backtrace[0]['line']}";
74
75 echo "</pre>";
76
77 exit;
78 }
79
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'])));
84 }
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'])));
87 }
88
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'];
93 }
94 }
95 }
96
97 // setting global gzip compression breaks output buffering
98 if (@ini_get('zlib.output_compression')) {
99 @ini_set('zlib.output_compression', '0');
100 }
101
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;
105 }
106 }
107 }
108
109 namespace wcf {
110 function getRequestId(): string {
111 if (!defined('WCF_REQUEST_ID_HEADER') || !WCF_REQUEST_ID_HEADER) return '';
112
113 return $_SERVER[WCF_REQUEST_ID_HEADER] ?? '';
114 }
115
116 function getMinorVersion(): string {
117 return preg_replace('/^(\d+\.\d+)\..*$/', '\\1', WCF_VERSION);
118 }
119
120 #[Attribute(\Attribute::TARGET_PARAMETER)]
121 class SensitiveArgument
122 {
123 }
124 }
125
126 namespace wcf\functions\exception {
127 use wcf\system\WCF;
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;
133
134 /**
135 * If the stacktrace contains a compiled template, the context of the relevant template line
136 * is returned, otherwise an empty array is returned.
137 */
138 function getTemplateContextLines(\Throwable $e): array {
139 try {
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) {
147 $startLine = 0;
148 $relativeErrorLine = $traceEntry['line'] - 1;
149 }
150
151 $file = \fopen($traceEntry['file'], 'r');
152 if (!$file) {
153 return [];
154 }
155
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.
159 return [];
160 }
161 }
162
163 $maxLineCount = 2 * $contextLineCount + 1;
164 $lines = [];
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.
169 return [];
170 }
171
172 if (count($lines) === $relativeErrorLine - 1) {
173 $line = "====> {$line}";
174 }
175
176 $lines[] = $line;
177 }
178
179 return $lines;
180 }
181 }
182 }
183 catch (\Throwable $e) {
184 // Ignore errors while extracting the template context to be saved in the exception log.
185 }
186
187 return [];
188 }
189
190 /**
191 * Logs the given Throwable.
192 *
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.
195 */
196 function logThrowable(\Throwable $e, &$logFile = null): string {
197 if ($logFile === null) $logFile = WCF_DIR . 'log/' . gmdate('Y-m-d', TIME_NOW) . '.txt';
198 touch($logFile);
199
200 $stripNewlines = function ($item) {
201 return str_replace("\n", ' ', $item);
202 };
203
204 $getExtraInformation = function (\Throwable $e) {
205 $extraInformation = [];
206
207 if ($e instanceof IExtraInformationException) {
208 $extraInformation = $e->getExtraInformation();
209 }
210
211 $templateContextLines = getTemplateContextLines($e);
212 if (!empty($templateContextLines)) {
213 $extraInformation[] = [
214 'Template Context',
215 \implode("", $templateContextLines),
216 ];
217 }
218
219 return !empty($extraInformation) ? base64_encode(serialize($extraInformation)) : "-";
220 };
221
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";
231 $prev = $e;
232 do {
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)) {
242 case 'object':
243 return get_class($item);
244 case 'array':
245 return array_map(function () {
246 return '[redacted]';
247 }, $item);
248 case 'resource':
249 return 'resource('.get_resource_type($item).')';
250 default:
251 return $item;
252 }
253 }, $item['args']);
254
255 return $item;
256 }, sanitizeStacktrace($prev, true)))."\n";
257 }
258 while ($prev = $prev->getPrevious());
259
260 // calculate Exception-ID
261 $exceptionID = sha1($message);
262 $entry = "<<<<<<<<".$exceptionID."<<<<\n".$message."<<<<\n\n";
263
264 file_put_contents($logFile, $entry, FILE_APPEND);
265
266 // let the Exception know it has been logged
267 if (
268 $e instanceof ILoggingAwareException
269 || (method_exists($e, 'finalizeLog') && is_callable([$e, 'finalizeLog']))
270 ) {
271 /** @var ILoggingAwareException $e */
272 $e->finalizeLog($exceptionID, $logFile);
273 }
274
275 return $exceptionID;
276 }
277
278 /**
279 * Pretty prints the given Throwable. It is recommended to `exit;`
280 * the request after calling this function.
281 *
282 * @throws \Exception
283 */
284 function printThrowable(\Throwable $e) {
285 $exceptionID = logThrowable($e, $logFile);
286 if (\wcf\getRequestId()) $exceptionID .= '/'.\wcf\getRequestId();
287
288 $exceptionTitle = $exceptionSubtitle = $exceptionExplanation = '';
289 $logFile = sanitizePath($logFile);
290 try {
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));
295 }
296 }
297 catch (\Throwable $e) {
298 // ignore
299 }
300
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">&nbsp;</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.
312 </p>
313 <p class="exceptionText">&nbsp;</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>
315 EXPLANATION;
316
317 }
318
319 /*
320 * A notice on the HTML used below:
321 *
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.
326 *
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.
330 */
331 ?><!DOCTYPE html>
332 <html>
333 <head>
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>
337 <?php } else { ?>
338 <title>Fatal Error</title>
339 <?php } ?>
340 <meta name="viewport" content="width=device-width, initial-scale=1">
341 <style>
342 .exceptionBody {
343 background-color: rgb(250, 250, 250);
344 color: rgb(44, 62, 80);
345 margin: 0;
346 padding: 0;
347 }
348
349 .exceptionContainer {
350 box-sizing: border-box;
351 font-family: 'Segoe UI', 'Lucida Grande', 'Helvetica Neue', Helvetica, Arial, sans-serif;
352 font-size: 14px;
353 padding-bottom: 20px;
354 }
355
356 .exceptionContainer * {
357 box-sizing: inherit;
358 line-height: 1.5em;
359 margin: 0;
360 padding: 0;
361 }
362
363 .exceptionHeader {
364 background-color: rgb(58, 109, 156);
365 padding: 30px 0;
366 }
367
368 .exceptionTitle {
369 color: #fff;
370 font-size: 28px;
371 font-weight: 300;
372 }
373
374 .exceptionErrorCode {
375 color: #fff;
376 margin-top: .5em;
377 }
378
379 .exceptionErrorCode .exceptionInlineCode {
380 background-color: rgb(43, 79, 113);
381 border-radius: 3px;
382 color: #fff;
383 font-family: monospace;
384 padding: 3px 10px;
385 white-space: nowrap;
386 }
387
388 .exceptionSubtitle {
389 border-bottom: 1px solid rgb(238, 238, 238);
390 font-size: 24px;
391 font-weight: 300;
392 margin-bottom: 15px;
393 padding-bottom: 10px;
394 }
395
396 .exceptionContainer > .exceptionBoundary {
397 margin-top: 30px;
398 }
399
400 .exceptionText .exceptionInlineCodeWrapper {
401 border: 1px solid rgb(169, 169, 169);
402 border-radius: 3px;
403 padding: 2px 5px;
404 }
405
406 .exceptionText .exceptionInlineCode {
407 font-family: monospace;
408 white-space: nowrap;
409 }
410
411 .exceptionFieldTitle {
412 color: rgb(59, 109, 169);
413 }
414
415 .exceptionFieldTitle .exceptionColon {
416 /* hide colon in browser, but will be visible after copy & paste */
417 opacity: 0;
418 }
419
420 .exceptionFieldValue {
421 font-size: 18px;
422 min-height: 1.5em;
423 }
424
425 pre.exceptionFieldValue {
426 font-size: 14px;
427 white-space: pre-wrap;
428 }
429
430 .exceptionSystemInformation,
431 .exceptionErrorDetails,
432 .exceptionStacktrace {
433 list-style-type: none;
434 }
435
436 .exceptionSystemInformation > li:not(:first-child),
437 .exceptionErrorDetails > li:not(:first-child) {
438 margin-top: 10px;
439 }
440
441 .exceptionStacktrace {
442 display: block;
443 margin-top: 5px;
444 overflow: auto;
445 padding-bottom: 20px;
446 }
447
448 .exceptionStacktraceFile,
449 .exceptionStacktraceFile span,
450 .exceptionStacktraceCall,
451 .exceptionStacktraceCall span {
452 font-family: monospace !important;
453 white-space: nowrap !important;
454 }
455
456 .exceptionStacktraceCall + .exceptionStacktraceFile {
457 margin-top: 5px;
458 }
459
460 .exceptionStacktraceCall {
461 padding-left: 40px;
462 }
463
464 .exceptionStacktraceCall,
465 .exceptionStacktraceCall span {
466 color: rgb(102, 102, 102) !important;
467 font-size: 13px !important;
468 }
469
470 /* mobile */
471 @media (max-width: 767px) {
472 .exceptionBoundary {
473 min-width: 320px;
474 padding: 0 10px;
475 }
476
477 .exceptionText .exceptionInlineCodeWrapper {
478 display: inline-block;
479 overflow: auto;
480 }
481
482 .exceptionErrorCode .exceptionInlineCode {
483 font-size: 13px;
484 padding: 2px 5px;
485 }
486 }
487
488 /* desktop */
489 @media (min-width: 768px) {
490 .exceptionBoundary {
491 margin: 0 auto;
492 max-width: 1400px;
493 min-width: 1200px;
494 padding: 0 10px;
495 }
496
497 .exceptionSystemInformation {
498 display: flex;
499 flex-wrap: wrap;
500 }
501
502 .exceptionSystemInformation1,
503 .exceptionSystemInformation3,
504 .exceptionSystemInformation5 {
505 flex: 0 0 200px;
506 margin: 0 0 10px 0 !important;
507 }
508
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);
515 }
516
517 .exceptionSystemInformation1 { order: 1; }
518 .exceptionSystemInformation2 { order: 2; }
519 .exceptionSystemInformation3 { order: 3; }
520 .exceptionSystemInformation4 { order: 4; }
521 .exceptionSystemInformation5 { order: 5; }
522 .exceptionSystemInformation6 { order: 6; }
523
524 .exceptionSystemInformation .exceptionFieldValue {
525 overflow: hidden;
526 text-overflow: ellipsis;
527 white-space: nowrap;
528 }
529 }
530 </style>
531 </head>
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>
538 </div>
539 </div>
540
541 <div class="exceptionBoundary">
542 <?php echo $exceptionExplanation; ?>
543 </div>
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>
551 </li>
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>
555 </li>
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>
559 </li>
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>
563 </li>
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>
567 </li>
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>
571 </li>
572 </ul>
573 </div>
574
575 <?php
576 $first = true;
577 $exceptions = [];
578 $current = $e;
579 do {
580 $exceptions[] = $current;
581 }
582 while ($current = $current->getPrevious());
583
584 $e = array_pop($exceptions);
585 do {
586 ?>
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>
591 <?php } ?>
592 <ul class="exceptionErrorDetails">
593 <li>
594 <p class="exceptionFieldTitle">Error Type<span class="exceptionColon">:</span></p>
595 <p class="exceptionFieldValue"><?php echo StringUtil::encodeHTML(get_class($e)); ?></p>
596 </li>
597 <li>
598 <p class="exceptionFieldTitle">Error Message<span class="exceptionColon">:</span></p>
599 <p class="exceptionFieldValue"><?php echo StringUtil::encodeHTML($e->getMessage()); ?></p>
600 </li>
601 <?php if ($e->getCode()) { ?>
602 <li>
603 <p class="exceptionFieldTitle">Error Code<span class="exceptionColon">:</span></p>
604 <p class="exceptionFieldValue"><?php echo StringUtil::encodeHTML($e->getCode()); ?></p>
605 </li>
606 <?php } ?>
607 <li>
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>
610 </li>
611
612 <?php
613 if ($e instanceof SystemException) {
614 ob_start();
615 $e->show();
616 ob_end_clean();
617
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.");
623 }
624 }
625 if ($e instanceof IExtraInformationException) {
626 foreach ($e->getExtraInformation() as list($key, $value)) {
627 ?>
628 <li>
629 <p class="exceptionFieldTitle"><?php echo StringUtil::encodeHTML($key); ?><span class="exceptionColon">:</span></p>
630 <p class="exceptionFieldValue"><?php echo StringUtil::encodeHTML($value); ?></p>
631 </li>
632 <?php
633 }
634 }
635
636 $templateContextLines = getTemplateContextLines($e);
637 if (!empty($templateContextLines)) {
638 ?>
639 <li>
640 <p class="exceptionFieldTitle">Template Context<span class="exceptionColon">:</span></p>
641 <pre class="exceptionFieldValue"><?php echo StringUtil::encodeHTML(implode("", $templateContextLines));?></pre>
642 </li>
643 <?php
644 }
645 ?>
646 <li>
647 <p class="exceptionFieldTitle">Stack Trace<span class="exceptionColon">:</span></p>
648 <ul class="exceptionStacktrace">
649 <?php
650 $trace = sanitizeStacktrace($e);
651 for ($i = 0, $max = count($trace); $i < $max; $i++) {
652 ?>
653 <li class="exceptionStacktraceFile"><?php echo '#'.$i.' '.StringUtil::encodeHTML($trace[$i]['file']).' ('.$trace[$i]['line'].')'.':'; ?></li>
654 <li class="exceptionStacktraceCall">
655 <?php
656 echo $trace[$i]['class'].$trace[$i]['type'].$trace[$i]['function'].'(';
657 echo implode(', ', array_map(function ($item) {
658 switch (gettype($item)) {
659 case 'integer':
660 case 'double':
661 return $item;
662 case 'NULL':
663 return 'null';
664 case 'string':
665 return "'".addcslashes(StringUtil::encodeHTML($item), "\\'")."'";
666 case 'boolean':
667 return $item ? 'true' : 'false';
668 case 'array':
669 $keys = array_keys($item);
670 if (count($keys) > 5) return "[ ".count($keys)." items ]";
671 return '[ '.implode(', ', array_map(function ($item) {
672 return $item.' => ';
673 }, $keys)).']';
674 case 'object':
675 return get_class($item);
676 case 'resource':
677 return 'resource('.get_resource_type($item).')';
678 case 'resource (closed)':
679 return 'resource (closed)';
680 }
681
682 throw new \LogicException('Unreachable');
683 }, $trace[$i]['args']));
684 echo ')</li>';
685 }
686 ?>
687 </ul>
688 </li>
689 </ul>
690 </div>
691 <?php
692 $first = false;
693 } while ($e = array_pop($exceptions));
694 ?>
695 <?php } ?>
696 </div>
697 </body>
698 </html>
699 <?php
700 }
701
702 /**
703 * Returns the stack trace of the given Throwable with sensitive
704 * information removed.
705 *
706 * @param bool $ignorePaths If set to `true`: Don't call `sanitizePath`.
707 * @return mixed[]
708 */
709 function sanitizeStacktrace(\Throwable $e, bool $ignorePaths = false) {
710 $trace = $e->getTrace();
711
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'] = [];
718
719 if (!empty($item['args'])) {
720 if ($item['class']) {
721 $function = new \ReflectionMethod($item['class'], $item['function']);
722 }
723 else {
724 $function = new \ReflectionFunction($item['function']);
725 }
726
727 $parameters = $function->getParameters();
728 $i = 0;
729 foreach ($parameters as $parameter) {
730 $isSensitive = false;
731 if (
732 \method_exists($parameter, 'getAttributes')
733 && !empty($parameter->getAttributes(\wcf\SensitiveArgument::class))
734 ) {
735 $isSensitive = true;
736 }
737 if (\preg_match(
738 '/(?:^(?:password|passphrase|secret)|(?:Password|Passphrase|Secret))/',
739 $parameter->getName()
740 )) {
741 $isSensitive = true;
742 }
743
744 if ($isSensitive && isset($item['args'][$i])) {
745 $item['args'][$i] = '[redacted]';
746 }
747 $i++;
748 }
749
750 // strip database credentials
751 if (
752 preg_match('~\\\\?wcf\\\\system\\\\database\\\\[a-zA-Z]*Database~', $item['class'])
753 || $item['class'] === 'PDO'
754 ) {
755 if ($item['function'] === '__construct') {
756 $item['args'] = array_map(function () {
757 return '[redacted]';
758 }, $item['args']);
759 }
760 }
761 }
762
763 if (!$ignorePaths) {
764 $item['args'] = array_map(function ($item) {
765 if (!is_string($item)) return $item;
766
767 if (preg_match('~^('.preg_quote($_SERVER['DOCUMENT_ROOT'], '~').'|'.preg_quote(WCF_DIR, '~').')~', $item)) {
768 $item = sanitizePath($item);
769 }
770
771 return $item;
772 }, $item['args']);
773
774 $item['file'] = sanitizePath($item['file']);
775 }
776
777 return $item;
778 }, $trace);
779 }
780
781 /**
782 * Returns the given path relative to `WCF_DIR`, unless both,
783 * `EXCEPTION_PRIVACY` is `public` and the debug mode is enabled.
784 *
785 * @param string $path
786 * @return string
787 */
788 function sanitizePath(string $path): string {
789 if (WCF::debugModeIsEnabled() && defined('EXCEPTION_PRIVACY') && EXCEPTION_PRIVACY === 'public') {
790 return $path;
791 }
792
793 return '*/'.FileUtil::removeTrailingSlash(FileUtil::getRelativePath(WCF_DIR, $path));
794 }
795 }