Migrate exception log to grid view
authorMarcel Werk <burntime@woltlab.com>
Mon, 4 Nov 2024 14:56:15 +0000 (15:56 +0100)
committerMarcel Werk <burntime@woltlab.com>
Mon, 4 Nov 2024 14:56:15 +0000 (15:56 +0100)
16 files changed:
com.woltlab.wcf/templates/shared_gridView.tpl
com.woltlab.wcf/templates/shared_gridViewRows.tpl
ts/WoltLabSuite/Core/Acp/Controller/ExceptionLog/View.ts [new file with mode: 0644]
ts/WoltLabSuite/Core/Api/GridViews/GetRows.ts
ts/WoltLabSuite/Core/Component/GridView.ts
wcfsetup/install/files/acp/templates/exceptionLogView.tpl
wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Controller/ExceptionLog/View.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Api/GridViews/GetRows.js
wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView.js
wcfsetup/install/files/lib/acp/page/ExceptionLogViewPage.class.php
wcfsetup/install/files/lib/page/AbstractGridViewPage.class.php
wcfsetup/install/files/lib/system/endpoint/controller/core/gridViews/GetRows.class.php
wcfsetup/install/files/lib/system/view/grid/ArrayGridView.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/view/grid/DatabaseObjectListGridView.class.php
wcfsetup/install/files/lib/system/view/grid/ExceptionLogGridView.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/view/grid/GridViewColumn.class.php

index b8ea8b45aeb66fe3d554d95d43d3df595872d744..7838d6b57094f74960a5a63e6b23138b6de6ffae 100644 (file)
                                {$view->getPageNo()},
                                '{unsafe:$view->getBaseUrl()|encodeJS}',
                                '{unsafe:$view->getSortField()|encodeJS}',
-                               '{unsafe:$view->getSortOrder()|encodeJS}'
+                               '{unsafe:$view->getSortOrder()|encodeJS}',
+                               new Map([
+                                       {foreach from=$view->getParameters() key='name' item='value'}
+                                               ['{unsafe:$name|encodeJs}', '{unsafe:$value|encodeJs}'],
+                                       {/foreach}
+                               ])
                        );
                });
        </script>
index f494ad22515033996c0f7fee024d446f1a4c370b..3974a709127c0ed4f0defcd14458b8137753e0a9 100644 (file)
@@ -1,6 +1,6 @@
 
 {foreach from=$view->getRows() item='row'}
-       <tr>
+       <tr data-object-id="{$view->getObjectID($row)}">
                {foreach from=$view->getColumns() item='column'}
                        <td class="{$column->getClasses()}">
                                {unsafe:$view->renderColumn($column, $row)}
diff --git a/ts/WoltLabSuite/Core/Acp/Controller/ExceptionLog/View.ts b/ts/WoltLabSuite/Core/Acp/Controller/ExceptionLog/View.ts
new file mode 100644 (file)
index 0000000..0edc37c
--- /dev/null
@@ -0,0 +1,38 @@
+/**
+ * Shows the dialog that shows exception details.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.1
+ */
+
+import { renderException } from "WoltLabSuite/Core/Api/Exceptions/RenderException";
+import { copyTextToClipboard } from "WoltLabSuite/Core/Clipboard";
+import { dialogFactory } from "WoltLabSuite/Core/Component/Dialog";
+import { promiseMutex } from "WoltLabSuite/Core/Helper/PromiseMutex";
+import { wheneverFirstSeen } from "WoltLabSuite/Core/Helper/Selector";
+import { getPhrase } from "WoltLabSuite/Core/Language";
+
+async function showDialog(button: HTMLElement): Promise<void> {
+  const response = await renderException(button.closest("tr")!.dataset.objectId!);
+  if (!response.ok) {
+    return;
+  }
+
+  const dialog = dialogFactory().fromHtml(response.value.template).withoutControls();
+  dialog.content.querySelector(".jsCopyButton")?.addEventListener("click", () => {
+    void copyTextToClipboard(dialog.content.querySelector<HTMLTextAreaElement>(".jsCopyException")!.value);
+  });
+
+  dialog.show(getPhrase("wcf.acp.exceptionLog.exception.message"));
+}
+
+export function setup(): void {
+  wheneverFirstSeen(".jsExceptionLogEntry", (button) => {
+    button.addEventListener(
+      "click",
+      promiseMutex(() => showDialog(button)),
+    );
+  });
+}
index 6dc07920dccde5e913012e03fdb66312147ef85b..e271f09e25a6bd243b4b61303d98d134d8bdf123 100644 (file)
@@ -13,6 +13,7 @@ export async function getRows(
   sortField: string = "",
   sortOrder: string = "ASC",
   filters?: Map<string, string>,
+  gridViewParameters?: Map<string, string>,
 ): Promise<ApiResult<Response>> {
   const url = new URL(`${window.WSC_RPC_API_URL}core/gridViews/rows`);
   url.searchParams.set("gridView", gridViewClass);
@@ -24,6 +25,11 @@ export async function getRows(
       url.searchParams.set(`filters[${key}]`, value);
     });
   }
