--- /dev/null
+<dl>
+ <dt></dt>
+ <dd>
+ <button type="button" class="button jsCopyButton">{lang}wcf.acp.exceptionLog.exception.copy{/lang}</button>
+ <textarea rows="5" cols="40" class="jsCopyException" hidden>{$exception[0]}</textarea>
+ </dd>
+</dl>
+<dl>
+ <dt>{lang}wcf.acp.exceptionLog.exception.date{/lang}</dt>
+ <dd>{$exception[date]|plainTime}</dd>
+</dl>
+<dl>
+ <dt>{lang}wcf.acp.exceptionLog.exception.requestURI{/lang}</dt>
+ <dd>{$exception[requestURI]}</dd>
+</dl>
+<dl>
+ <dt>{lang}wcf.acp.exceptionLog.exception.referrer{/lang}</dt>
+ <dd>{$exception[referrer]}</dd>
+</dl>
+<dl>
+ <dt>{lang}wcf.acp.exceptionLog.exception.userAgent{/lang}</dt>
+ <dd>{$exception[userAgent]}</dd>
+</dl>
+<dl>
+ <dt>{lang}wcf.acp.exceptionLog.exception.memory{/lang}</dt>
+ <dd>{$exception[peakMemory]|filesizeBinary} / {if $exception[maxMemory] == -1}∞{else}{$exception[maxMemory]|filesizeBinary}{/if}</dd>
+</dl>
+{foreach from=$exception[chain] item=chain}
+ <dl>
+ <dt>{lang}wcf.acp.exceptionLog.exception.message{/lang}</dt>
+ <dd>{$chain[message]}</dd>
+ </dl>
+ <dl>
+ <dt>{lang}wcf.acp.exceptionLog.exception.class{/lang}</dt>
+ <dd>{$chain[class]}</dd>
+ </dl>
+ <dl>
+ <dt>{lang}wcf.acp.exceptionLog.exception.file{/lang}</dt>
+ <dd>{$chain[file]} ({$chain[line]})</dd>
+ </dl>
+ {if !$chain[information]|empty}
+ {foreach from=$chain[information] item=extraInformation}
+ <dl>
+ <dt>{$extraInformation[0]}</dt>
+ <dd style="white-space: pre-wrap;">{$extraInformation[1]}</dd>
+ </dl>
+ {/foreach}
+ {/if}
+ <dl>
+ <dt>{lang}wcf.acp.exceptionLog.exception.stacktrace{/lang}</dt>
+ <dd>
+ <ol start="0" class="nativeList">
+ {foreach from=$chain[stack] item=stack}
+ <li>{$stack[file]} ({$stack[line]}): {$stack[class]}{$stack[type]}{$stack[function]}(…)</li>
+ {/foreach}
+ </ol>
+ </dd>
+ </dl>
+{/foreach}
--- /dev/null
+/**
+ * Gets the html code for the rendering of a exception log entry.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ */
+
+import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend";
+import { ApiResult, apiResultFromError, apiResultFromValue } from "../Result";
+
+type Response = {
+ template: string;
+};
+
+export async function renderException(exceptionId: string): Promise<ApiResult<Response>> {
+ const url = new URL(`${window.WSC_RPC_API_URL}core/exceptions/${exceptionId}/render`);
+
+ let response: Response;
+ try {
+ response = (await prepareRequest(url).get().fetchAsJson()) as Response;
+ } catch (e) {
+ return apiResultFromError(e);
+ }
+
+ return apiResultFromValue(response);
+}
--- /dev/null
+/**
+ * Gets the html code for the rendering of a exception log entry.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ */
+define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "../Result"], function (require, exports, Backend_1, Result_1) {
+ "use strict";
+ Object.defineProperty(exports, "__esModule", { value: true });
+ exports.renderException = renderException;
+ async function renderException(exceptionId) {
+ const url = new URL(`${window.WSC_RPC_API_URL}core/exceptions/${exceptionId}/render`);
+ let response;
+ try {
+ response = (await (0, Backend_1.prepareRequest)(url).get().fetchAsJson());
+ }
+ catch (e) {
+ return (0, Result_1.apiResultFromError)(e);
+ }
+ return (0, Result_1.apiResultFromValue)(response);
+ }
+});
$event->register(new \wcf\system\endpoint\controller\core\comments\responses\RenderResponse);
$event->register(new \wcf\system\endpoint\controller\core\comments\responses\RenderResponses);
$event->register(new \wcf\system\endpoint\controller\core\comments\responses\UpdateResponse);
+ $event->register(new \wcf\system\endpoint\controller\core\exceptions\RenderException);
$event->register(new \wcf\system\endpoint\controller\core\gridViews\GetRows);
$event->register(new \wcf\system\endpoint\controller\core\messages\GetMentionSuggestions);
$event->register(new \wcf\system\endpoint\controller\core\sessions\DeleteSession);
--- /dev/null
+<?php
+
+namespace wcf\system\endpoint\controller\core\exceptions;
+
+use Laminas\Diactoros\Response\JsonResponse;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use wcf\system\endpoint\GetRequest;
+use wcf\system\endpoint\IController;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\exception\PermissionDeniedException;
+use wcf\system\Regex;
+use wcf\system\WCF;
+use wcf\util\DirectoryUtil;
+use wcf\util\ExceptionLogUtil;
+
+/**
+ * API endpoint for the rendering of an exception log entry.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.1
+ */
+#[GetRequest('/core/exceptions/{id:[a-f0-9]{40}}/render')]
+final class RenderException implements IController
+{
+ #[\Override]
+ public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface
+ {
+ $this->assertExceptionLogIsAccessible();
+
+ $exception = $this->getException($variables['id']);
+ if (!$exception) {
+ throw new IllegalLinkException();
+ }
+
+ return new JsonResponse([
+ 'template' => WCF::getTPL()->fetch('shared_exceptionLogDetails', 'wcf', [
+ 'exception' => $exception,
+ 'exceptionID' => $variables['id'],
+ ])
+ ]);
+ }
+
+ private function assertExceptionLogIsAccessible(): void
+ {
+ if (!WCF::getSession()->getPermission('admin.management.canViewLog')) {
+ throw new PermissionDeniedException();
+ }
+ }
+
+ private function getException(string $exceptionID): ?array
+ {
+ $logFile = $this->getLogFile($exceptionID);
+ if (!$logFile) {
+ return null;
+ }
+
+ try {
+ $exceptions = ExceptionLogUtil::splitLog(\file_get_contents($logFile));
+
+ return ExceptionLogUtil::parseException($exceptions[$exceptionID]);
+ } catch (\Exception $e) {
+ return null;
+ }
+ }
+
+ private function getLogFile(string $exceptionID): ?string
+ {
+ $fileNameRegex = new Regex('(?:^|/)\d{4}-\d{2}-\d{2}\.txt$');
+ $logFiles = DirectoryUtil::getInstance(WCF_DIR . 'log/', false)->getFiles(\SORT_DESC, $fileNameRegex);
+ foreach ($logFiles as $logFile) {
+ $pathname = WCF_DIR . 'log/' . $logFile;
+ $contents = \file_get_contents($pathname);
+
+ if (\str_contains($contents, '<<<<<<<<' . $exceptionID . '<<<<')) {
+ return $pathname;
+ }
+ }
+
+ return null;
+ }
+}