Merge branch '3.1' into 5.2
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / core.functions.php
1 <?php
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 /**
21 * Escapes a string for use in sql query.
22 *
23 * @see \wcf\system\database\Database::escapeString()
24 * @param string $string
25 * @return string
26 */
27 function escapeString($string) {
28 return WCF::getDB()->escapeString($string);
29 }
30
31 /**
32 * Helper method to output debug data for all passed variables,
33 * uses `print_r()` for arrays and objects, `var_dump()` otherwise.
34 */
35 function wcfDebug() {
36 echo "<pre>";
37
38 $args = func_get_args();
39 $length = count($args);
40 if ($length === 0) {
41 echo "ERROR: No arguments provided.<hr>";
42 }
43 else {
44 for ($i = 0; $i < $length; $i++) {
45 $arg = $args[$i];
46
47 echo "<h2>Argument {$i} (" . gettype($arg) . ")</h2>";
48
49 if (is_array($arg) || is_object($arg)) {
50 print_r($arg);
51 }
52 else {
53 var_dump($arg);
54 }
55
56 echo "<hr>";
57 }
58 }
59
60 $backtrace = debug_backtrace();
61
62 // output call location to help finding these debug outputs again
63 echo "wcfDebug() called in {$backtrace[0]['file']} on line {$backtrace[0]['line']}";
64
65 echo "</pre>";
66
67 exit;
68 }
69
70 // define DOCUMENT_ROOT on IIS if not set
71 if (PHP_EOL == "\r\n") {
72 if (!isset($_SERVER['DOCUMENT_ROOT']) && isset($_SERVER['SCRIPT_FILENAME'])) {
73 $_SERVER['DOCUMENT_ROOT'] = str_replace('\\', '/', substr($_SERVER['SCRIPT_FILENAME'], 0, 0 - strlen($_SERVER['PHP_SELF'])));
74 }
75 if (!isset($_SERVER['DOCUMENT_ROOT']) && isset($_SERVER['PATH_TRANSLATED'])) {
76 $_SERVER['DOCUMENT_ROOT'] = str_replace('\\', '/', substr(str_replace('\\\\', '\\', $_SERVER['PATH_TRANSLATED']), 0, 0 - strlen($_SERVER['PHP_SELF'])));
77 }
78
79 if (!isset($_SERVER['REQUEST_URI'])) {
80 $_SERVER['REQUEST_URI'] = substr($_SERVER['PHP_SELF'], 1);
81 if (isset($_SERVER['QUERY_STRING'])) {
82 $_SERVER['REQUEST_URI'] .= '?' . $_SERVER['QUERY_STRING'];
83 }
84 }
85 }
86
87 // setting global gzip compression breaks output buffering
88 if (@ini_get('zlib.output_compression')) {
89 @ini_set('zlib.output_compression', '0');
90 }
91
92 if (!function_exists('is_countable')) {
93 function is_countable($var) {
94 return is_array($var) || $var instanceof Countable || $var instanceof ResourceBundle || $var instanceof SimpleXmlElement;
95 }
96 }
97 }
98
99 // @codingStandardsIgnoreStart
100 namespace wcf {
101 function getRequestId() {
102 if (!defined('WCF_REQUEST_ID_HEADER') || !WCF_REQUEST_ID_HEADER) return '';
103
104 return $_SERVER[WCF_REQUEST_ID_HEADER] ?? '';
105 }
106 }
107
108 namespace wcf\functions\exception {
109 use wcf\system\WCF;
110 use wcf\system\exception\IExtraInformationException;
111 use wcf\system\exception\SystemException;
112 use wcf\util\FileUtil;
113 use wcf\util\StringUtil;
114
115 /**
116 * Logs the given Throwable.
117 *
118 * @param \Throwable|\Exception $e
119 * @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.
120 * @return string The ID of the log entry.
121 */
122 function logThrowable($e, &$logFile = null) {
123 if ($logFile === null) $logFile = WCF_DIR . 'log/' . gmdate('Y-m-d', TIME_NOW) . '.txt';
124 touch($logFile);
125
126 $stripNewlines = function ($item) {
127 return str_replace("\n", ' ', $item);
128 };
129
130 // don't forget to update ExceptionLogViewPage, when changing the log file format
131 $message = gmdate('r', TIME_NOW)."\n".
132 'Message: '.$stripNewlines($e->getMessage())."\n".
133 'PHP version: '.phpversion()."\n".
134 'WoltLab Suite version: '.WCF_VERSION."\n".
135 'Request URI: '.$stripNewlines($_SERVER['REQUEST_URI'] ?? '').(\wcf\getRequestId() ? ' ('.\wcf\getRequestId().')' : '')."\n".
136 'Referrer: '.$stripNewlines($_SERVER['HTTP_REFERER'] ?? '')."\n".
137 'User Agent: '.$stripNewlines($_SERVER['HTTP_USER_AGENT'] ?? '')."\n".
138 'Peak Memory Usage: '.memory_get_peak_usage().'/'.FileUtil::getMemoryLimit()."\n";
139 $prev = $e;
140 do {
141 $message .= "======\n".
142 'Error Class: '.get_class($prev)."\n".
143 'Error Message: '.$stripNewlines($prev->getMessage())."\n".
144 'Error Code: '.intval($prev->getCode())."\n".
145 'File: '.$stripNewlines($prev->getFile()).' ('.$prev->getLine().')'."\n".
146 'Extra Information: '.($prev instanceof IExtraInformationException ? base64_encode(serialize($prev->getExtraInformation())) : '-')."\n".
147 'Stack Trace: '.json_encode(array_map(function ($item) {
148 $item['args'] = array_map(function ($item) {
149 switch (gettype($item)) {
150 case 'object':
151 return get_class($item);
152 case 'array':
153 return array_map(function () {
154 return '[redacted]';
155 }, $item);
156 case 'resource':
157 return 'resource('.get_resource_type($item).')';
158 default:
159 return $item;
160 }
161 }, $item['args']);
162
163 return $item;
164 }, sanitizeStacktrace($prev, true)))."\n";
165 }
166 while ($prev = $prev->getPrevious());
167
168 // calculate Exception-ID
169 $exceptionID = sha1($message);
170 $entry = "<<<<<<<<".$exceptionID."<<<<\n".$message."<<<<\n\n";
171
172 file_put_contents($logFile, $entry, FILE_APPEND);
173
174 // let the Exception know it has been logged
175 if (method_exists($e, 'finalizeLog') && is_callable([$e, 'finalizeLog'])) $e->finalizeLog($exceptionID, $logFile);
176
177 return $exceptionID;
178 }
179
180 /**
181 * Pretty prints the given Throwable. It is recommended to `exit;`
182 * the request after calling this function.
183 *
184 * @param \Throwable|\Exception $e
185 * @throws \Exception
186 */
187 function printThrowable($e) {
188 $exceptionID = logThrowable($e, $logFile);
189 if (\wcf\getRequestId()) $exceptionID .= '/'.\wcf\getRequestId();
190
191 $exceptionTitle = $exceptionSubtitle = $exceptionExplanation = '';
192 $logFile = sanitizePath($logFile);
193 try {
194 if (WCF::getLanguage() !== null) {
195 $exceptionTitle = WCF::getLanguage()->get('wcf.global.exception.title', true);
196 $exceptionSubtitle = str_replace('{$exceptionID}', $exceptionID, WCF::getLanguage()->get('wcf.global.exception.subtitle', true));
197 $exceptionExplanation = str_replace('{$logFile}', $logFile, WCF::getLanguage()->get('wcf.global.exception.explanation', true));
198 }
199 }
200 catch (\Throwable $e) {
201 // ignore
202 }
203
204 if (!$exceptionTitle || !$exceptionSubtitle || !$exceptionExplanation) {
205 // one or more failed, fallback to english
206 $exceptionTitle = 'An error has occurred';
207 $exceptionSubtitle = 'Internal error code: <span class="exceptionInlineCodeWrapper"><span class="exceptionInlineCode">'.$exceptionID.'</span></span>';
208 $exceptionExplanation = <<<EXPLANATION
209 <p class="exceptionSubtitle">What happened?</p>
210 <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>
211 <p class="exceptionText">&nbsp;</p> <!-- required to ensure spacing after copy & paste -->
212 <p class="exceptionText">
213 The error code can be used by an administrator to lookup the full error message in the Administration Control Panel via “Logs » Errors”.
214 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.
215 </p>
216 <p class="exceptionText">&nbsp;</p> <!-- required to ensure spacing after copy & paste -->
217 <p class="exceptionText">Notice: The error code was randomly generated and has no use beyond looking up the full message.</p>
218 EXPLANATION;
219
220 }
221
222 /*
223 * A notice on the HTML used below:
224 *
225 * It might appear a bit weird to use <p> all over the place where semantically
226 * other elements would fit in way better. The reason behind this is that we avoid
227 * inheriting unwanted styles (e.g. exception displayed in an overlay) and that
228 * the output needs to be properly readable when copied & pasted somewhere.
229 *
230 * Besides the visual appearance, the output was built to provide a maximum of
231 * compatibility and readability when pasted somewhere else, e.g. a WYSIWYG editor
232 * without the potential of messing up the formatting and thus harming the readability.
233 */
234 ?><!DOCTYPE html>
235 <html>
236 <head>
237 <?php if (!defined('EXCEPTION_PRIVACY') || EXCEPTION_PRIVACY !== 'private') { ?>
238 <title>Fatal Error: <?php echo StringUtil::encodeHTML($e->getMessage()); ?></title>
239 <?php } else { ?>
240 <title>Fatal Error</title>
241 <?php } ?>
242 <meta charset="utf-8">
243 <meta name="viewport" content="width=device-width, initial-scale=1">
244 <style>
245 .exceptionBody {
246 background-color: rgb(250, 250, 250);
247 color: rgb(44, 62, 80);
248 margin: 0;
249 padding: 0;
250 }
251
252 .exceptionContainer {
253 box-sizing: border-box;
254 font-family: 'Segoe UI', 'Lucida Grande', 'Helvetica Neue', Helvetica, Arial, sans-serif;
255 font-size: 14px;
256 padding-bottom: 20px;
257 }
258
259 .exceptionContainer * {
260 box-sizing: inherit;
261 line-height: 1.5em;
262 margin: 0;
263 padding: 0;
264 }
265
266 .exceptionHeader {
267 background-color: rgb(58, 109, 156);
268 padding: 30px 0;
269 }
270
271 .exceptionTitle {
272 color: #fff;
273 font-size: 28px;
274 font-weight: 300;
275 }
276
277 .exceptionErrorCode {
278 color: #fff;
279 margin-top: .5em;
280 }
281
282 .exceptionErrorCode .exceptionInlineCode {
283 background-color: rgb(43, 79, 113);
284 border-radius: 3px;
285 color: #fff;
286 font-family: monospace;
287 padding: 3px 10px;
288 white-space: nowrap;
289 }
290
291 .exceptionSubtitle {
292 border-bottom: 1px solid rgb(238, 238, 238);
293 font-size: 24px;
294 font-weight: 300;
295 margin-bottom: 15px;
296 padding-bottom: 10px;
297 }
298
299 .exceptionContainer > .exceptionBoundary {
300 margin-top: 30px;
301 }
302
303 .exceptionText .exceptionInlineCodeWrapper {
304 border: 1px solid rgb(169, 169, 169);
305 border-radius: 3px;
306 padding: 2px 5px;
307 }
308
309 .exceptionText .exceptionInlineCode {
310 font-family: monospace;
311 white-space: nowrap;
312 }
313
314 .exceptionFieldTitle {
315 color: rgb(59, 109, 169);
316 }
317
318 .exceptionFieldTitle .exceptionColon {
319 /* hide colon in browser, but will be visible after copy & paste */
320 opacity: 0;
321 }
322
323 .exceptionFieldValue {
324 font-size: 18px;
325 min-height: 1.5em;
326 }
327
328 .exceptionSystemInformation,
329 .exceptionErrorDetails,
330 .exceptionStacktrace {
331 list-style-type: none;
332 }
333
334 .exceptionSystemInformation > li:not(:first-child),
335 .exceptionErrorDetails > li:not(:first-child) {
336 margin-top: 10px;
337 }
338
339 .exceptionStacktrace {
340 display: block;
341 margin-top: 5px;
342 overflow: auto;
343 padding-bottom: 20px;
344 }
345
346 .exceptionStacktraceFile,
347 .exceptionStacktraceFile span,
348 .exceptionStacktraceCall,
349 .exceptionStacktraceCall span {
350 font-family: monospace !important;
351 white-space: nowrap !important;
352 }
353
354 .exceptionStacktraceCall + .exceptionStacktraceFile {
355 margin-top: 5px;
356 }
357
358 .exceptionStacktraceCall {
359 padding-left: 40px;
360 }
361
362 .exceptionStacktraceCall,
363 .exceptionStacktraceCall span {
364 color: rgb(102, 102, 102) !important;
365 font-size: 13px !important;
366 }
367
368 /* mobile */
369 @media (max-width: 767px) {
370 .exceptionBoundary {
371 min-width: 320px;
372 padding: 0 10px;
373 }
374
375 .exceptionText .exceptionInlineCodeWrapper {
376 display: inline-block;
377 overflow: auto;
378 }
379
380 .exceptionErrorCode .exceptionInlineCode {
381 font-size: 13px;
382 padding: 2px 5px;
383 }
384 }
385
386 /* desktop */
387 @media (min-width: 768px) {
388 .exceptionBoundary {
389 margin: 0 auto;
390 max-width: 1400px;
391 min-width: 1200px;
392 padding: 0 10px;
393 }
394
395 .exceptionSystemInformation {
396 display: flex;
397 flex-wrap: wrap;
398 }
399
400 .exceptionSystemInformation1,
401 .exceptionSystemInformation3,
402 .exceptionSystemInformation5 {
403 flex: 0 0 200px;
404 margin: 0 0 10px 0 !important;
405 }
406
407 .exceptionSystemInformation2,
408 .exceptionSystemInformation4,
409 .exceptionSystemInformation6 {
410 flex: 0 0 calc(100% - 210px);
411 margin: 0 0 10px 10px !important;
412 max-width: calc(100% - 210px);
413 }
414
415 .exceptionSystemInformation1 { order: 1; }
416 .exceptionSystemInformation2 { order: 2; }
417 .exceptionSystemInformation3 { order: 3; }
418 .exceptionSystemInformation4 { order: 4; }
419 .exceptionSystemInformation5 { order: 5; }
420 .exceptionSystemInformation6 { order: 6; }
421
422 .exceptionSystemInformation .exceptionFieldValue {
423 overflow: hidden;
424 text-overflow: ellipsis;
425 white-space: nowrap;
426 }
427 }
428 </style>
429 </head>
430 <body class="exceptionBody">
431 <div class="exceptionContainer">
432 <div class="exceptionHeader">
433 <div class="exceptionBoundary">
434 <p class="exceptionTitle"><?php echo $exceptionTitle; ?></p>
435 <p class="exceptionErrorCode"><?php echo str_replace('{$exceptionID}', $exceptionID, $exceptionSubtitle); ?></p>
436 </div>
437 </div>
438
439 <div class="exceptionBoundary">
440 <?php echo $exceptionExplanation; ?>
441 </div>
442 <?php if (!defined('EXCEPTION_PRIVACY') || EXCEPTION_PRIVACY !== 'private') { ?>
443 <div class="exceptionBoundary">
444 <p class="exceptionSubtitle">System Information</p>
445 <ul class="exceptionSystemInformation">
446 <li class="exceptionSystemInformation1">
447 <p class="exceptionFieldTitle">PHP Version<span class="exceptionColon">:</span></p>
448 <p class="exceptionFieldValue"><?php echo StringUtil::encodeHTML(phpversion()); ?></p>
449 </li>
450 <li class="exceptionSystemInformation3">
451 <p class="exceptionFieldTitle">WoltLab Suite Core<span class="exceptionColon">:</span></p>
452 <p class="exceptionFieldValue"><?php echo StringUtil::encodeHTML(WCF_VERSION); ?></p>
453 </li>
454 <li class="exceptionSystemInformation5">
455 <p class="exceptionFieldTitle">Peak Memory Usage<span class="exceptionColon">:</span></p>
456 <p class="exceptionFieldValue"><?php echo round(memory_get_peak_usage() / 1024 / 1024, 3); ?>/<?php echo round(FileUtil::getMemoryLimit() / 1024 / 1024, 3); ?> MiB</p>
457 </li>
458 <li class="exceptionSystemInformation2">
459 <p class="exceptionFieldTitle">Request URI<span class="exceptionColon">:</span></p>
460 <p class="exceptionFieldValue"><?php if (isset($_SERVER['REQUEST_URI'])) echo StringUtil::encodeHTML($_SERVER['REQUEST_URI']); ?></p>
461 </li>
462 <li class="exceptionSystemInformation4">
463 <p class="exceptionFieldTitle">Referrer<span class="exceptionColon">:</span></p>
464 <p class="exceptionFieldValue"><?php if (isset($_SERVER['HTTP_REFERER'])) echo StringUtil::encodeHTML($_SERVER['HTTP_REFERER']); ?></p>
465 </li>
466 <li class="exceptionSystemInformation6">
467 <p class="exceptionFieldTitle">User Agent<span class="exceptionColon">:</span></p>
468 <p class="exceptionFieldValue"><?php if (isset($_SERVER['HTTP_USER_AGENT'])) echo StringUtil::encodeHTML($_SERVER['HTTP_USER_AGENT']); ?></p>
469 </li>
470 </ul>
471 </div>
472
473 <?php
474 $first = true;
475 $exceptions = [];
476 $current = $e;
477 do {
478 $exceptions[] = $current;
479 }
480 while ($current = $current->getPrevious());
481
482 $e = array_pop($exceptions);
483 do {
484 ?>
485 <div class="exceptionBoundary">
486 <p class="exceptionSubtitle"><?php if (!empty($exceptions) && $first) { echo "Original "; } else if (empty($exceptions) && !$first) { echo "Final "; } ?>Error</p>
487 <?php if ($e instanceof SystemException && $e->getDescription()) { ?>
488 <p class="exceptionText"><?php echo $e->getDescription(); ?></p>
489 <?php } ?>
490 <ul class="exceptionErrorDetails">
491 <li>
492 <p class="exceptionFieldTitle">Error Type<span class="exceptionColon">:</span></p>
493 <p class="exceptionFieldValue"><?php echo StringUtil::encodeHTML(get_class($e)); ?></p>
494 </li>
495 <li>
496 <p class="exceptionFieldTitle">Error Message<span class="exceptionColon">:</span></p>
497 <p class="exceptionFieldValue"><?php echo StringUtil::encodeHTML($e->getMessage()); ?></p>
498 </li>
499 <?php if ($e->getCode()) { ?>
500 <li>
501 <p class="exceptionFieldTitle">Error Code<span class="exceptionColon">:</span></p>
502 <p class="exceptionFieldValue"><?php echo intval($e->getCode()); ?></p>
503 </li>
504 <?php } ?>
505 <li>
506 <p class="exceptionFieldTitle">File<span class="exceptionColon">:</span></p>
507 <p class="exceptionFieldValue" style="word-break: break-all"><?php echo StringUtil::encodeHTML(sanitizePath($e->getFile())); ?> (<?php echo $e->getLine(); ?>)</p>
508 </li>
509
510 <?php
511 if ($e instanceof SystemException) {
512 ob_start();
513 $e->show();
514 ob_end_clean();
515
516 $reflection = new \ReflectionClass($e);
517 $property = $reflection->getProperty('information');
518 $property->setAccessible(true);
519 if ($property->getValue($e)) {
520 throw new \Exception("Using the 'information' property of SystemException is not supported any more.");
521 }
522 }
523 if ($e instanceof IExtraInformationException) {
524 foreach ($e->getExtraInformation() as list($key, $value)) {
525 ?>
526 <li>
527 <p class="exceptionFieldTitle"><?php echo StringUtil::encodeHTML($key); ?><span class="exceptionColon">:</span></p>
528 <p class="exceptionFieldValue"><?php echo StringUtil::encodeHTML($value); ?></p>
529 </li>
530 <?php
531 }
532 }
533 ?>
534 <li>
535 <p class="exceptionFieldTitle">Stack Trace<span class="exceptionColon">:</span></p>
536 <ul class="exceptionStacktrace">
537 <?php
538 $trace = sanitizeStacktrace($e);
539 for ($i = 0, $max = count($trace); $i < $max; $i++) {
540 ?>
541 <li class="exceptionStacktraceFile"><?php echo '#'.$i.' '.StringUtil::encodeHTML($trace[$i]['file']).' ('.$trace[$i]['line'].')'.':'; ?></li>
542 <li class="exceptionStacktraceCall">
543 <?php
544 echo $trace[$i]['class'].$trace[$i]['type'].$trace[$i]['function'].'(';
545 echo implode(', ', array_map(function ($item) {
546 switch (gettype($item)) {
547 case 'integer':
548 case 'double':
549 return $item;
550 case 'NULL':
551 return 'null';
552 case 'string':
553 return "'".addcslashes(StringUtil::encodeHTML($item), "\\'")."'";
554 case 'boolean':
555 return $item ? 'true' : 'false';
556 case 'array':
557 $keys = array_keys($item);
558 if (count($keys) > 5) return "[ ".count($keys)." items ]";
559 return '[ '.implode(', ', array_map(function ($item) {
560 return $item.' => ';
561 }, $keys)).']';
562 case 'object':
563 return get_class($item);
564 case 'resource':
565 return 'resource('.get_resource_type($item).')';
566 case 'resource (closed)':
567 return 'resource (closed)';
568 }
569
570 throw new \LogicException('Unreachable');
571 }, $trace[$i]['args']));
572 echo ')</li>';
573 }
574 ?>
575 </ul>
576 </li>
577 </ul>
578 </div>
579 <?php
580 $first = false;
581 } while ($e = array_pop($exceptions));
582 ?>
583 <?php } ?>
584 </div>
585 </body>
586 </html>
587 <?php
588 }
589
590 /**
591 * Returns the stack trace of the given Throwable with sensitive
592 * information removed.
593 *
594 * @param \Throwable|\Exception $e
595 * @param boolean $ignorePaths If set to `true`: Don't call `sanitizePath`.
596 * @return mixed[]
597 */
598 function sanitizeStacktrace($e, $ignorePaths = false) {
599 $trace = $e->getTrace();
600
601 return array_map(function ($item) use ($ignorePaths) {
602 if (!isset($item['file'])) $item['file'] = '[internal function]';
603 if (!isset($item['line'])) $item['line'] = '?';
604 if (!isset($item['class'])) $item['class'] = '';
605 if (!isset($item['type'])) $item['type'] = '';
606 if (!isset($item['args'])) $item['args'] = [];
607
608 // strip database credentials
609 if (preg_match('~\\\\?wcf\\\\system\\\\database\\\\[a-zA-Z]*Database~', $item['class']) || $item['class'] === 'PDO') {
610 if ($item['function'] === '__construct') {
611 $item['args'] = array_map(function () {
612 return '[redacted]';
613 }, $item['args']);
614 }
615 }
616
617 if (!$ignorePaths) {
618 $item['args'] = array_map(function ($item) {
619 if (!is_string($item)) return $item;
620
621 if (preg_match('~^('.preg_quote($_SERVER['DOCUMENT_ROOT'], '~').'|'.preg_quote(WCF_DIR, '~').')~', $item)) {
622 $item = sanitizePath($item);
623 }
624
625 return $item;
626 }, $item['args']);
627
628 $item['file'] = sanitizePath($item['file']);
629 }
630
631 return $item;
632 }, $trace);
633 }
634
635 /**
636 * Returns the given path relative to `WCF_DIR`, unless both,
637 * `EXCEPTION_PRIVACY` is `public` and the debug mode is enabled.
638 *
639 * @param string $path
640 * @return string
641 */
642 function sanitizePath($path) {
643 if (WCF::debugModeIsEnabled() && defined('EXCEPTION_PRIVACY') && EXCEPTION_PRIVACY === 'public') {
644 return $path;
645 }
646
647 return '*/'.FileUtil::removeTrailingSlash(FileUtil::getRelativePath(WCF_DIR, $path));
648 }
649 }
650 // @codingStandardsIgnoreEnd