+  if (gridViewParameters) {
+    gridViewParameters.forEach((value, key) => {
+      url.searchParams.set(`gridViewParameters[${key}]`, value);
+    });
+  }
 
   let response: Response;
   try {
index 6730ea4cb588695075f3c1a09c9539997268a272..644331c155b60accb122074a019862ed622c66e6 100644 (file)
@@ -18,6 +18,7 @@ export class GridView {
   #defaultSortField: string;
   #defaultSortOrder: string;
   #filters: Map<string, string>;
+  #gridViewParameters?: Map<string, string>;
 
   constructor(
     gridId: string,
@@ -26,6 +27,7 @@ export class GridView {
     baseUrl: string = "",
     sortField = "",
     sortOrder = "ASC",
+    gridViewParameters?: Map<string, string>,
   ) {
     this.#gridClassName = gridClassName;
     this.#table = document.getElementById(`${gridId}_table`) as HTMLTableElement;
@@ -39,6 +41,7 @@ export class GridView {
     this.#defaultSortField = sortField;
     this.#sortOrder = sortOrder;
     this.#defaultSortOrder = sortOrder;
+    this.#gridViewParameters = gridViewParameters;
 
     this.#initPagination();
     this.#initSorting();
@@ -107,7 +110,14 @@ export class GridView {
 
   async #loadRows(updateQueryString: boolean = true): Promise<void> {
     const response = (
-      await getRows(this.#gridClassName, this.#pageNo, this.#sortField, this.#sortOrder, this.#filters)
+      await getRows(
+        this.#gridClassName,
+        this.#pageNo,
+        this.#sortField,
+        this.#sortOrder,
+        this.#filters,
+        this.#gridViewParameters,
+      )
     ).unwrap();
     DomUtil.setInnerHtml(this.#table.querySelector("tbody")!, response.template);
 
@@ -137,9 +147,11 @@ export class GridView {
       parameters.push(["sortField", this.#sortField]);
       parameters.push(["sortOrder", this.#sortOrder]);
     }
-    this.#filters.forEach((value, key) => {
-      parameters.push([`filters[${key}]`, value]);
-    });
+    if (this.#filters) {
+      this.#filters.forEach((value, key) => {
+        parameters.push([`filters[${key}]`, value]);
+      });
+    }
 
     if (parameters.length > 0) {
       url.search += url.search !== "" ? "&" : "?";
@@ -212,6 +224,9 @@ export class GridView {
   }
 
   #renderFilters(labels: ArrayLike<string>): void {
+    if (!this.#filterPills) {
+      return;
+    }
     this.#filterPills.innerHTML = "";
     if (!this.#filters) {
       return;
index f33ad72f1a8262288fb920043ec2a76397c5e398..88c1702b725b5cb7e25954d086e5a3c6b40919ff 100644 (file)
@@ -1,22 +1,4 @@
 {include file='header' pageTitle='wcf.acp.exceptionLog'}
-<script data-relocate="true">
-       $(function() {
-               {if $exceptionID}window.location.hash = '{$exceptionID|encodeJS}';{/if}
-               
-               $('#exceptionID').on('keyup keydown keypress', function () {
-                       if ($.trim($(this).val()) == '') {
-                               $('#logFile').enable().parents('dl').removeClass('disabled');
-                       }
-                       else {
-                               $('#logFile').disable().parents('dl').addClass('disabled');
-                       }
-               }).trigger('keypress');
-               
-               $('.jsCopyException').click(function () {
-                       $(this).select();
-               });
-       });
-</script>
 
 <header class="contentHeader">
        <div class="contentHeaderTitle">
        </form>
 {/if}
 
-{hascontent}
-       <div class="paginationTop">
-               {content}{pages print=true assign=pagesLinks controller="ExceptionLogView" link="pageNo=%d&logFile=$logFile"}{/content}
-       </div>
-{/hascontent}
+{unsafe:$gridView->render()}
 
-{if !$logFiles|empty}
-       {if $logFile}
-               {foreach from=$exceptions item='exception' key='exceptionKey'}
-                       <details id="{$exceptionKey}" class="section exceptionContainer"{if $exception[collapsed]|empty} open{/if}>
-                               <summary class="sectionTitle">
-                                       {$exception[message]}
-                               </summary>
-                               
-                               <div class="exceptionDetails">
-                                       <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}
-                                       <dl>
-                                               <dt><label for="copyException{$exceptionKey}">{lang}wcf.acp.exceptionLog.exception.copy{/lang}</label></dt>
-                                               <dd><textarea id="copyException{$exceptionKey}" rows="5" cols="40" class="jsCopyException" readonly>{$exception[0]}</textarea></dd>
-                                       </dl>
-                               </div>
-                       </details>
-               {/foreach}
-
-               <footer class="contentFooter">
-                       {hascontent}
-                               <div class="paginationBottom">
-                                       {content}{@$pagesLinks}{/content}
-                               </div>
-                       {/hascontent}
-               </footer>
-       {elseif $exceptionID}
-               <woltlab-core-notice type="error">{lang}wcf.acp.exceptionLog.exceptionNotFound{/lang}</woltlab-core-notice>
-       {/if}
-{else}
-       <woltlab-core-notice type="info">{lang}wcf.global.noItems{/lang}</woltlab-core-notice>
-{/if}
+<script data-relocate="true">
+       require(['WoltLabSuite/Core/Acp/Controller/ExceptionLog/View'], ({ setup }) => {
+               {jsphrase name='wcf.acp.exceptionLog.exception.message'}
+               setup();
+       });
+</script>
 
 {include file='footer'}
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Controller/ExceptionLog/View.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Controller/ExceptionLog/View.js
new file mode 100644 (file)
index 0000000..f8632d8
--- /dev/null
@@ -0,0 +1,29 @@
+/**
+ * Shows the dialog that shows exception details.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.1
+ */
+define(["require", "exports", "WoltLabSuite/Core/Api/Exceptions/RenderException", "WoltLabSuite/Core/Clipboard", "WoltLabSuite/Core/Component/Dialog", "WoltLabSuite/Core/Helper/PromiseMutex", "WoltLabSuite/Core/Helper/Selector", "WoltLabSuite/Core/Language"], function (require, exports, RenderException_1, Clipboard_1, Dialog_1, PromiseMutex_1, Selector_1, Language_1) {
+    "use strict";
+    Object.defineProperty(exports, "__esModule", { value: true });
+    exports.setup = setup;
+    async function showDialog(button) {
+        const response = await (0, RenderException_1.renderException)(button.closest("tr").dataset.objectId);
+        if (!response.ok) {
+            return;
+        }
+        const dialog = (0, Dialog_1.dialogFactory)().fromHtml(response.value.template).withoutControls();
+        dialog.content.querySelector(".jsCopyButton")?.addEventListener("click", () => {
+            void (0, Clipboard_1.copyTextToClipboard)(dialog.content.querySelector(".jsCopyException").value);
+        });
+        dialog.show((0, Language_1.getPhrase)("wcf.acp.exceptionLog.exception.message"));
+    }
+    function setup() {
+        (0, Selector_1.wheneverFirstSeen)(".jsExceptionLogEntry", (button) => {
+            button.addEventListener("click", (0, PromiseMutex_1.promiseMutex)(() => showDialog(button)));
+        });
+    }
+});
index 7a31864e72ceaab37cbf8999a25eff80ae07447e..3299f1af960517b4599cbf3bf374455976045c58 100644 (file)
@@ -2,7 +2,7 @@ define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "../Result"], fu
     "use strict";
     Object.defineProperty(exports, "__esModule", { value: true });
     exports.getRows = getRows;
-    async function getRows(gridViewClass, pageNo, sortField = "", sortOrder = "ASC", filters) {
+    async function getRows(gridViewClass, pageNo, sortField = "", sortOrder = "ASC", filters, gridViewParameters) {
         const url = new URL(`${window.WSC_RPC_API_URL}core/gridViews/rows`);
         url.searchParams.set("gridView", gridViewClass);
         url.searchParams.set("pageNo", pageNo.toString());
@@ -13,6 +13,11 @@ define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "../Result"], fu
                 url.searchParams.set(`filters[${key}]`, value);
             });
         }
+        if (gridViewParameters) {
+            gridViewParameters.forEach((value, key) => {
+                url.searchParams.set(`gridViewParameters[${key}]`, value);
+            });
+        }
         let response;
         try {
             response = (await (0, Backend_1.prepareRequest)(url).get().allowCaching().disableLoadingIndicator().fetchAsJson());
index 416ef22d0c4143657710eb153d4ad7531e072394..b2159fbc406018d5771403d66f3176eac2a55b05 100644 (file)
@@ -18,7 +18,8 @@ define(["require", "exports", "tslib", "../Api/GridViews/GetRows", "../Dom/Util"
         #defaultSortField;
         #defaultSortOrder;
         #filters;
-        constructor(gridId, gridClassName, pageNo, baseUrl = "", sortField = "", sortOrder = "ASC") {
+        #gridViewParameters;
+        constructor(gridId, gridClassName, pageNo, baseUrl = "", sortField = "", sortOrder = "ASC", gridViewParameters) {
             this.#gridClassName = gridClassName;
             this.#table = document.getElementById(`${gridId}_table`);
             this.#topPagination = document.getElementById(`${gridId}_topPagination`);
@@ -31,6 +32,7 @@ define(["require", "exports", "tslib", "../Api/GridViews/GetRows", "../Dom/Util"
             this.#defaultSortField = sortField;
             this.#sortOrder = sortOrder;
             this.#defaultSortOrder = sortOrder;
+            this.#gridViewParameters = gridViewParameters;
             this.#initPagination();
             this.#initSorting();
             this.#initActions();
@@ -86,7 +88,7 @@ define(["require", "exports", "tslib", "../Api/GridViews/GetRows", "../Dom/Util"
             void this.#loadRows(updateQueryString);
         }
         async #loadRows(updateQueryString = true) {
-            const response = (await (0, GetRows_1.getRows)(this.#gridClassName, this.#pageNo, this.#sortField, this.#sortOrder, this.#filters)).unwrap();
+            const response = (await (0, GetRows_1.getRows)(this.#gridClassName, this.#pageNo, this.#sortField, this.#sortOrder, this.#filters, this.#gridViewParameters)).unwrap();
             Util_1.default.setInnerHtml(this.#table.querySelector("tbody"), response.template);
             this.#topPagination.count = response.pages;
             this.#bottomPagination.count = response.pages;
@@ -109,9 +111,11 @@ define(["require", "exports", "tslib", "../Api/GridViews/GetRows", "../Dom/Util"
                 parameters.push(["sortField", this.#sortField]);
                 parameters.push(["sortOrder", this.#sortOrder]);
             }
-            this.#filters.forEach((value, key) => {
-                parameters.push([`filters[${key}]`, value]);
-            });
+            if (this.#filters) {
+                this.#filters.forEach((value, key) => {
+                    parameters.push([`filters[${key}]`, value]);
+                });
+            }
             if (parameters.length > 0) {
                 url.search += url.search !== "" ? "&" : "?";
                 url.search += new URLSearchParams(parameters).toString();
@@ -167,6 +171,9 @@ define(["require", "exports", "tslib", "../Api/GridViews/GetRows", "../Dom/Util"
             }
         }
         #renderFilters(labels) {
+            if (!this.#filterPills) {
+                return;
+            }
             this.#filterPills.innerHTML = "";
             if (!this.#filters) {
                 return;
index 5495a24425c06f0156d47a8927adbc666ddb9f32..36005b821af517527dd5bf61689c7037a8f4d8a3 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace wcf\acp\page;
 
+use wcf\page\AbstractGridViewPage;
 use wcf\page\AbstractPage;
 use wcf\page\MultipleLinkPage;
 use wcf\system\event\EventHandler;
@@ -9,6 +10,8 @@ use wcf\system\exception\IllegalLinkException;
 use wcf\system\Regex;
 use wcf\system\registry\RegistryHandler;
 use wcf\system\request\LinkHandler;
+use wcf\system\view\grid\AbstractGridView;
+use wcf\system\view\grid\ExceptionLogGridView;
 use wcf\system\WCF;
 use wcf\util\DirectoryUtil;
 use wcf\util\ExceptionLogUtil;
@@ -21,7 +24,7 @@ use wcf\util\StringUtil;
  * @copyright   2001-2019 WoltLab GmbH
  * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  */
-class ExceptionLogViewPage extends MultipleLinkPage
+class ExceptionLogViewPage extends AbstractGridViewPage
 {
     /**
      * @inheritDoc
@@ -33,43 +36,21 @@ class ExceptionLogViewPage extends MultipleLinkPage
      */
     public $neededPermissions = ['admin.management.canViewLog'];
 
-    /**
-     * @inheritDoc
-     */
-    public $itemsPerPage = 10;
-
-    /**
-     * given exceptionID
-     * @var string
-     */
-    public $exceptionID = '';
-
     /**
      * @inheritDoc
      */
     public $forceCanonicalURL = true;
 
-    /**
-     * active logfile
-     * @var string
-     */
-    public $logFile = '';
+    public string $exceptionID = '';
+    public string $logFile = '';
 
     /**
      * available logfiles
      * @var string[]
      */
-    public $logFiles = [];
+    public array $logFiles = [];
 
-    /**
-     * exceptions shown
-     * @var array
-     */
-    public $exceptions = [];
-
-    /**
-     * @inheritDoc
-     */
+    #[\Override]
     public function readParameters()
     {
         parent::readParameters();
@@ -91,22 +72,91 @@ class ExceptionLogViewPage extends MultipleLinkPage
         $this->canonicalURL = LinkHandler::getInstance()->getControllerLink(self::class, $parameters);
     }
 
-    /**
-     * @inheritDoc
-     */
+    #[\Override]
     public function readData()
     {
-        AbstractPage::readData();
+        $this->markNotificationsAsRead();
+        $this->readLogFiles();
+        $this->validateParameters();
+
+        parent::readData();
+    }
 
-        // mark notifications as read
+    private function markNotificationsAsRead(): void
+    {
         RegistryHandler::getInstance()->set('com.woltlab.wcf', 'exceptionMailerTimestamp', TIME_NOW);
+    }
 
+    private function readLogFiles(): void
+    {
         $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;
             $this->logFiles[$pathname] = $pathname;
         }
+    }
+
+    private function validateParameters(): void
+    {
+        $fileNameRegex = new Regex('(?:^|/)\d{4}-\d{2}-\d{2}\.txt$');
+        if ($this->exceptionID) {
+            // search the appropriate file
+            foreach ($this->logFiles as $logFile) {
+                $contents = \file_get_contents($logFile);
+
+                if (\str_contains($contents, '<<<<<<<<' . $this->exceptionID . '<<<<')) {
+                    $fileNameRegex->match($logFile);
+                    $matches = $fileNameRegex->getMatches();
+                    $this->logFile = $matches[0];
+                    break;
+                }
+
+                unset($contents);
+            }
+
+            if (!isset($contents)) {
+                $this->logFile = '';
+
+                return;
+            }
+        } elseif ($this->logFile) {
+            if (!$fileNameRegex->match(\basename($this->logFile))) {
+                throw new IllegalLinkException();
+            }
+            if (!\file_exists(WCF_DIR . 'log/' . $this->logFile)) {
+                throw new IllegalLinkException();
+            }
+        }
+    }
+
+    #[\Override]
+    protected function createGridViewController(): AbstractGridView
+    {
+        return new ExceptionLogGridView($this->logFile, $this->exceptionID);
+    }
+
+    #[\Override]
+    protected function initGridView(): void
+    {
+        parent::initGridView();
+
+        $parameters = [];
+        if ($this->exceptionID !== '') {
+            $parameters['exceptionID'] = $this->exceptionID;
+        } elseif ($this->logFile !== '') {
+            $parameters['logFile'] = $this->logFile;
+        }
+
+        $this->gridView->setBaseUrl(LinkHandler::getInstance()->getControllerLink(static::class, $parameters));
+    }
+
+    /**
+     * @inheritDoc
+     */
+    /*public function readData()
+    {
+        AbstractPage::readData();
 
         if ($this->exceptionID) {
             // search the appropriate file
@@ -176,25 +226,25 @@ class ExceptionLogViewPage extends MultipleLinkPage
                 unset($this->exceptions[$key]);
             }
         }
-    }
+    }*/
 
     /**
      * @inheritDoc
      */
-    public function countItems()
+    /*public function countItems()
     {
         // call countItems event
         EventHandler::getInstance()->fireAction($this, 'countItems');
 
         return \count($this->exceptions);
-    }
+    }*/
 
     /**
      * Switches to the page containing the exception with the given ID.
      *
      * @param string $exceptionID
      */
-    public function searchPage($exceptionID)
+    /*public function searchPage($exceptionID)
     {
         $i = 1;
 
@@ -206,11 +256,9 @@ class ExceptionLogViewPage extends MultipleLinkPage
         }
 
         $this->pageNo = \ceil($i / $this->itemsPerPage);
-    }
+    }*/
 
-    /**
-     * @inheritDoc
-     */
+    #[\Override]
     public function assignVariables()
     {
         parent::assignVariables();
@@ -219,7 +267,6 @@ class ExceptionLogViewPage extends MultipleLinkPage
             'exceptionID' => $this->exceptionID,
             'logFiles' => \array_flip(\array_map('basename', $this->logFiles)),
             'logFile' => $this->logFile,
-            'exceptions' => $this->exceptions,
         ]);
     }
 }
index 0164e432cb04863a44372bf4050f2b2dbef4ebfb..fcc7029e474d60a50f82028d8cc99e1ec8db5c95 100644 (file)
@@ -64,7 +64,9 @@ abstract class AbstractGridViewPage extends AbstractPage
         if ($this->filters !== []) {
             $this->gridView->setActiveFilters($this->filters);
         }
-        $this->gridView->setPageNo($this->pageNo);
+        if ($this->pageNo != 1) {
+            $this->gridView->setPageNo($this->pageNo);
+        }
         $this->gridView->setBaseUrl(LinkHandler::getInstance()->getControllerLink(static::class));
     }
 
index 0aba1ecac1ed2be02afda6c8480f742027b56294..f11b75f330252c7aa82d69255dd9bff159a7b9a1 100644 (file)
@@ -24,7 +24,7 @@ final class GetRows implements IController
             throw new UserInputException('gridView', 'invalid');
         }
 
-        $view = new $parameters->gridView();
+        $view = new $parameters->gridView(...$parameters->gridViewParameters);
         \assert($view instanceof AbstractGridView);
 
         if (!$view->isAccessible()) {
@@ -66,6 +66,8 @@ final class GetRowsParameters
         public readonly string $sortField,
         public readonly string $sortOrder,
         /** @var string[] */
-        public readonly array $filters
+        public readonly array $filters,
+        /** @var string[] */
+        public readonly array $gridViewParameters,
     ) {}
 }
diff --git a/wcfsetup/install/files/lib/system/view/grid/ArrayGridView.class.php b/wcfsetup/install/files/lib/system/view/grid/ArrayGridView.class.php
new file mode 100644 (file)
index 0000000..58efe74
--- /dev/null
@@ -0,0 +1,45 @@
+<?php
+
+namespace wcf\system\view\grid;
+
+abstract class ArrayGridView extends AbstractGridView
+{
+    protected array $dataArray = [];
+
+    public function __construct()
+    {
+        parent::__construct();
+
+        $this->dataArray = $this->getDataArray();
+    }
+
+    public function getRows(): array
+    {
+        $this->sortRows();
+
+        return $this->getRowsForPage();
+    }
+
+    protected function sortRows(): void
+    {
+        \uasort($this->dataArray, function (array $a, array $b) {
+            if ($this->getSortOrder() === 'ASC') {
+                return \strcmp($a[$this->getSortField()], $b[$this->getSortField()]);
+            } else {
+                return \strcmp($b[$this->getSortField()], $a[$this->getSortField()]);
+            }
+        });
+    }
+
+    protected function getRowsForPage(): array
+    {
+        return \array_slice($this->dataArray, ($this->getPageNo() - 1) * $this->getRowsPerPage(), $this->getRowsPerPage());
+    }
+
+    public function countRows(): int
+    {
+        return \count($this->dataArray);
+    }
+
+    protected abstract function getDataArray(): array;
+}
index d1ae67b3d9d6b080a7ac9aee9a2b2a8593f97548..486477c18dffbfd33e02c8a5eab146fa2de9c438 100644 (file)
@@ -71,5 +71,13 @@ abstract class DatabaseObjectListGridView extends AbstractGridView
         }
     }
 
+    #[\Override]
+    public function getObjectID(mixed $row): mixed
+    {
+        \assert($row instanceof DatabaseObject);
+
+        return $row->getObjectID();
+    }
+
     protected abstract function createObjectList(): DatabaseObjectList;
 }
diff --git a/wcfsetup/install/files/lib/system/view/grid/ExceptionLogGridView.class.php b/wcfsetup/install/files/lib/system/view/grid/ExceptionLogGridView.class.php
new file mode 100644 (file)
index 0000000..f2860ac
--- /dev/null
@@ -0,0 +1,99 @@
+<?php
+
+namespace wcf\system\view\grid;
+
+use wcf\system\view\grid\renderer\TimeColumnRenderer;
+use wcf\system\view\grid\renderer\TitleColumnRenderer;
+use wcf\system\WCF;
+use wcf\util\ExceptionLogUtil;
+
+final class ExceptionLogGridView extends ArrayGridView
+{
+    public function __construct(
+        private readonly string $logFile,
+        private readonly string $exceptionID = ''
+    ) {
+        parent::__construct();
+
+        if ($this->exceptionID) {
+            $this->sortRows();
+            $this->jumpToException();
+        }
+    }
+
+    #[\Override]
+    protected function init(): void
+    {
+        $this->addColumns([
+            GridViewColumn::for('message')
+                ->label('wcf.acp.exceptionLog.exception.message')
+                ->sortable()
+                ->renderer(new TitleColumnRenderer()),
+            GridViewColumn::for('exceptionID')
+                ->label('wcf.global.objectID')
+                ->sortable(),
+            GridViewColumn::for('date')
+                ->label('wcf.acp.exceptionLog.exception.date')
+                ->sortable()
+                ->renderer(new TimeColumnRenderer()),
+        ]);
+
+        $this->addRowLink(new GridViewRowLink(cssClass: 'jsExceptionLogEntry'));
+        $this->setSortField('date');
+        $this->setSortOrder('DESC');
+    }
+
+    #[\Override]
+    public function isAccessible(): bool
+    {
+        return WCF::getSession()->getPermission('admin.management.canViewLog');
+    }
+
+    private function jumpToException(): void
+    {
+        $i = 1;
+        foreach ($this->dataArray as $key => $val) {
+            if ($key == $this->exceptionID) {
+                break;
+            }
+            $i++;
+        }
+
+        $this->setPageNo(\ceil($i / $this->getRowsPerPage()));
+    }
+
+    #[\Override]
+    public function getParameters(): array
+    {
+        return ['logFile' => $this->logFile];
+    }
+
+    #[\Override]
+    public function getObjectID(mixed $row): mixed
+    {
+        return $row['exceptionID'];
+    }
+
+    protected function getDataArray(): array
+    {
+        if (!$this->logFile) {
+            return [];
+        }
+
+        $contents = \file_get_contents(WCF_DIR . 'log/' . $this->logFile);
+        $exceptions = ExceptionLogUtil::splitLog($contents);
+        $parsedExceptions = [];
+
+        foreach ($exceptions as $key => $val) {
+            $parsed = ExceptionLogUtil::parseException($val);
+
+            $parsedExceptions[$key] = [
+                'exceptionID' => $key,
+                'message' => $parsed['message'],
+                'date' => $parsed['date'],
+            ];
+        }
+
+        return $parsedExceptions;
+    }
+}
index d8c699a82ac67dae16f42dc4e5fcf1509b7e4cb1..cafa07d190db57847df0405e24217aabbd09106c 100644 (file)
@@ -6,6 +6,7 @@ use wcf\system\form\builder\field\AbstractFormField;
 use wcf\system\view\grid\filter\IGridViewFilter;
 use wcf\system\view\grid\renderer\DefaultColumnRenderer;
 use wcf\system\view\grid\renderer\IColumnRenderer;
+use wcf\system\view\grid\renderer\TitleColumnRenderer;
 use wcf\system\WCF;
 
 final class GridViewColumn
@@ -146,4 +147,15 @@ final class GridViewColumn
 
         return self::$defaultRenderer;
     }
+
+    public function isTitleColumn(): bool
+    {
+        foreach ($this->getRenderers() as $renderer) {
+            if ($renderer instanceof TitleColumnRenderer) {
+                return true;
+            }
+        }
+
+        return false;
+    }
 }