Add api endpoint for exception details
authorMarcel Werk <burntime@woltlab.com>
Mon, 4 Nov 2024 14:54:38 +0000 (15:54 +0100)
committerMarcel Werk <burntime@woltlab.com>
Tue, 12 Nov 2024 11:53:43 +0000 (12:53 +0100)
com.woltlab.wcf/templates/shared_exceptionLogDetails.tpl [new file with mode: 0644]
ts/WoltLabSuite/Core/Api/Exceptions/RenderException.ts [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Api/Exceptions/RenderException.js [new file with mode: 0644]
wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php
wcfsetup/install/files/lib/system/endpoint/controller/core/exceptions/RenderException.class.php [new file with mode: 0644]

diff --git a/com.woltlab.wcf/templates/shared_exceptionLogDetails.tpl b/com.woltlab.wcf/templates/shared_exceptionLogDetails.tpl
new file mode 100644 (file)
index 0000000..0f8df80
--- /dev/null
@@ -0,0 +1,59 @@
+<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}&infin;{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]}(&hellip;)</li>
+                               {/foreach}
+                       </ol>
+               </dd>
+       </dl>
+{/foreach}
diff --git a/ts/WoltLabSuite/Core/Api/Exceptions/RenderException.ts b/ts/WoltLabSuite/Core/Api/Exceptions/RenderException.ts
new file mode 100644 (file)
index 0000000..fc309d0
--- /dev/null
@@ -0,0 +1,28 @@
+/**
+ * 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);
+}
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Exceptions/RenderException.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Exceptions/RenderException.js
new file mode 100644 (file)
index 0000000..360833b
--- /dev/null
@@ -0,0 +1,24 @@
+/**
+ * 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);
+    }
+});
index 98da328d9633fbe68cba337986a29777c7fcf2e3..9cefa279607f4836fb66f27dced29ae30a8a262a 100644 (file)
@@ -134,6 +134,7 @@ return static function (): void {
             $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);
diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/exceptions/RenderException.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/exceptions/RenderException.class.php
new file mode 100644 (file)
index 0000000..39afb20
--- /dev/null
@@ -0,0 +1,84 @@
+<?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;
+    }
+}