namespace wcf\acp\page;
use wcf\page\AbstractGridViewPage;
-use wcf\system\view\grid\AbstractGridView;
-use wcf\system\view\grid\CronjobLogGridView;
+use wcf\system\gridView\AbstractGridView;
+use wcf\system\gridView\CronjobLogGridView;
/**
* Shows cronjob log information.
namespace wcf\acp\page;
use wcf\page\AbstractGridViewPage;
+use wcf\system\gridView\AbstractGridView;
+use wcf\system\gridView\ExceptionLogGridView;
use wcf\system\registry\RegistryHandler;
-use wcf\system\view\grid\AbstractGridView;
-use wcf\system\view\grid\ExceptionLogGridView;
/**
* Shows the exception log.
namespace wcf\acp\page;
use wcf\page\AbstractGridViewPage;
-use wcf\system\view\grid\AbstractGridView;
-use wcf\system\view\grid\UserOptionGridView;
+use wcf\system\gridView\AbstractGridView;
+use wcf\system\gridView\UserOptionGridView;
/**
* Shows a list of the installed user options.
namespace wcf\acp\page;
use wcf\page\AbstractGridViewPage;
-use wcf\system\view\grid\AbstractGridView;
-use wcf\system\view\grid\UserRankGridView;
+use wcf\system\gridView\AbstractGridView;
+use wcf\system\gridView\UserRankGridView;
/**
* Lists available user ranks.
namespace wcf\event\gridView;
use wcf\event\IPsr14Event;
-use wcf\system\view\grid\CronjobLogGridView;
+use wcf\system\gridView\CronjobLogGridView;
/**
* Indicates that the cronjob log grid view has been initialized.
namespace wcf\event\gridView;
use wcf\event\IPsr14Event;
-use wcf\system\view\grid\ExceptionLogGridView;
+use wcf\system\gridView\ExceptionLogGridView;
/**
* Indicates that the exception log grid view has been initialized.
namespace wcf\event\gridView;
use wcf\event\IPsr14Event;
-use wcf\system\view\grid\UserOptionGridView;
+use wcf\system\gridView\UserOptionGridView;
/**
* Indicates that the user option grid view has been initialized.
namespace wcf\event\gridView;
use wcf\event\IPsr14Event;
-use wcf\system\view\grid\UserRankGridView;
+use wcf\system\gridView\UserRankGridView;
/**
* Indicates that the user rank grid view has been initialized.
namespace wcf\page;
use wcf\system\request\LinkHandler;
-use wcf\system\view\grid\AbstractGridView;
+use wcf\system\gridView\AbstractGridView;
use wcf\system\WCF;
abstract class AbstractGridViewPage extends AbstractPage
use wcf\system\endpoint\IController;
use wcf\system\exception\PermissionDeniedException;
use wcf\system\exception\UserInputException;
-use wcf\system\view\grid\AbstractGridView;
+use wcf\system\gridView\AbstractGridView;
#[GetRequest('/core/gridViews/rows')]
final class GetRows implements IController
--- /dev/null
+<?php
+
+namespace wcf\system\gridView;
+
+use LogicException;
+use wcf\action\GridViewFilterAction;
+use wcf\event\IPsr14Event;
+use wcf\system\event\EventHandler;
+use wcf\system\gridView\action\IGridViewAction;
+use wcf\system\request\LinkHandler;
+use wcf\system\WCF;
+
+abstract class AbstractGridView
+{
+ /**
+ * @var GridViewColumn[]
+ */
+ private array $columns = [];
+
+ /**
+ * @var IGridViewAction[]
+ */
+ private array $actions = [];
+
+ private GridViewRowLink $rowLink;
+ private int $rowsPerPage = 20;
+ private string $baseUrl = '';
+ private string $sortField = '';
+ private string $sortOrder = 'ASC';
+ private int $pageNo = 1;
+ private array $activeFilters = [];
+
+ /**
+ * Adds a new column to the grid view.
+ */
+ public function addColumn(GridViewColumn $column): void
+ {
+ $this->columns[] = $column;
+ }
+
+ /**
+ * Adds a new column to the grid view at the position before the specific id.
+ */
+ public function addColumnBefore(GridViewColumn $column, string $beforeID): void
+ {
+ $position = -1;
+
+ foreach ($this->getColumns() as $key => $existingColumn) {
+ if ($existingColumn->getID() === $beforeID) {
+ $position = $key;
+ break;
+ }
+ }
+
+ if ($position === -1) {
+ throw new \InvalidArgumentException("Invalid column id '{$beforeID}' given.");
+ }
+
+ array_splice($this->columns, $position, 0, [
+ $column,
+ ]);
+ }
+
+ /**
+ * Adds a new column to the grid view at the position after the specific id.
+ */
+ public function addColumnAfter(GridViewColumn $column, string $afterID): void
+ {
+ $position = -1;
+
+ foreach ($this->getColumns() as $key => $existingColumn) {
+ if ($existingColumn->getID() === $afterID) {
+ $position = $key;
+ break;
+ }
+ }
+
+ if ($position === -1) {
+ throw new \InvalidArgumentException("Invalid column id '{$afterID}' given.");
+ }
+
+ array_splice($this->columns, $position + 1, 0, [
+ $column,
+ ]);
+ }
+
+ /**
+ * Adds multiple new columns to the grid view.
+ * @param GridViewColumn[] $columns
+ */
+ public function addColumns(array $columns): void
+ {
+ foreach ($columns as $column) {
+ $this->addColumn($column);
+ }
+ }
+
+ /**
+ * Returns all columns of the grid view.
+ * @return GridViewColumn[]
+ */
+ public function getColumns(): array
+ {
+ return $this->columns;
+ }
+
+ /**
+ * Returns all visible (non-hidden) columns of the grid view.
+ * @return GridViewColumn[]
+ */
+ public function getVisibleColumns(): array
+ {
+ return \array_filter($this->getColumns(), fn($column) => !$column->isHidden());
+ }
+
+ /**
+ * Returns the column with the given id or null if no such column exists.
+ */
+ public function getColumn(string $id): ?GridViewColumn
+ {
+ foreach ($this->getColumns() as $column) {
+ if ($column->getID() === $id) {
+ return $column;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * @param IGridViewAction[] $columns
+ */
+ public function addActions(array $actions): void
+ {
+ foreach ($actions as $action) {
+ $this->addAction($action);
+ }
+ }
+
+ public function addAction(IGridViewAction $action): void
+ {
+ $this->actions[] = $action;
+ }
+
+ /**
+ * @return IGridViewAction[]
+ */
+ public function getActions(): array
+ {
+ return $this->actions;
+ }
+
+ public function hasActions(): bool
+ {
+ return $this->actions !== [];
+ }
+
+ public function hasDropdownActions(): bool
+ {
+ return $this->getDropdownActions() !== [];
+ }
+
+ /**
+ * @return IGridViewAction[]
+ */
+ public function getDropdownActions(): array
+ {
+ return \array_filter($this->getActions(), fn($action) => !$action->isQuickAction());
+ }
+
+ /**
+ * @return IGridViewAction[]
+ */
+ public function getQuickActions(): array
+ {
+ return \array_filter($this->getActions(), fn($action) => $action->isQuickAction());
+ }
+
+ public function render(): string
+ {
+ return WCF::getTPL()->fetch('shared_gridView', 'wcf', ['view' => $this], true);
+ }
+
+ public function renderRows(): string
+ {
+ return WCF::getTPL()->fetch('shared_gridViewRows', 'wcf', ['view' => $this], true);
+ }
+
+ public function renderColumn(GridViewColumn $column, mixed $row): string
+ {
+ $value = $column->render($this->getData($row, $column->getID()), $row);
+
+ if (isset($this->rowLink)) {
+ $value = $this->rowLink->render($value, $row, $column->isTitleColumn());
+ }
+
+ return $value;
+ }
+
+ public function renderAction(IGridViewAction $action, mixed $row): string
+ {
+ return $action->render($row);
+ }
+
+ public function renderActionInitialization(): string
+ {
+ return implode(
+ "\n",
+ \array_map(
+ fn($action) => $action->renderInitialization($this),
+ $this->getActions()
+ )
+ );
+ }
+
+ protected function getData(mixed $row, string $identifer): mixed
+ {
+ return $row[$identifer] ?? '';
+ }
+
+ public abstract function getRows(): array;
+
+ public abstract function countRows(): int;
+
+ public function countPages(): int
+ {
+ return \ceil($this->countRows() / $this->getRowsPerPage());
+ }
+
+ public function getClassName(): string
+ {
+ return \get_class($this);
+ }
+
+ public function isAccessible(): bool
+ {
+ return true;
+ }
+
+ public function getID(): string
+ {
+ $classNamePieces = \explode('\\', static::class);
+
+ return \implode('-', $classNamePieces);
+ }
+
+ public function setBaseUrl(string $url): void
+ {
+ $this->baseUrl = $url;
+ }
+
+ public function getBaseUrl(): string
+ {
+ return $this->baseUrl;
+ }
+
+ /**
+ * @return GridViewColumn[]
+ */
+ public function getSortableColumns(): array
+ {
+ return \array_filter($this->getColumns(), fn($column) => $column->isSortable());
+ }
+
+ /**
+ * @return GridViewColumn[]
+ */
+ public function getFilterableColumns(): array
+ {
+ return \array_filter($this->getColumns(), fn($column) => $column->getFilter() !== null);
+ }
+
+ public function setSortField(string $sortField): void
+ {
+ if (!\in_array($sortField, \array_map(fn($column) => $column->getID(), $this->getSortableColumns()))) {
+ throw new \InvalidArgumentException("Invalid value '{$sortField}' as sort field given.");
+ }
+
+ $this->sortField = $sortField;
+ }
+
+ public function setSortOrder(string $sortOrder): void
+ {
+ if ($sortOrder !== 'ASC' && $sortOrder !== 'DESC') {
+ throw new \InvalidArgumentException("Invalid value '{$sortOrder}' as sort order given.");
+ }
+
+ $this->sortOrder = $sortOrder;
+ }
+
+ public function getSortField(): string
+ {
+ return $this->sortField;
+ }
+
+ public function getSortOrder(): string
+ {
+ return $this->sortOrder;
+ }
+
+ public function getPageNo(): int
+ {
+ return $this->pageNo;
+ }
+
+ public function setPageNo(int $pageNo): void
+ {
+ $this->pageNo = $pageNo;
+ }
+
+ public function getRowsPerPage(): int
+ {
+ return $this->rowsPerPage;
+ }
+
+ public function setRowsPerPage(int $rowsPerPage): void
+ {
+ $this->rowsPerPage = $rowsPerPage;
+ }
+
+ public function isFilterable(): bool
+ {
+ return $this->getFilterableColumns() !== [];
+ }
+
+ public function getFilterActionEndpoint(): string
+ {
+ return LinkHandler::getInstance()->getControllerLink(
+ GridViewFilterAction::class,
+ ['gridView' => \get_class($this)]
+ );
+ }
+
+ public function setActiveFilters(array $filters): void
+ {
+ $this->activeFilters = $filters;
+ }
+
+ public function getActiveFilters(): array
+ {
+ return $this->activeFilters;
+ }
+
+ public function getFilterLabel(string $id): string
+ {
+ $column = $this->getColumn($id);
+ if (!$column) {
+ throw new LogicException("Unknown column '" . $id . "'.");
+ }
+
+ if (!$column->getFilter()) {
+ throw new LogicException("Column '" . $id . "' has no filter.");
+ }
+
+ if (!isset($this->activeFilters[$id])) {
+ throw new LogicException("No value for filter '" . $id . "' found.");
+ }
+
+ return $column->getLabel() . ': ' . $column->getFilter()->renderValue($this->activeFilters[$id]);
+ }
+
+ public function getParameters(): array
+ {
+ return [];
+ }
+
+ public function addRowLink(GridViewRowLink $rowLink): void
+ {
+ $this->rowLink = $rowLink;
+ }
+
+ public function getObjectID(mixed $row): mixed
+ {
+ return '';
+ }
+
+ protected function fireInitializedEvent(): void
+ {
+ $event = $this->getInitializedEvent();
+ if ($event === null) {
+ return;
+ }
+
+ EventHandler::getInstance()->fire($event);
+ }
+
+ protected function getInitializedEvent(): ?IPsr14Event
+ {
+ return null;
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\gridView;
+
+use wcf\data\cronjob\Cronjob;
+use wcf\data\cronjob\I18nCronjobList;
+use wcf\data\cronjob\log\CronjobLog;
+use wcf\data\cronjob\log\CronjobLogList;
+use wcf\data\DatabaseObjectList;
+use wcf\event\gridView\CronjobLogGridViewInitialized;
+use wcf\event\IPsr14Event;
+use wcf\system\gridView\filter\SelectFilter;
+use wcf\system\gridView\filter\TimeFilter;
+use wcf\system\gridView\renderer\DefaultColumnRenderer;
+use wcf\system\gridView\renderer\NumberColumnRenderer;
+use wcf\system\gridView\renderer\TimeColumnRenderer;
+use wcf\system\gridView\renderer\TitleColumnRenderer;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+final class CronjobLogGridView extends DatabaseObjectListGridView
+{
+ public function __construct()
+ {
+ $availableCronjobs = $this->getAvailableCronjobs();
+
+ $this->addColumns([
+ GridViewColumn::for('cronjobLogID')
+ ->label('wcf.global.objectID')
+ ->renderer(new NumberColumnRenderer())
+ ->sortable(),
+ GridViewColumn::for('cronjobID')
+ ->label('wcf.acp.cronjob')
+ ->sortable()
+ ->filter(new SelectFilter($availableCronjobs))
+ ->renderer([
+ new class($availableCronjobs) extends TitleColumnRenderer {
+ public function __construct(private readonly array $availableCronjobs) {}
+
+ public function render(mixed $value, mixed $context = null): string
+ {
+ return $this->availableCronjobs[$value];
+ }
+ },
+ ]),
+ GridViewColumn::for('execTime')
+ ->label('wcf.acp.cronjob.log.execTime')
+ ->sortable()
+ ->filter(new TimeFilter())
+ ->renderer(new TimeColumnRenderer()),
+ GridViewColumn::for('success')
+ ->label('wcf.acp.cronjob.log.status')
+ ->sortable()
+ ->filter(new SelectFilter([
+ 1 => 'wcf.acp.cronjob.log.success',
+ 0 => 'wcf.acp.cronjob.log.error',
+ ]))
+ ->renderer([
+ new class extends DefaultColumnRenderer {
+ public function render(mixed $value, mixed $context = null): string
+ {
+ \assert($context instanceof CronjobLog);
+
+ if ($context->success) {
+ return '<span class="badge green">' . WCF::getLanguage()->get('wcf.acp.cronjob.log.success') . '</span>';
+ }
+ if ($context->error) {
+ $label = WCF::getLanguage()->get('wcf.acp.cronjob.log.error');
+ $buttonId = 'cronjobLogErrorButton' . $context->cronjobLogID;
+ $id = 'cronjobLogError' . $context->cronjobLogID;
+ $error = StringUtil::encodeHTML($context->error);
+ $dialogTitle = StringUtil::encodeJS(WCF::getLanguage()->get('wcf.acp.cronjob.log.error.details'));
+
+ return <<<HTML
+ <button type="button" id="{$buttonId}" class="badge red">
+ {$label}
+ </button>
+ <template id="{$id}">{$error}</template>
+ <script data-relocate="true">
+ require(['WoltLabSuite/Core/Component/Dialog'], ({ dialogFactory }) => {
+ document.getElementById('{$buttonId}').addEventListener('click', () => {
+ const dialog = dialogFactory().fromId('{$id}').withoutControls();
+ dialog.show('{$dialogTitle}');
+ });
+ });
+ </script>
+ HTML;
+ }
+
+ return '';
+ }
+ },
+ ]),
+ ]);
+
+ $this->setSortField('execTime');
+ $this->setSortOrder('DESC');
+ }
+
+ #[\Override]
+ public function isAccessible(): bool
+ {
+ return WCF::getSession()->getPermission('admin.management.canManageCronjob');
+ }
+
+ #[\Override]
+ protected function createObjectList(): DatabaseObjectList
+ {
+ return new CronjobLogList();
+ }
+
+ #[\Override]
+ protected function getInitializedEvent(): ?IPsr14Event
+ {
+ return new CronjobLogGridViewInitialized($this);
+ }
+
+ private function getAvailableCronjobs(): array
+ {
+ $list = new I18nCronjobList();
+ $list->sqlOrderBy = 'descriptionI18n';
+ $list->readObjects();
+
+ return \array_map(fn(Cronjob $cronjob) => $cronjob->getDescription(), $list->getObjects());
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\gridView;
+
+use LogicException;
+
+abstract class DataSourceGridView extends AbstractGridView
+{
+ protected array $dataSource;
+
+ public function getRows(): array
+ {
+ $this->sortRows();
+
+ return $this->getRowsForPage();
+ }
+
+ protected function sortRows(): void
+ {
+ $this->getDataSource();
+
+ \uasort($this->dataSource, 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->getDataSource(), ($this->getPageNo() - 1) * $this->getRowsPerPage(), $this->getRowsPerPage());
+ }
+
+ public function countRows(): int
+ {
+ return \count($this->getDataSource());
+ }
+
+ protected function getDataSource(): array
+ {
+ if (!isset($this->dataSource)) {
+ $this->dataSource = $this->loadDataSource();
+ $this->applyFilters();
+ $this->fireInitializedEvent();
+ }
+
+ return $this->dataSource;
+ }
+
+ protected function applyFilters(): void
+ {
+ foreach ($this->getActiveFilters() as $key => $value) {
+ $column = $this->getColumn($key);
+ if (!$column) {
+ throw new LogicException("Unknown column '" . $key . "'");
+ }
+
+ $this->dataSource = \array_filter($this->dataSource, function (array $row) use ($column, $value) {
+ return $column->getFilter()->matches($value, $row[$column->getID()]);
+ });
+ }
+ }
+
+ protected abstract function loadDataSource(): array;
+}
--- /dev/null
+<?php
+
+namespace wcf\system\gridView;
+
+use LogicException;
+use wcf\data\DatabaseObject;
+use wcf\data\DatabaseObjectList;
+
+abstract class DatabaseObjectListGridView extends AbstractGridView
+{
+ protected DatabaseObjectList $objectList;
+ private int $objectCount;
+
+ public function getRows(): array
+ {
+ $this->getObjectList()->readObjects();
+
+ return $this->getObjectList()->getObjects();
+ }
+
+ public function countRows(): int
+ {
+ if (!isset($this->objectCount)) {
+ $this->objectCount = $this->getObjectList()->countObjects();
+ }
+
+ return $this->objectCount;
+ }
+
+ protected function getData(mixed $row, string $identifer): mixed
+ {
+ \assert($row instanceof DatabaseObject);
+
+ return $row->__get($identifer);
+ }
+
+ protected function initObjectList(): void
+ {
+ $this->objectList = $this->createObjectList();
+ $this->objectList->sqlLimit = $this->getRowsPerPage();
+ $this->objectList->sqlOffset = ($this->getPageNo() - 1) * $this->getRowsPerPage();
+ if ($this->getSortField()) {
+ $column = $this->getColumn($this->getSortField());
+ if ($column && $column->getSortById()) {
+ $this->objectList->sqlOrderBy = $column->getSortById() . ' ' . $this->getSortOrder();
+ } else {
+ $this->objectList->sqlOrderBy = $this->getSortField() . ' ' . $this->getSortOrder();
+ }
+ }
+ $this->applyFilters();
+ $this->fireInitializedEvent();
+ }
+
+ public function getObjectList(): DatabaseObjectList
+ {
+ if (!isset($this->objectList)) {
+ $this->initObjectList();
+ }
+
+ return $this->objectList;
+ }
+
+ protected function applyFilters(): void
+ {
+ foreach ($this->getActiveFilters() as $key => $value) {
+ $column = $this->getColumn($key);
+ if (!$column) {
+ throw new LogicException("Unknown column '" . $key . "'");
+ }
+
+ $column->getFilter()->applyFilter($this->getObjectList(), $column->getID(), $value);
+ }
+ }
+
+ #[\Override]
+ public function getObjectID(mixed $row): mixed
+ {
+ \assert($row instanceof DatabaseObject);
+
+ return $row->getObjectID();
+ }
+
+ protected abstract function createObjectList(): DatabaseObjectList;
+}
--- /dev/null
+<?php
+
+namespace wcf\system\gridView;
+
+use wcf\event\gridView\ExceptionLogGridViewInitialized;
+use wcf\event\IPsr14Event;
+use wcf\system\Regex;
+use wcf\system\gridView\filter\SelectFilter;
+use wcf\system\gridView\filter\TextFilter;
+use wcf\system\gridView\renderer\TimeColumnRenderer;
+use wcf\system\gridView\renderer\TitleColumnRenderer;
+use wcf\system\WCF;
+use wcf\util\DirectoryUtil;
+use wcf\util\ExceptionLogUtil;
+
+final class ExceptionLogGridView extends DataSourceGridView
+{
+ private array $availableLogFiles;
+
+ public function __construct(bool $applyDefaultFilter = false)
+ {
+ $this->addColumns([
+ GridViewColumn::for('message')
+ ->label('wcf.acp.exceptionLog.exception.message')
+ ->sortable()
+ ->renderer(new TitleColumnRenderer()),
+ GridViewColumn::for('exceptionID')
+ ->label('wcf.acp.exceptionLog.search.exceptionID')
+ ->filter(new TextFilter())
+ ->sortable(),
+ GridViewColumn::for('date')
+ ->label('wcf.acp.exceptionLog.exception.date')
+ ->sortable()
+ ->renderer(new TimeColumnRenderer()),
+ GridViewColumn::for('logFile')
+ ->label('wcf.acp.exceptionLog.search.logFile')
+ ->filter(new SelectFilter($this->getAvailableLogFiles()))
+ ->hidden(true),
+ ]);
+
+ $this->addRowLink(new GridViewRowLink(cssClass: 'jsExceptionLogEntry'));
+ $this->setSortField('date');
+ $this->setSortOrder('DESC');
+
+ if ($applyDefaultFilter && $this->getDefaultLogFile() !== null) {
+ $this->setActiveFilters([
+ 'logFile' => $this->getDefaultLogFile(),
+ ]);
+ }
+ }
+
+ #[\Override]
+ public function isAccessible(): bool
+ {
+ return WCF::getSession()->getPermission('admin.management.canViewLog');
+ }
+
+ #[\Override]
+ public function getObjectID(mixed $row): mixed
+ {
+ return $row['exceptionID'];
+ }
+
+ #[\Override]
+ protected function loadDataSource(): array
+ {
+ if (!empty($this->getActiveFilters()['exceptionID'])) {
+ $exceptionID = $this->getActiveFilters()['exceptionID'];
+ $contents = $logFile = '';
+ foreach ($this->getAvailableLogFiles() as $logFile) {
+ $contents = \file_get_contents(WCF_DIR . $logFile);
+
+ if (\str_contains($contents, '<<<<<<<<' . $exceptionID . '<<<<')) {
+ break;
+ }
+
+ unset($contents);
+ }
+
+ if ($contents === '') {
+ return [];
+ }
+
+ $exceptions = ExceptionLogUtil::splitLog($contents);
+ $parsedExceptions = [];
+
+ foreach ($exceptions as $key => $val) {
+ if ($key !== $exceptionID) {
+ continue;
+ }
+
+ $parsed = ExceptionLogUtil::parseException($val);
+
+ $parsedExceptions[$key] = [
+ 'exceptionID' => $key,
+ 'message' => $parsed['message'],
+ 'date' => $parsed['date'],
+ 'logFile' => $logFile,
+ ];
+ }
+
+ return $parsedExceptions;
+ } elseif (!empty($this->getActiveFilters()['logFile'])) {
+ $contents = \file_get_contents(WCF_DIR . $this->getActiveFilters()['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'],
+ 'logFile' => $this->getActiveFilters()['logFile'],
+ ];
+ }
+
+ return $parsedExceptions;
+ }
+
+ return [];
+ }
+
+ #[\Override]
+ protected function applyFilters(): void
+ {
+ // Overwrite the default filtering, as this is already applied when the data is loaded.
+ }
+
+ private function getAvailableLogFiles(): array
+ {
+ if (!isset($this->availableLogFiles)) {
+ $this->availableLogFiles = [];
+ $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) {
+ $this->availableLogFiles['log/' . $logFile] = 'log/' . $logFile;
+ }
+ }
+
+ return $this->availableLogFiles;
+ }
+
+ private function getDefaultLogFile(): ?string
+ {
+ return \array_key_first($this->getAvailableLogFiles());
+ }
+
+ #[\Override]
+ protected function getInitializedEvent(): ?IPsr14Event
+ {
+ return new ExceptionLogGridViewInitialized($this);
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\gridView;
+
+use wcf\system\form\builder\field\AbstractFormField;
+use wcf\system\gridView\filter\IGridViewFilter;
+use wcf\system\gridView\renderer\DefaultColumnRenderer;
+use wcf\system\gridView\renderer\IColumnRenderer;
+use wcf\system\gridView\renderer\TitleColumnRenderer;
+use wcf\system\WCF;
+
+final class GridViewColumn
+{
+ /**
+ * @var IColumnRenderer[]
+ */
+ private array $renderer = [];
+ private string $label = '';
+ private static DefaultColumnRenderer $defaultRenderer;
+ private bool $sortable = false;
+ private string $sortById = '';
+ private ?IGridViewFilter $filter = null;
+ private bool $hidden = false;
+
+ private function __construct(private readonly string $id) {}
+
+ public static function for(string $id): static
+ {
+ return new static($id);
+ }
+
+ public function render(mixed $value, mixed $context = null): string
+ {
+ if ($this->getRenderers() === []) {
+ return self::getDefaultRenderer()->render($value, $context);
+ }
+
+ foreach ($this->getRenderers() as $renderer) {
+ $value = $renderer->render($value, $context);
+ }
+
+ return $value;
+ }
+
+ public function getClasses(): string
+ {
+ if ($this->getRenderers() === []) {
+ return self::getDefaultRenderer()->getClasses();
+ }
+
+ return \implode(' ', \array_map(
+ static function (IColumnRenderer $renderer) {
+ return $renderer->getClasses();
+ },
+ $this->getRenderers()
+ ));
+ }
+
+ public function renderer(array|IColumnRenderer $renderers): static
+ {
+ if (!\is_array($renderers)) {
+ $renderers = [$renderers];
+ }
+
+ foreach ($renderers as $renderer) {
+ \assert($renderer instanceof IColumnRenderer);
+ $this->renderer[] = $renderer;
+ }
+
+ return $this;
+ }
+
+ public function label(string $languageItem): static
+ {
+ $this->label = WCF::getLanguage()->get($languageItem);
+
+ return $this;
+ }
+
+ public function sortable(bool $sortable = true): static
+ {
+ $this->sortable = $sortable;
+
+ return $this;
+ }
+
+ public function sortById(string $id): static
+ {
+ $this->sortById = $id;
+
+ return $this;
+ }
+
+ /**
+ * @return IColumnRenderer[]
+ */
+ public function getRenderers(): array
+ {
+ return $this->renderer;
+ }
+
+ public function getID(): string
+ {
+ return $this->id;
+ }
+
+ public function getLabel(): string
+ {
+ return $this->label;
+ }
+
+ public function isSortable(): bool
+ {
+ return $this->sortable;
+ }
+
+ public function getSortById(): string
+ {
+ return $this->sortById;
+ }
+
+ public function filter(?IGridViewFilter $filter): static
+ {
+ $this->filter = $filter;
+
+ return $this;
+ }
+
+ public function getFilter(): ?IGridViewFilter
+ {
+ return $this->filter;
+ }
+
+ public function getFilterFormField(): AbstractFormField
+ {
+ if ($this->getFilter() === null) {
+ throw new \LogicException('This column has no filter.');
+ }
+
+ return $this->getFilter()->getFormField($this->getID(), $this->getLabel());
+ }
+
+ private static function getDefaultRenderer(): DefaultColumnRenderer
+ {
+ if (!isset(self::$defaultRenderer)) {
+ self::$defaultRenderer = new DefaultColumnRenderer();
+ }
+
+ return self::$defaultRenderer;
+ }
+
+ public function isTitleColumn(): bool
+ {
+ foreach ($this->getRenderers() as $renderer) {
+ if ($renderer instanceof TitleColumnRenderer) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public function hidden(bool $hidden = true): static
+ {
+ $this->hidden = $hidden;
+
+ return $this;
+ }
+
+ public function isHidden(): bool
+ {
+ return $this->hidden;
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\gridView;
+
+use wcf\data\DatabaseObject;
+use wcf\system\request\LinkHandler;
+use wcf\util\StringUtil;
+
+class GridViewRowLink
+{
+ public function __construct(
+ private readonly string $controllerClass = '',
+ private readonly array $parameters = [],
+ private readonly string $cssClass = ''
+ ) {}
+
+ public function render(mixed $value, mixed $context = null, bool $isPrimaryColumn = false): string
+ {
+ $href = '';
+ if ($this->controllerClass) {
+ \assert($context instanceof DatabaseObject);
+ $href = LinkHandler::getInstance()->getControllerLink(
+ $this->controllerClass,
+ \array_merge($this->parameters, ['object' => $context])
+ );
+ }
+
+ $attributes = [];
+ $isButton = true;
+ if ($href) {
+ $attributes[] = 'href="' . $href . '"';
+ $isButton = false;
+ }
+ $attributes[] = 'class="gridView__rowLink ' . StringUtil::encodeHTML($this->cssClass) . '"';
+ $attributes[] = 'tabindex="' . ($isPrimaryColumn ? '0' : '-1') . '"';
+
+ if ($isButton) {
+ return '<button type="button" ' . implode(' ', $attributes) . '>'
+ . $value
+ . '</button>';
+ } else {
+ return '<a ' . implode(' ', $attributes) . '>'
+ . $value
+ . '</a>';
+ }
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\gridView;
+
+use wcf\acp\form\UserOptionEditForm;
+use wcf\data\DatabaseObjectList;
+use wcf\data\user\option\UserOption;
+use wcf\data\user\option\UserOptionList;
+use wcf\event\gridView\UserOptionGridViewInitialized;
+use wcf\event\IPsr14Event;
+use wcf\system\gridView\action\DeleteAction;
+use wcf\system\gridView\action\EditAction;
+use wcf\system\gridView\action\ToggleAction;
+use wcf\system\gridView\renderer\DefaultColumnRenderer;
+use wcf\system\gridView\renderer\NumberColumnRenderer;
+use wcf\system\gridView\renderer\TitleColumnRenderer;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+final class UserOptionGridView extends DatabaseObjectListGridView
+{
+ public function __construct()
+ {
+ $this->addColumns([
+ GridViewColumn::for('optionID')
+ ->label('wcf.global.objectID')
+ ->renderer(new NumberColumnRenderer())
+ ->sortable(),
+ GridViewColumn::for('optionName')
+ ->label('wcf.global.name')
+ ->sortable()
+ ->renderer([
+ new class extends TitleColumnRenderer {
+ public function render(mixed $value, mixed $context = null): string
+ {
+ \assert($context instanceof UserOption);
+
+ return StringUtil::encodeHTML($context->getTitle());
+ }
+ }
+ ]),
+ GridViewColumn::for('categoryName')
+ ->label('wcf.global.category')
+ ->sortable()
+ ->renderer([
+ new class extends DefaultColumnRenderer {
+ public function render(mixed $value, mixed $context = null): string
+ {
+ \assert($context instanceof UserOption);
+
+ return StringUtil::encodeHTML(
+ WCF::getLanguage()->get('wcf.user.option.category.' . $context->categoryName)
+ );
+ }
+ }
+ ]),
+ GridViewColumn::for('optionType')
+ ->label('wcf.acp.user.option.optionType')
+ ->sortable(),
+ GridViewColumn::for('showOrder')
+ ->label('wcf.global.showOrder')
+ ->sortable()
+ ->renderer(new NumberColumnRenderer()),
+ ]);
+
+ $this->addActions([
+ new ToggleAction('core/users/options/%s/enable', 'core/users/options/%s/disable'),
+ new EditAction(UserOptionEditForm::class),
+ new DeleteAction('core/users/options/%s', fn(UserOption $row) => $row->canDelete()),
+ ]);
+ $this->addRowLink(new GridViewRowLink(UserOptionEditForm::class));
+ $this->setSortField('showOrder');
+ }
+
+ #[\Override]
+ public function isAccessible(): bool
+ {
+ return WCF::getSession()->getPermission('admin.user.canManageUserOption');
+ }
+
+ #[\Override]
+ protected function createObjectList(): DatabaseObjectList
+ {
+ $list = new UserOptionList();
+ $list->getConditionBuilder()->add(
+ "option_table.categoryName IN (
+ SELECT categoryName
+ FROM wcf" . WCF_N . "_user_option_category
+ WHERE parentCategoryName = ?
+ )",
+ ['profile']
+ );
+
+ return $list;
+ }
+
+ #[\Override]
+ protected function getInitializedEvent(): ?IPsr14Event
+ {
+ return new UserOptionGridViewInitialized($this);
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\gridView;
+
+use wcf\acp\form\UserRankEditForm;
+use wcf\data\DatabaseObjectList;
+use wcf\data\user\group\UserGroup;
+use wcf\data\user\rank\I18nUserRankList;
+use wcf\data\user\rank\UserRank;
+use wcf\event\gridView\UserRankGridViewInitialized;
+use wcf\event\IPsr14Event;
+use wcf\system\gridView\action\DeleteAction;
+use wcf\system\gridView\action\EditAction;
+use wcf\system\gridView\filter\I18nTextFilter;
+use wcf\system\gridView\filter\SelectFilter;
+use wcf\system\gridView\renderer\DefaultColumnRenderer;
+use wcf\system\gridView\renderer\NumberColumnRenderer;
+use wcf\system\gridView\renderer\TitleColumnRenderer;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+final class UserRankGridView extends DatabaseObjectListGridView
+{
+ public function __construct()
+ {
+ $this->addColumns([
+ GridViewColumn::for('rankID')
+ ->label('wcf.global.objectID')
+ ->renderer(new NumberColumnRenderer())
+ ->sortable(),
+ GridViewColumn::for('rankTitle')
+ ->label('wcf.acp.user.rank.title')
+ ->sortable()
+ ->sortById('rankTitleI18n')
+ ->filter(new I18nTextFilter())
+ ->renderer([
+ new class extends TitleColumnRenderer {
+ public function render(mixed $value, mixed $context = null): string
+ {
+ \assert($context instanceof UserRank);
+
+ return '<span class="badge label' . ($context->cssClassName ? ' ' . $context->cssClassName : '') . '">'
+ . StringUtil::encodeHTML($context->getTitle())
+ . '<span>';
+ }
+ }
+ ]),
+ GridViewColumn::for('rankImage')
+ ->label('wcf.acp.user.rank.image')
+ ->sortable()
+ ->renderer([
+ new class extends DefaultColumnRenderer {
+ public function render(mixed $value, mixed $context = null): string
+ {
+ \assert($context instanceof UserRank);
+
+ return $context->rankImage ? $context->getImage() : '';
+ }
+ },
+ ]),
+ GridViewColumn::for('groupID')
+ ->label('wcf.user.group')
+ ->sortable()
+ ->filter(new SelectFilter($this->getAvailableUserGroups()))
+ ->renderer([
+ new class extends DefaultColumnRenderer {
+ public function render(mixed $value, mixed $context = null): string
+ {
+ return StringUtil::encodeHTML(UserGroup::getGroupByID($value)->getName());
+ }
+ },
+ ]),
+ GridViewColumn::for('requiredGender')
+ ->label('wcf.user.option.gender')
+ ->sortable()
+ ->renderer([
+ new class extends DefaultColumnRenderer {
+ public function render(mixed $value, mixed $context = null): string
+ {
+ if (!$value) {
+ return '';
+ }
+
+ return WCF::getLanguage()->get(match ($value) {
+ 1 => 'wcf.user.gender.male',
+ 2 => 'wcf.user.gender.female',
+ default => 'wcf.user.gender.other'
+ });
+ }
+ },
+ ]),
+ GridViewColumn::for('requiredPoints')
+ ->label('wcf.acp.user.rank.requiredPoints')
+ ->sortable()
+ ->renderer(new NumberColumnRenderer()),
+ ]);
+
+ $this->addActions([
+ new EditAction(UserRankEditForm::class),
+ new DeleteAction('core/users/ranks/%s'),
+ ]);
+ $this->addRowLink(new GridViewRowLink(UserRankEditForm::class));
+ $this->setSortField('rankTitle');
+ }
+
+ #[\Override]
+ public function isAccessible(): bool
+ {
+ return \MODULE_USER_RANK && WCF::getSession()->getPermission('admin.user.rank.canManageRank');
+ }
+
+ #[\Override]
+ protected function createObjectList(): DatabaseObjectList
+ {
+ return new I18nUserRankList();
+ }
+
+ #[\Override]
+ protected function getInitializedEvent(): ?IPsr14Event
+ {
+ return new UserRankGridViewInitialized($this);
+ }
+
+ private function getAvailableUserGroups(): array
+ {
+ $groups = [];
+ foreach (UserGroup::getSortedGroupsByType([], [UserGroup::GUESTS, UserGroup::EVERYONE]) as $group) {
+ $groups[$group->groupID] = $group->getName();
+ }
+
+ return $groups;
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\gridView\action;
+
+use Closure;
+
+abstract class AbstractAction implements IGridViewAction
+{
+ public function __construct(
+ private readonly ?Closure $isAvailableCallback = null
+ ) {}
+
+ #[\Override]
+ public function isAvailable(mixed $row): bool
+ {
+ if ($this->isAvailableCallback === null) {
+ return true;
+ }
+
+ return ($this->isAvailableCallback)($row);
+ }
+
+ #[\Override]
+ public function isQuickAction(): bool
+ {
+ return false;
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\gridView\action;
+
+use Closure;
+use wcf\action\ApiAction;
+use wcf\data\DatabaseObject;
+use wcf\data\ITitledObject;
+use wcf\system\gridView\AbstractGridView;
+use wcf\system\request\LinkHandler;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+class DeleteAction extends AbstractAction
+{
+ public function __construct(
+ private readonly string $endpoint,
+ ?Closure $isAvailableCallback = null
+ ) {
+ parent::__construct($isAvailableCallback);
+ }
+
+ #[\Override]
+ public function render(mixed $row): string
+ {
+ \assert($row instanceof DatabaseObject);
+
+ $label = WCF::getLanguage()->get('wcf.global.button.delete');
+
+ if (!$this->isAvailable($row)) {
+ return <<<HTML
+ <span>
+ {$label}
+ </span>
+ HTML;
+ }
+
+ $endpoint = StringUtil::encodeHTML(
+ LinkHandler::getInstance()->getControllerLink(ApiAction::class, ['id' => 'rpc']) .
+ \sprintf($this->endpoint, $row->getObjectID())
+ );
+ if ($row instanceof ITitledObject) {
+ $objectName = StringUtil::encodeHTML($row->getTitle());
+ } else {
+ $objectName = '';
+ }
+
+ return <<<HTML
+ <button type="button" data-action="delete" data-object-name="{$objectName}" data-endpoint="{$endpoint}">
+ {$label}
+ </button>
+ HTML;
+ }
+
+ #[\Override]
+ public function renderInitialization(AbstractGridView $gridView): ?string
+ {
+ $id = StringUtil::encodeJS($gridView->getID());
+
+ return <<<HTML
+ <script data-relocate="true">
+ require(['WoltLabSuite/Core/Component/GridView/Action/Delete'], ({ setup }) => {
+ setup(document.getElementById('{$id}_table'));
+ });
+ </script>
+ HTML;
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\gridView\action;
+
+use Closure;
+use wcf\data\DatabaseObject;
+use wcf\system\gridView\AbstractGridView;
+use wcf\system\request\LinkHandler;
+use wcf\system\WCF;
+
+class EditAction extends AbstractAction
+{
+ public function __construct(
+ private readonly string $controllerClass,
+ ?Closure $isAvailableCallback = null
+ ) {
+ parent::__construct($isAvailableCallback);
+ }
+
+ #[\Override]
+ public function render(mixed $row): string
+ {
+ \assert($row instanceof DatabaseObject);
+ $href = LinkHandler::getInstance()->getControllerLink(
+ $this->controllerClass,
+ ['object' => $row]
+ );
+
+ return '<a href="' . $href . '">' . WCF::getLanguage()->get('wcf.global.button.edit') . '</a>';
+ }
+
+ #[\Override]
+ public function renderInitialization(AbstractGridView $gridView): ?string
+ {
+ return null;
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\gridView\action;
+
+use wcf\system\gridView\AbstractGridView;
+
+interface IGridViewAction
+{
+ public function render(mixed $row): string;
+
+ public function renderInitialization(AbstractGridView $gridView): ?string;
+
+ public function isQuickAction(): bool;
+
+ public function isAvailable(mixed $row): bool;
+}
--- /dev/null
+<?php
+
+namespace wcf\system\gridView\action;
+
+use Closure;
+use wcf\action\ApiAction;
+use wcf\data\DatabaseObject;
+use wcf\system\gridView\AbstractGridView;
+use wcf\system\request\LinkHandler;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+class ToggleAction extends AbstractAction
+{
+ public function __construct(
+ private readonly string $enableEndpoint,
+ private readonly string $disableEndpoint,
+ private readonly string $propertyName = 'isDisabled',
+ ?Closure $isAvailableCallback = null
+ ) {
+ parent::__construct($isAvailableCallback);
+ }
+
+ #[\Override]
+ public function render(mixed $row): string
+ {
+ \assert($row instanceof DatabaseObject);
+
+ $enableEndpoint = StringUtil::encodeHTML(
+ LinkHandler::getInstance()->getControllerLink(ApiAction::class, ['id' => 'rpc']) .
+ \sprintf($this->enableEndpoint, $row->getObjectID())
+ );
+ $disableEndpoint = StringUtil::encodeHTML(
+ LinkHandler::getInstance()->getControllerLink(ApiAction::class, ['id' => 'rpc']) .
+ \sprintf($this->disableEndpoint, $row->getObjectID())
+ );
+
+ $ariaLabel = WCF::getLanguage()->get('wcf.global.button.enable');
+ $checked = !$row->{$this->propertyName} ? 'checked' : '';
+
+ return <<<HTML
+ <woltlab-core-toggle-button aria-label="{$ariaLabel}" data-enable-endpoint="{$enableEndpoint}" data-disable-endpoint="{$disableEndpoint}" {$checked}></woltlab-core-toggle-button>
+ HTML;
+ }
+
+ #[\Override]
+ public function renderInitialization(AbstractGridView $gridView): ?string
+ {
+ $id = StringUtil::encodeJS($gridView->getID());
+
+ return <<<HTML
+ <script data-relocate="true">
+ require(['WoltLabSuite/Core/Component/GridView/Action/Toggle'], ({ setup }) => {
+ setup('{$id}_table');
+ });
+ </script>
+ HTML;
+ }
+
+ #[\Override]
+ public function isQuickAction(): bool
+ {
+ return true;
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\gridView\filter;
+
+use wcf\data\DatabaseObjectList;
+use wcf\system\WCF;
+
+class I18nTextFilter extends TextFilter
+{
+ #[\Override]
+ public function applyFilter(DatabaseObjectList $list, string $id, string $value): void
+ {
+ $list->getConditionBuilder()->add("($id LIKE ? OR $id IN (SELECT languageItem FROM wcf1_language_item WHERE languageID = ? AND languageItemValue LIKE ?))", [
+ '%' . $value . '%',
+ WCF::getLanguage()->languageID,
+ '%' . $value . '%'
+ ]);
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\gridView\filter;
+
+use wcf\data\DatabaseObjectList;
+use wcf\system\form\builder\field\AbstractFormField;
+
+interface IGridViewFilter
+{
+ public function getFormField(string $id, string $label): AbstractFormField;
+
+ public function applyFilter(DatabaseObjectList $list, string $id, string $value): void;
+
+ public function matches(string $filterValue, string $rowValue): bool;
+
+ public function renderValue(string $value): string;
+}
--- /dev/null
+<?php
+
+namespace wcf\system\gridView\filter;
+
+use wcf\data\DatabaseObjectList;
+use wcf\system\form\builder\field\AbstractFormField;
+use wcf\system\form\builder\field\SelectFormField;
+use wcf\system\WCF;
+
+class SelectFilter implements IGridViewFilter
+{
+ public function __construct(private readonly array $options) {}
+
+ #[\Override]
+ public function getFormField(string $id, string $label): AbstractFormField
+ {
+ return SelectFormField::create($id)
+ ->label($label)
+ ->options($this->options);
+ }
+
+ #[\Override]
+ public function applyFilter(DatabaseObjectList $list, string $id, string $value): void
+ {
+ $list->getConditionBuilder()->add("$id = ?", [$value]);
+ }
+
+ #[\Override]
+ public function matches(string $filterValue, string $rowValue): bool
+ {
+ return $filterValue === $rowValue;
+ }
+
+ #[\Override]
+ public function renderValue(string $value): string
+ {
+ return WCF::getLanguage()->get($this->options[$value]);
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\gridView\filter;
+
+use wcf\data\DatabaseObjectList;
+use wcf\system\form\builder\field\AbstractFormField;
+use wcf\system\form\builder\field\TextFormField;
+
+class TextFilter implements IGridViewFilter
+{
+ #[\Override]
+ public function getFormField(string $id, string $label): AbstractFormField
+ {
+ return TextFormField::create($id)
+ ->label($label);
+ }
+
+ #[\Override]
+ public function applyFilter(DatabaseObjectList $list, string $id, string $value): void
+ {
+ $list->getConditionBuilder()->add("$id LIKE ?", ['%' . $value . '%']);
+ }
+
+ #[\Override]
+ public function matches(string $filterValue, string $rowValue): bool
+ {
+ return \str_contains(\mb_strtolower($rowValue), \mb_strtolower($filterValue));
+ }
+
+ #[\Override]
+ public function renderValue(string $value): string
+ {
+ return $value;
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\gridView\filter;
+
+use wcf\data\DatabaseObjectList;
+use wcf\system\form\builder\field\AbstractFormField;
+use wcf\system\form\builder\field\DateRangeFormField;
+use wcf\system\WCF;
+
+class TimeFilter implements IGridViewFilter
+{
+ #[\Override]
+ public function getFormField(string $id, string $label): AbstractFormField
+ {
+ return DateRangeFormField::create($id)
+ ->label($label)
+ ->supportTime();
+ }
+
+ #[\Override]
+ public function applyFilter(DatabaseObjectList $list, string $id, string $value): void
+ {
+ $timestamps = $this->getTimestamps($value);
+
+ if (!$timestamps['from'] && !$timestamps['to']) {
+ return;
+ }
+
+ if (!$timestamps['to']) {
+ $list->getConditionBuilder()->add("$id >= ?", [$timestamps['from']]);
+ } else {
+ $list->getConditionBuilder()->add("$id BETWEEN ? AND ?", [$timestamps['from'], $timestamps['to']]);
+ }
+ }
+
+ #[\Override]
+ public function matches(string $filterValue, string $rowValue): bool
+ {
+ $timestamps = $this->getTimestamps($filterValue);
+
+ if (!$timestamps['from'] && !$timestamps['to']) {
+ return true;
+ }
+
+ if (!$timestamps['to']) {
+ return $rowValue >= $timestamps['from'];
+ } else {
+ return $rowValue >= $timestamps['from'] && $rowValue <= $timestamps['to'];
+ }
+ }
+
+ #[\Override]
+ public function renderValue(string $value): string
+ {
+ $values = explode(';', $value);
+ if (\count($values) !== 2) {
+ return '';
+ }
+
+ $locale = WCF::getLanguage()->getLocale();;
+ $fromString = $toString = '';
+ if ($values[0] !== '') {
+ $fromDateTime = \DateTime::createFromFormat(
+ 'Y-m-d\TH:i:sP',
+ $values[0],
+ WCF::getUser()->getTimeZone()
+ );
+ if ($fromDateTime !== false) {
+ $fromString = \IntlDateFormatter::formatObject(
+ $fromDateTime,
+ [
+ \IntlDateFormatter::LONG,
+ \IntlDateFormatter::SHORT,
+ ],
+ $locale
+ );
+ }
+ }
+ if ($values[1] !== '') {
+ $toDateTime = \DateTime::createFromFormat(
+ 'Y-m-d\TH:i:sP',
+ $values[1],
+ WCF::getUser()->getTimeZone()
+ );
+ if ($toDateTime !== false) {
+ $toString = \IntlDateFormatter::formatObject(
+ $toDateTime,
+ [
+ \IntlDateFormatter::LONG,
+ \IntlDateFormatter::SHORT,
+ ],
+ $locale
+ );
+ }
+ }
+
+ if ($fromString && $toString) {
+ return $fromString . ' ‐ ' . $toString;
+ } else if ($fromString) {
+ return '>= ' . $fromString;
+ } else if ($toString) {
+ return '<= ' . $toString;
+ }
+
+ return '';
+ }
+
+ private function getTimestamps(string $value): array
+ {
+ $from = 0;
+ $to = 0;
+
+ $values = explode(';', $value);
+ if (\count($values) === 2) {
+ $from = $this->getTimestamp($values[0]);
+ $to = $this->getTimestamp($values[1]);
+ }
+
+ return [
+ 'from' => $from,
+ 'to' => $to,
+ ];
+ }
+
+ private function getTimestamp(string $date): int
+ {
+ $dateTime = \DateTime::createFromFormat(
+ 'Y-m-d\TH:i:sP',
+ $date
+ );
+
+ if ($dateTime !== false) {
+ return $dateTime->getTimestamp();
+ }
+
+ return 0;
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\gridView\renderer;
+
+abstract class AbstractColumnRenderer implements IColumnRenderer
+{
+ public function getClasses(): string
+ {
+ return '';
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\gridView\renderer;
+
+use wcf\util\StringUtil;
+
+class DefaultColumnRenderer extends AbstractColumnRenderer
+{
+ public function render(mixed $value, mixed $context = null): string
+ {
+ return StringUtil::encodeHTML($value);
+ }
+
+ public function getClasses(): string
+ {
+ return 'gridView__column--text';
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\gridView\renderer;
+
+interface IColumnRenderer
+{
+ public function render(mixed $value, mixed $context = null): string;
+
+ public function getClasses(): string;
+}
--- /dev/null
+<?php
+
+namespace wcf\system\gridView\renderer;
+
+use wcf\data\DatabaseObject;
+use wcf\system\request\LinkHandler;
+use wcf\system\WCF;
+
+class LinkColumnRenderer extends AbstractColumnRenderer
+{
+ public function __construct(
+ private readonly string $controllerClass,
+ private readonly array $parameters = [],
+ private readonly string $titleLanguageItem = ''
+ ) {}
+
+ public function render(mixed $value, mixed $context = null): string
+ {
+ \assert($context instanceof DatabaseObject);
+ $href = LinkHandler::getInstance()->getControllerLink(
+ $this->controllerClass,
+ \array_merge($this->parameters, ['object' => $context])
+ );
+
+ return '<a href="' . $href . '"'
+ . ($this->titleLanguageItem ? ' title="' . WCF::getLanguage()->get($this->titleLanguageItem) . '"' : '') . '>'
+ . $value
+ . '</a>';
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\gridView\renderer;
+
+use wcf\util\StringUtil;
+
+class NumberColumnRenderer extends AbstractColumnRenderer
+{
+ public function render(mixed $value, mixed $context = null): string
+ {
+ return StringUtil::formatNumeric($value);
+ }
+
+ public function getClasses(): string
+ {
+ return 'gridView__column--digits';
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\gridView\renderer;
+
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+class PhraseColumnRenderer extends DefaultColumnRenderer
+{
+ public function render(mixed $value, mixed $context = null): string
+ {
+ return StringUtil::encodeHTML(WCF::getLanguage()->get($value));
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\gridView\renderer;
+
+use wcf\system\WCF;
+
+class TimeColumnRenderer extends AbstractColumnRenderer
+{
+ public function render(mixed $value, mixed $context = null): string
+ {
+ $timestamp = \intval($value);
+ if (!$timestamp) {
+ return '';
+ }
+
+ $dateTime = new \DateTimeImmutable('@' . $timestamp);
+ $dateTime = $dateTime->setTimezone(WCF::getUser()->getTimeZone());
+ $locale = WCF::getLanguage()->getLocale();
+
+ $isFutureDate = $dateTime->getTimestamp() > TIME_NOW;
+
+ $dateAndTime = \IntlDateFormatter::formatObject(
+ $dateTime,
+ [
+ \IntlDateFormatter::LONG,
+ \IntlDateFormatter::SHORT,
+ ],
+ $locale
+ );
+
+ return \sprintf(
+ '<woltlab-core-date-time date="%s"%s>%s</woltlab-core-date-time>',
+ $dateTime->format('c'),
+ $isFutureDate ? ' static' : '',
+ $dateAndTime
+ );
+ }
+
+ public function getClasses(): string
+ {
+ return 'gridView__column--date';
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\gridView\renderer;
+
+class TitleColumnRenderer extends DefaultColumnRenderer
+{
+ public function getClasses(): string
+ {
+ return 'gridView__column--title';
+ }
+}
+++ /dev/null
-<?php
-
-namespace wcf\system\view\grid;
-
-use LogicException;
-use wcf\action\GridViewFilterAction;
-use wcf\event\IPsr14Event;
-use wcf\system\event\EventHandler;
-use wcf\system\request\LinkHandler;
-use wcf\system\view\grid\action\IGridViewAction;
-use wcf\system\WCF;
-
-abstract class AbstractGridView
-{
- /**
- * @var GridViewColumn[]
- */
- private array $columns = [];
-
- /**
- * @var IGridViewAction[]
- */
- private array $actions = [];
-
- private GridViewRowLink $rowLink;
- private int $rowsPerPage = 20;
- private string $baseUrl = '';
- private string $sortField = '';
- private string $sortOrder = 'ASC';
- private int $pageNo = 1;
- private array $activeFilters = [];
-
- /**
- * Adds a new column to the grid view.
- */
- public function addColumn(GridViewColumn $column): void
- {
- $this->columns[] = $column;
- }
-
- /**
- * Adds a new column to the grid view at the position before the specific id.
- */
- public function addColumnBefore(GridViewColumn $column, string $beforeID): void
- {
- $position = -1;
-
- foreach ($this->getColumns() as $key => $existingColumn) {
- if ($existingColumn->getID() === $beforeID) {
- $position = $key;
- break;
- }
- }
-
- if ($position === -1) {
- throw new \InvalidArgumentException("Invalid column id '{$beforeID}' given.");
- }
-
- array_splice($this->columns, $position, 0, [
- $column,
- ]);
- }
-
- /**
- * Adds a new column to the grid view at the position after the specific id.
- */
- public function addColumnAfter(GridViewColumn $column, string $afterID): void
- {
- $position = -1;
-
- foreach ($this->getColumns() as $key => $existingColumn) {
- if ($existingColumn->getID() === $afterID) {
- $position = $key;
- break;
- }
- }
-
- if ($position === -1) {
- throw new \InvalidArgumentException("Invalid column id '{$afterID}' given.");
- }
-
- array_splice($this->columns, $position + 1, 0, [
- $column,
- ]);
- }
-
- /**
- * Adds multiple new columns to the grid view.
- * @param GridViewColumn[] $columns
- */
- public function addColumns(array $columns): void
- {
- foreach ($columns as $column) {
- $this->addColumn($column);
- }
- }
-
- /**
- * Returns all columns of the grid view.
- * @return GridViewColumn[]
- */
- public function getColumns(): array
- {
- return $this->columns;
- }
-
- /**
- * Returns all visible (non-hidden) columns of the grid view.
- * @return GridViewColumn[]
- */
- public function getVisibleColumns(): array
- {
- return \array_filter($this->getColumns(), fn($column) => !$column->isHidden());
- }
-
- /**
- * Returns the column with the given id or null if no such column exists.
- */
- public function getColumn(string $id): ?GridViewColumn
- {
- foreach ($this->getColumns() as $column) {
- if ($column->getID() === $id) {
- return $column;
- }
- }
-
- return null;
- }
-
- /**
- * @param IGridViewAction[] $columns
- */
- public function addActions(array $actions): void
- {
- foreach ($actions as $action) {
- $this->addAction($action);
- }
- }
-
- public function addAction(IGridViewAction $action): void
- {
- $this->actions[] = $action;
- }
-
- /**
- * @return IGridViewAction[]
- */
- public function getActions(): array
- {
- return $this->actions;
- }
-
- public function hasActions(): bool
- {
- return $this->actions !== [];
- }
-
- public function hasDropdownActions(): bool
- {
- return $this->getDropdownActions() !== [];
- }
-
- /**
- * @return IGridViewAction[]
- */
- public function getDropdownActions(): array
- {
- return \array_filter($this->getActions(), fn($action) => !$action->isQuickAction());
- }
-
- /**
- * @return IGridViewAction[]
- */
- public function getQuickActions(): array
- {
- return \array_filter($this->getActions(), fn($action) => $action->isQuickAction());
- }
-
- public function render(): string
- {
- return WCF::getTPL()->fetch('shared_gridView', 'wcf', ['view' => $this], true);
- }
-
- public function renderRows(): string
- {
- return WCF::getTPL()->fetch('shared_gridViewRows', 'wcf', ['view' => $this], true);
- }
-
- public function renderColumn(GridViewColumn $column, mixed $row): string
- {
- $value = $column->render($this->getData($row, $column->getID()), $row);
-
- if (isset($this->rowLink)) {
- $value = $this->rowLink->render($value, $row, $column->isTitleColumn());
- }
-
- return $value;
- }
-
- public function renderAction(IGridViewAction $action, mixed $row): string
- {
- return $action->render($row);
- }
-
- public function renderActionInitialization(): string
- {
- return implode(
- "\n",
- \array_map(
- fn($action) => $action->renderInitialization($this),
- $this->getActions()
- )
- );
- }
-
- protected function getData(mixed $row, string $identifer): mixed
- {
- return $row[$identifer] ?? '';
- }
-
- public abstract function getRows(): array;
-
- public abstract function countRows(): int;
-
- public function countPages(): int
- {
- return \ceil($this->countRows() / $this->getRowsPerPage());
- }
-
- public function getClassName(): string
- {
- return \get_class($this);
- }
-
- public function isAccessible(): bool
- {
- return true;
- }
-
- public function getID(): string
- {
- $classNamePieces = \explode('\\', static::class);
-
- return \implode('-', $classNamePieces);
- }
-
- public function setBaseUrl(string $url): void
- {
- $this->baseUrl = $url;
- }
-
- public function getBaseUrl(): string
- {
- return $this->baseUrl;
- }
-
- /**
- * @return GridViewColumn[]
- */
- public function getSortableColumns(): array
- {
- return \array_filter($this->getColumns(), fn($column) => $column->isSortable());
- }
-
- /**
- * @return GridViewColumn[]
- */
- public function getFilterableColumns(): array
- {
- return \array_filter($this->getColumns(), fn($column) => $column->getFilter() !== null);
- }
-
- public function setSortField(string $sortField): void
- {
- if (!\in_array($sortField, \array_map(fn($column) => $column->getID(), $this->getSortableColumns()))) {
- throw new \InvalidArgumentException("Invalid value '{$sortField}' as sort field given.");
- }
-
- $this->sortField = $sortField;
- }
-
- public function setSortOrder(string $sortOrder): void
- {
- if ($sortOrder !== 'ASC' && $sortOrder !== 'DESC') {
- throw new \InvalidArgumentException("Invalid value '{$sortOrder}' as sort order given.");
- }
-
- $this->sortOrder = $sortOrder;
- }
-
- public function getSortField(): string
- {
- return $this->sortField;
- }
-
- public function getSortOrder(): string
- {
- return $this->sortOrder;
- }
-
- public function getPageNo(): int
- {
- return $this->pageNo;
- }
-
- public function setPageNo(int $pageNo): void
- {
- $this->pageNo = $pageNo;
- }
-
- public function getRowsPerPage(): int
- {
- return $this->rowsPerPage;
- }
-
- public function setRowsPerPage(int $rowsPerPage): void
- {
- $this->rowsPerPage = $rowsPerPage;
- }
-
- public function isFilterable(): bool
- {
- return $this->getFilterableColumns() !== [];
- }
-
- public function getFilterActionEndpoint(): string
- {
- return LinkHandler::getInstance()->getControllerLink(
- GridViewFilterAction::class,
- ['gridView' => \get_class($this)]
- );
- }
-
- public function setActiveFilters(array $filters): void
- {
- $this->activeFilters = $filters;
- }
-
- public function getActiveFilters(): array
- {
- return $this->activeFilters;
- }
-
- public function getFilterLabel(string $id): string
- {
- $column = $this->getColumn($id);
- if (!$column) {
- throw new LogicException("Unknown column '" . $id . "'.");
- }
-
- if (!$column->getFilter()) {
- throw new LogicException("Column '" . $id . "' has no filter.");
- }
-
- if (!isset($this->activeFilters[$id])) {
- throw new LogicException("No value for filter '" . $id . "' found.");
- }
-
- return $column->getLabel() . ': ' . $column->getFilter()->renderValue($this->activeFilters[$id]);
- }
-
- public function getParameters(): array
- {
- return [];
- }
-
- public function addRowLink(GridViewRowLink $rowLink): void
- {
- $this->rowLink = $rowLink;
- }
-
- public function getObjectID(mixed $row): mixed
- {
- return '';
- }
-
- protected function fireInitializedEvent(): void
- {
- $event = $this->getInitializedEvent();
- if ($event === null) {
- return;
- }
-
- EventHandler::getInstance()->fire($event);
- }
-
- protected function getInitializedEvent(): ?IPsr14Event
- {
- return null;
- }
-}
+++ /dev/null
-<?php
-
-namespace wcf\system\view\grid;
-
-use wcf\data\cronjob\Cronjob;
-use wcf\data\cronjob\I18nCronjobList;
-use wcf\data\cronjob\log\CronjobLog;
-use wcf\data\cronjob\log\CronjobLogList;
-use wcf\data\DatabaseObjectList;
-use wcf\event\gridView\CronjobLogGridViewInitialized;
-use wcf\event\IPsr14Event;
-use wcf\system\view\grid\filter\SelectFilter;
-use wcf\system\view\grid\filter\TimeFilter;
-use wcf\system\view\grid\renderer\DefaultColumnRenderer;
-use wcf\system\view\grid\renderer\NumberColumnRenderer;
-use wcf\system\view\grid\renderer\TimeColumnRenderer;
-use wcf\system\view\grid\renderer\TitleColumnRenderer;
-use wcf\system\WCF;
-use wcf\util\StringUtil;
-
-final class CronjobLogGridView extends DatabaseObjectListGridView
-{
- public function __construct()
- {
- $availableCronjobs = $this->getAvailableCronjobs();
-
- $this->addColumns([
- GridViewColumn::for('cronjobLogID')
- ->label('wcf.global.objectID')
- ->renderer(new NumberColumnRenderer())
- ->sortable(),
- GridViewColumn::for('cronjobID')
- ->label('wcf.acp.cronjob')
- ->sortable()
- ->filter(new SelectFilter($availableCronjobs))
- ->renderer([
- new class($availableCronjobs) extends TitleColumnRenderer {
- public function __construct(private readonly array $availableCronjobs) {}
-
- public function render(mixed $value, mixed $context = null): string
- {
- return $this->availableCronjobs[$value];
- }
- },
- ]),
- GridViewColumn::for('execTime')
- ->label('wcf.acp.cronjob.log.execTime')
- ->sortable()
- ->filter(new TimeFilter())
- ->renderer(new TimeColumnRenderer()),
- GridViewColumn::for('success')
- ->label('wcf.acp.cronjob.log.status')
- ->sortable()
- ->filter(new SelectFilter([
- 1 => 'wcf.acp.cronjob.log.success',
- 0 => 'wcf.acp.cronjob.log.error',
- ]))
- ->renderer([
- new class extends DefaultColumnRenderer {
- public function render(mixed $value, mixed $context = null): string
- {
- \assert($context instanceof CronjobLog);
-
- if ($context->success) {
- return '<span class="badge green">' . WCF::getLanguage()->get('wcf.acp.cronjob.log.success') . '</span>';
- }
- if ($context->error) {
- $label = WCF::getLanguage()->get('wcf.acp.cronjob.log.error');
- $buttonId = 'cronjobLogErrorButton' . $context->cronjobLogID;
- $id = 'cronjobLogError' . $context->cronjobLogID;
- $error = StringUtil::encodeHTML($context->error);
- $dialogTitle = StringUtil::encodeJS(WCF::getLanguage()->get('wcf.acp.cronjob.log.error.details'));
-
- return <<<HTML
- <button type="button" id="{$buttonId}" class="badge red">
- {$label}
- </button>
- <template id="{$id}">{$error}</template>
- <script data-relocate="true">
- require(['WoltLabSuite/Core/Component/Dialog'], ({ dialogFactory }) => {
- document.getElementById('{$buttonId}').addEventListener('click', () => {
- const dialog = dialogFactory().fromId('{$id}').withoutControls();
- dialog.show('{$dialogTitle}');
- });
- });
- </script>
- HTML;
- }
-
- return '';
- }
- },
- ]),
- ]);
-
- $this->setSortField('execTime');
- $this->setSortOrder('DESC');
- }
-
- #[\Override]
- public function isAccessible(): bool
- {
- return WCF::getSession()->getPermission('admin.management.canManageCronjob');
- }
-
- #[\Override]
- protected function createObjectList(): DatabaseObjectList
- {
- return new CronjobLogList();
- }
-
- #[\Override]
- protected function getInitializedEvent(): ?IPsr14Event
- {
- return new CronjobLogGridViewInitialized($this);
- }
-
- private function getAvailableCronjobs(): array
- {
- $list = new I18nCronjobList();
- $list->sqlOrderBy = 'descriptionI18n';
- $list->readObjects();
-
- return \array_map(fn(Cronjob $cronjob) => $cronjob->getDescription(), $list->getObjects());
- }
-}
+++ /dev/null
-<?php
-
-namespace wcf\system\view\grid;
-
-use LogicException;
-
-abstract class DataSourceGridView extends AbstractGridView
-{
- protected array $dataSource;
-
- public function getRows(): array
- {
- $this->sortRows();
-
- return $this->getRowsForPage();
- }
-
- protected function sortRows(): void
- {
- $this->getDataSource();
-
- \uasort($this->dataSource, 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->getDataSource(), ($this->getPageNo() - 1) * $this->getRowsPerPage(), $this->getRowsPerPage());
- }
-
- public function countRows(): int
- {
- return \count($this->getDataSource());
- }
-
- protected function getDataSource(): array
- {
- if (!isset($this->dataSource)) {
- $this->dataSource = $this->loadDataSource();
- $this->applyFilters();
- $this->fireInitializedEvent();
- }
-
- return $this->dataSource;
- }
-
- protected function applyFilters(): void
- {
- foreach ($this->getActiveFilters() as $key => $value) {
- $column = $this->getColumn($key);
- if (!$column) {
- throw new LogicException("Unknown column '" . $key . "'");
- }
-
- $this->dataSource = \array_filter($this->dataSource, function (array $row) use ($column, $value) {
- return $column->getFilter()->matches($value, $row[$column->getID()]);
- });
- }
- }
-
- protected abstract function loadDataSource(): array;
-}
+++ /dev/null
-<?php
-
-namespace wcf\system\view\grid;
-
-use LogicException;
-use wcf\data\DatabaseObject;
-use wcf\data\DatabaseObjectList;
-
-abstract class DatabaseObjectListGridView extends AbstractGridView
-{
- protected DatabaseObjectList $objectList;
- private int $objectCount;
-
- public function getRows(): array
- {
- $this->getObjectList()->readObjects();
-
- return $this->getObjectList()->getObjects();
- }
-
- public function countRows(): int
- {
- if (!isset($this->objectCount)) {
- $this->objectCount = $this->getObjectList()->countObjects();
- }
-
- return $this->objectCount;
- }
-
- protected function getData(mixed $row, string $identifer): mixed
- {
- \assert($row instanceof DatabaseObject);
-
- return $row->__get($identifer);
- }
-
- protected function initObjectList(): void
- {
- $this->objectList = $this->createObjectList();
- $this->objectList->sqlLimit = $this->getRowsPerPage();
- $this->objectList->sqlOffset = ($this->getPageNo() - 1) * $this->getRowsPerPage();
- if ($this->getSortField()) {
- $column = $this->getColumn($this->getSortField());
- if ($column && $column->getSortById()) {
- $this->objectList->sqlOrderBy = $column->getSortById() . ' ' . $this->getSortOrder();
- } else {
- $this->objectList->sqlOrderBy = $this->getSortField() . ' ' . $this->getSortOrder();
- }
- }
- $this->applyFilters();
- $this->fireInitializedEvent();
- }
-
- public function getObjectList(): DatabaseObjectList
- {
- if (!isset($this->objectList)) {
- $this->initObjectList();
- }
-
- return $this->objectList;
- }
-
- protected function applyFilters(): void
- {
- foreach ($this->getActiveFilters() as $key => $value) {
- $column = $this->getColumn($key);
- if (!$column) {
- throw new LogicException("Unknown column '" . $key . "'");
- }
-
- $column->getFilter()->applyFilter($this->getObjectList(), $column->getID(), $value);
- }
- }
-
- #[\Override]
- public function getObjectID(mixed $row): mixed
- {
- \assert($row instanceof DatabaseObject);
-
- return $row->getObjectID();
- }
-
- protected abstract function createObjectList(): DatabaseObjectList;
-}
+++ /dev/null
-<?php
-
-namespace wcf\system\view\grid;
-
-use wcf\event\gridView\ExceptionLogGridViewInitialized;
-use wcf\event\IPsr14Event;
-use wcf\system\Regex;
-use wcf\system\view\grid\filter\SelectFilter;
-use wcf\system\view\grid\filter\TextFilter;
-use wcf\system\view\grid\renderer\TimeColumnRenderer;
-use wcf\system\view\grid\renderer\TitleColumnRenderer;
-use wcf\system\WCF;
-use wcf\util\DirectoryUtil;
-use wcf\util\ExceptionLogUtil;
-
-final class ExceptionLogGridView extends DataSourceGridView
-{
- private array $availableLogFiles;
-
- public function __construct(bool $applyDefaultFilter = false)
- {
- $this->addColumns([
- GridViewColumn::for('message')
- ->label('wcf.acp.exceptionLog.exception.message')
- ->sortable()
- ->renderer(new TitleColumnRenderer()),
- GridViewColumn::for('exceptionID')
- ->label('wcf.acp.exceptionLog.search.exceptionID')
- ->filter(new TextFilter())
- ->sortable(),
- GridViewColumn::for('date')
- ->label('wcf.acp.exceptionLog.exception.date')
- ->sortable()
- ->renderer(new TimeColumnRenderer()),
- GridViewColumn::for('logFile')
- ->label('wcf.acp.exceptionLog.search.logFile')
- ->filter(new SelectFilter($this->getAvailableLogFiles()))
- ->hidden(true),
- ]);
-
- $this->addRowLink(new GridViewRowLink(cssClass: 'jsExceptionLogEntry'));
- $this->setSortField('date');
- $this->setSortOrder('DESC');
-
- if ($applyDefaultFilter && $this->getDefaultLogFile() !== null) {
- $this->setActiveFilters([
- 'logFile' => $this->getDefaultLogFile(),
- ]);
- }
- }
-
- #[\Override]
- public function isAccessible(): bool
- {
- return WCF::getSession()->getPermission('admin.management.canViewLog');
- }
-
- #[\Override]
- public function getObjectID(mixed $row): mixed
- {
- return $row['exceptionID'];
- }
-
- #[\Override]
- protected function loadDataSource(): array
- {
- if (!empty($this->getActiveFilters()['exceptionID'])) {
- $exceptionID = $this->getActiveFilters()['exceptionID'];
- $contents = $logFile = '';
- foreach ($this->getAvailableLogFiles() as $logFile) {
- $contents = \file_get_contents(WCF_DIR . $logFile);
-
- if (\str_contains($contents, '<<<<<<<<' . $exceptionID . '<<<<')) {
- break;
- }
-
- unset($contents);
- }
-
- if ($contents === '') {
- return [];
- }
-
- $exceptions = ExceptionLogUtil::splitLog($contents);
- $parsedExceptions = [];
-
- foreach ($exceptions as $key => $val) {
- if ($key !== $exceptionID) {
- continue;
- }
-
- $parsed = ExceptionLogUtil::parseException($val);
-
- $parsedExceptions[$key] = [
- 'exceptionID' => $key,
- 'message' => $parsed['message'],
- 'date' => $parsed['date'],
- 'logFile' => $logFile,
- ];
- }
-
- return $parsedExceptions;
- } elseif (!empty($this->getActiveFilters()['logFile'])) {
- $contents = \file_get_contents(WCF_DIR . $this->getActiveFilters()['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'],
- 'logFile' => $this->getActiveFilters()['logFile'],
- ];
- }
-
- return $parsedExceptions;
- }
-
- return [];
- }
-
- #[\Override]
- protected function applyFilters(): void
- {
- // Overwrite the default filtering, as this is already applied when the data is loaded.
- }
-
- private function getAvailableLogFiles(): array
- {
- if (!isset($this->availableLogFiles)) {
- $this->availableLogFiles = [];
- $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) {
- $this->availableLogFiles['log/' . $logFile] = 'log/' . $logFile;
- }
- }
-
- return $this->availableLogFiles;
- }
-
- private function getDefaultLogFile(): ?string
- {
- return \array_key_first($this->getAvailableLogFiles());
- }
-
- #[\Override]
- protected function getInitializedEvent(): ?IPsr14Event
- {
- return new ExceptionLogGridViewInitialized($this);
- }
-}
+++ /dev/null
-<?php
-
-namespace wcf\system\view\grid;
-
-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
-{
- /**
- * @var IColumnRenderer[]
- */
- private array $renderer = [];
- private string $label = '';
- private static DefaultColumnRenderer $defaultRenderer;
- private bool $sortable = false;
- private string $sortById = '';
- private ?IGridViewFilter $filter = null;
- private bool $hidden = false;
-
- private function __construct(private readonly string $id) {}
-
- public static function for(string $id): static
- {
- return new static($id);
- }
-
- public function render(mixed $value, mixed $context = null): string
- {
- if ($this->getRenderers() === []) {
- return self::getDefaultRenderer()->render($value, $context);
- }
-
- foreach ($this->getRenderers() as $renderer) {
- $value = $renderer->render($value, $context);
- }
-
- return $value;
- }
-
- public function getClasses(): string
- {
- if ($this->getRenderers() === []) {
- return self::getDefaultRenderer()->getClasses();
- }
-
- return \implode(' ', \array_map(
- static function (IColumnRenderer $renderer) {
- return $renderer->getClasses();
- },
- $this->getRenderers()
- ));
- }
-
- public function renderer(array|IColumnRenderer $renderers): static
- {
- if (!\is_array($renderers)) {
- $renderers = [$renderers];
- }
-
- foreach ($renderers as $renderer) {
- \assert($renderer instanceof IColumnRenderer);
- $this->renderer[] = $renderer;
- }
-
- return $this;
- }
-
- public function label(string $languageItem): static
- {
- $this->label = WCF::getLanguage()->get($languageItem);
-
- return $this;
- }
-
- public function sortable(bool $sortable = true): static
- {
- $this->sortable = $sortable;
-
- return $this;
- }
-
- public function sortById(string $id): static
- {
- $this->sortById = $id;
-
- return $this;
- }
-
- /**
- * @return IColumnRenderer[]
- */
- public function getRenderers(): array
- {
- return $this->renderer;
- }
-
- public function getID(): string
- {
- return $this->id;
- }
-
- public function getLabel(): string
- {
- return $this->label;
- }
-
- public function isSortable(): bool
- {
- return $this->sortable;
- }
-
- public function getSortById(): string
- {
- return $this->sortById;
- }
-
- public function filter(?IGridViewFilter $filter): static
- {
- $this->filter = $filter;
-
- return $this;
- }
-
- public function getFilter(): ?IGridViewFilter
- {
- return $this->filter;
- }
-
- public function getFilterFormField(): AbstractFormField
- {
- if ($this->getFilter() === null) {
- throw new \LogicException('This column has no filter.');
- }
-
- return $this->getFilter()->getFormField($this->getID(), $this->getLabel());
- }
-
- private static function getDefaultRenderer(): DefaultColumnRenderer
- {
- if (!isset(self::$defaultRenderer)) {
- self::$defaultRenderer = new DefaultColumnRenderer();
- }
-
- return self::$defaultRenderer;
- }
-
- public function isTitleColumn(): bool
- {
- foreach ($this->getRenderers() as $renderer) {
- if ($renderer instanceof TitleColumnRenderer) {
- return true;
- }
- }
-
- return false;
- }
-
- public function hidden(bool $hidden = true): static
- {
- $this->hidden = $hidden;
-
- return $this;
- }
-
- public function isHidden(): bool
- {
- return $this->hidden;
- }
-}
+++ /dev/null
-<?php
-
-namespace wcf\system\view\grid;
-
-use wcf\data\DatabaseObject;
-use wcf\system\request\LinkHandler;
-use wcf\util\StringUtil;
-
-class GridViewRowLink
-{
- public function __construct(
- private readonly string $controllerClass = '',
- private readonly array $parameters = [],
- private readonly string $cssClass = ''
- ) {}
-
- public function render(mixed $value, mixed $context = null, bool $isPrimaryColumn = false): string
- {
- $href = '';
- if ($this->controllerClass) {
- \assert($context instanceof DatabaseObject);
- $href = LinkHandler::getInstance()->getControllerLink(
- $this->controllerClass,
- \array_merge($this->parameters, ['object' => $context])
- );
- }
-
- $attributes = [];
- $isButton = true;
- if ($href) {
- $attributes[] = 'href="' . $href . '"';
- $isButton = false;
- }
- $attributes[] = 'class="gridView__rowLink ' . StringUtil::encodeHTML($this->cssClass) . '"';
- $attributes[] = 'tabindex="' . ($isPrimaryColumn ? '0' : '-1') . '"';
-
- if ($isButton) {
- return '<button type="button" ' . implode(' ', $attributes) . '>'
- . $value
- . '</button>';
- } else {
- return '<a ' . implode(' ', $attributes) . '>'
- . $value
- . '</a>';
- }
- }
-}
+++ /dev/null
-<?php
-
-namespace wcf\system\view\grid;
-
-use wcf\acp\form\UserOptionEditForm;
-use wcf\data\DatabaseObjectList;
-use wcf\data\user\option\UserOption;
-use wcf\data\user\option\UserOptionList;
-use wcf\event\gridView\UserOptionGridViewInitialized;
-use wcf\event\IPsr14Event;
-use wcf\system\view\grid\action\DeleteAction;
-use wcf\system\view\grid\action\EditAction;
-use wcf\system\view\grid\action\ToggleAction;
-use wcf\system\view\grid\renderer\DefaultColumnRenderer;
-use wcf\system\view\grid\renderer\NumberColumnRenderer;
-use wcf\system\view\grid\renderer\TitleColumnRenderer;
-use wcf\system\WCF;
-use wcf\util\StringUtil;
-
-final class UserOptionGridView extends DatabaseObjectListGridView
-{
- public function __construct()
- {
- $this->addColumns([
- GridViewColumn::for('optionID')
- ->label('wcf.global.objectID')
- ->renderer(new NumberColumnRenderer())
- ->sortable(),
- GridViewColumn::for('optionName')
- ->label('wcf.global.name')
- ->sortable()
- ->renderer([
- new class extends TitleColumnRenderer {
- public function render(mixed $value, mixed $context = null): string
- {
- \assert($context instanceof UserOption);
-
- return StringUtil::encodeHTML($context->getTitle());
- }
- }
- ]),
- GridViewColumn::for('categoryName')
- ->label('wcf.global.category')
- ->sortable()
- ->renderer([
- new class extends DefaultColumnRenderer {
- public function render(mixed $value, mixed $context = null): string
- {
- \assert($context instanceof UserOption);
-
- return StringUtil::encodeHTML(
- WCF::getLanguage()->get('wcf.user.option.category.' . $context->categoryName)
- );
- }
- }
- ]),
- GridViewColumn::for('optionType')
- ->label('wcf.acp.user.option.optionType')
- ->sortable(),
- GridViewColumn::for('showOrder')
- ->label('wcf.global.showOrder')
- ->sortable()
- ->renderer(new NumberColumnRenderer()),
- ]);
-
- $this->addActions([
- new ToggleAction('core/users/options/%s/enable', 'core/users/options/%s/disable'),
- new EditAction(UserOptionEditForm::class),
- new DeleteAction('core/users/options/%s', fn(UserOption $row) => $row->canDelete()),
- ]);
- $this->addRowLink(new GridViewRowLink(UserOptionEditForm::class));
- $this->setSortField('showOrder');
- }
-
- #[\Override]
- public function isAccessible(): bool
- {
- return WCF::getSession()->getPermission('admin.user.canManageUserOption');
- }
-
- #[\Override]
- protected function createObjectList(): DatabaseObjectList
- {
- $list = new UserOptionList();
- $list->getConditionBuilder()->add(
- "option_table.categoryName IN (
- SELECT categoryName
- FROM wcf" . WCF_N . "_user_option_category
- WHERE parentCategoryName = ?
- )",
- ['profile']
- );
-
- return $list;
- }
-
- #[\Override]
- protected function getInitializedEvent(): ?IPsr14Event
- {
- return new UserOptionGridViewInitialized($this);
- }
-}
+++ /dev/null
-<?php
-
-namespace wcf\system\view\grid;
-
-use wcf\acp\form\UserRankEditForm;
-use wcf\data\DatabaseObjectList;
-use wcf\data\user\group\UserGroup;
-use wcf\data\user\rank\I18nUserRankList;
-use wcf\data\user\rank\UserRank;
-use wcf\event\gridView\UserRankGridViewInitialized;
-use wcf\event\IPsr14Event;
-use wcf\system\view\grid\action\DeleteAction;
-use wcf\system\view\grid\action\EditAction;
-use wcf\system\view\grid\filter\I18nTextFilter;
-use wcf\system\view\grid\filter\SelectFilter;
-use wcf\system\view\grid\renderer\DefaultColumnRenderer;
-use wcf\system\view\grid\renderer\NumberColumnRenderer;
-use wcf\system\view\grid\renderer\TitleColumnRenderer;
-use wcf\system\WCF;
-use wcf\util\StringUtil;
-
-final class UserRankGridView extends DatabaseObjectListGridView
-{
- public function __construct()
- {
- $this->addColumns([
- GridViewColumn::for('rankID')
- ->label('wcf.global.objectID')
- ->renderer(new NumberColumnRenderer())
- ->sortable(),
- GridViewColumn::for('rankTitle')
- ->label('wcf.acp.user.rank.title')
- ->sortable()
- ->sortById('rankTitleI18n')
- ->filter(new I18nTextFilter())
- ->renderer([
- new class extends TitleColumnRenderer {
- public function render(mixed $value, mixed $context = null): string
- {
- \assert($context instanceof UserRank);
-
- return '<span class="badge label' . ($context->cssClassName ? ' ' . $context->cssClassName : '') . '">'
- . StringUtil::encodeHTML($context->getTitle())
- . '<span>';
- }
- }
- ]),
- GridViewColumn::for('rankImage')
- ->label('wcf.acp.user.rank.image')
- ->sortable()
- ->renderer([
- new class extends DefaultColumnRenderer {
- public function render(mixed $value, mixed $context = null): string
- {
- \assert($context instanceof UserRank);
-
- return $context->rankImage ? $context->getImage() : '';
- }
- },
- ]),
- GridViewColumn::for('groupID')
- ->label('wcf.user.group')
- ->sortable()
- ->filter(new SelectFilter($this->getAvailableUserGroups()))
- ->renderer([
- new class extends DefaultColumnRenderer {
- public function render(mixed $value, mixed $context = null): string
- {
- return StringUtil::encodeHTML(UserGroup::getGroupByID($value)->getName());
- }
- },
- ]),
- GridViewColumn::for('requiredGender')
- ->label('wcf.user.option.gender')
- ->sortable()
- ->renderer([
- new class extends DefaultColumnRenderer {
- public function render(mixed $value, mixed $context = null): string
- {
- if (!$value) {
- return '';
- }
-
- return WCF::getLanguage()->get(match ($value) {
- 1 => 'wcf.user.gender.male',
- 2 => 'wcf.user.gender.female',
- default => 'wcf.user.gender.other'
- });
- }
- },
- ]),
- GridViewColumn::for('requiredPoints')
- ->label('wcf.acp.user.rank.requiredPoints')
- ->sortable()
- ->renderer(new NumberColumnRenderer()),
- ]);
-
- $this->addActions([
- new EditAction(UserRankEditForm::class),
- new DeleteAction('core/users/ranks/%s'),
- ]);
- $this->addRowLink(new GridViewRowLink(UserRankEditForm::class));
- $this->setSortField('rankTitle');
- }
-
- #[\Override]
- public function isAccessible(): bool
- {
- return \MODULE_USER_RANK && WCF::getSession()->getPermission('admin.user.rank.canManageRank');
- }
-
- #[\Override]
- protected function createObjectList(): DatabaseObjectList
- {
- return new I18nUserRankList();
- }
-
- #[\Override]
- protected function getInitializedEvent(): ?IPsr14Event
- {
- return new UserRankGridViewInitialized($this);
- }
-
- private function getAvailableUserGroups(): array
- {
- $groups = [];
- foreach (UserGroup::getSortedGroupsByType([], [UserGroup::GUESTS, UserGroup::EVERYONE]) as $group) {
- $groups[$group->groupID] = $group->getName();
- }
-
- return $groups;
- }
-}
+++ /dev/null
-<?php
-
-namespace wcf\system\view\grid\action;
-
-use Closure;
-
-abstract class AbstractAction implements IGridViewAction
-{
- public function __construct(
- private readonly ?Closure $isAvailableCallback = null
- ) {}
-
- #[\Override]
- public function isAvailable(mixed $row): bool
- {
- if ($this->isAvailableCallback === null) {
- return true;
- }
-
- return ($this->isAvailableCallback)($row);
- }
-
- #[\Override]
- public function isQuickAction(): bool
- {
- return false;
- }
-}
+++ /dev/null
-<?php
-
-namespace wcf\system\view\grid\action;
-
-use Closure;
-use wcf\action\ApiAction;
-use wcf\data\DatabaseObject;
-use wcf\data\ITitledObject;
-use wcf\system\request\LinkHandler;
-use wcf\system\view\grid\AbstractGridView;
-use wcf\system\WCF;
-use wcf\util\StringUtil;
-
-class DeleteAction extends AbstractAction
-{
- public function __construct(
- private readonly string $endpoint,
- ?Closure $isAvailableCallback = null
- ) {
- parent::__construct($isAvailableCallback);
- }
-
- #[\Override]
- public function render(mixed $row): string
- {
- \assert($row instanceof DatabaseObject);
-
- $label = WCF::getLanguage()->get('wcf.global.button.delete');
-
- if (!$this->isAvailable($row)) {
- return <<<HTML
- <span>
- {$label}
- </span>
- HTML;
- }
-
- $endpoint = StringUtil::encodeHTML(
- LinkHandler::getInstance()->getControllerLink(ApiAction::class, ['id' => 'rpc']) .
- \sprintf($this->endpoint, $row->getObjectID())
- );
- if ($row instanceof ITitledObject) {
- $objectName = StringUtil::encodeHTML($row->getTitle());
- } else {
- $objectName = '';
- }
-
- return <<<HTML
- <button type="button" data-action="delete" data-object-name="{$objectName}" data-endpoint="{$endpoint}">
- {$label}
- </button>
- HTML;
- }
-
- #[\Override]
- public function renderInitialization(AbstractGridView $gridView): ?string
- {
- $id = StringUtil::encodeJS($gridView->getID());
-
- return <<<HTML
- <script data-relocate="true">
- require(['WoltLabSuite/Core/Component/GridView/Action/Delete'], ({ setup }) => {
- setup(document.getElementById('{$id}_table'));
- });
- </script>
- HTML;
- }
-}
+++ /dev/null
-<?php
-
-namespace wcf\system\view\grid\action;
-
-use Closure;
-use wcf\data\DatabaseObject;
-use wcf\system\request\LinkHandler;
-use wcf\system\view\grid\AbstractGridView;
-use wcf\system\WCF;
-
-class EditAction extends AbstractAction
-{
- public function __construct(
- private readonly string $controllerClass,
- ?Closure $isAvailableCallback = null
- ) {
- parent::__construct($isAvailableCallback);
- }
-
- #[\Override]
- public function render(mixed $row): string
- {
- \assert($row instanceof DatabaseObject);
- $href = LinkHandler::getInstance()->getControllerLink(
- $this->controllerClass,
- ['object' => $row]
- );
-
- return '<a href="' . $href . '">' . WCF::getLanguage()->get('wcf.global.button.edit') . '</a>';
- }
-
- #[\Override]
- public function renderInitialization(AbstractGridView $gridView): ?string
- {
- return null;
- }
-}
+++ /dev/null
-<?php
-
-namespace wcf\system\view\grid\action;
-
-use wcf\system\view\grid\AbstractGridView;
-
-interface IGridViewAction
-{
- public function render(mixed $row): string;
-
- public function renderInitialization(AbstractGridView $gridView): ?string;
-
- public function isQuickAction(): bool;
-
- public function isAvailable(mixed $row): bool;
-}
+++ /dev/null
-<?php
-
-namespace wcf\system\view\grid\action;
-
-use Closure;
-use wcf\action\ApiAction;
-use wcf\data\DatabaseObject;
-use wcf\system\request\LinkHandler;
-use wcf\system\view\grid\AbstractGridView;
-use wcf\system\WCF;
-use wcf\util\StringUtil;
-
-class ToggleAction extends AbstractAction
-{
- public function __construct(
- private readonly string $enableEndpoint,
- private readonly string $disableEndpoint,
- private readonly string $propertyName = 'isDisabled',
- ?Closure $isAvailableCallback = null
- ) {
- parent::__construct($isAvailableCallback);
- }
-
- #[\Override]
- public function render(mixed $row): string
- {
- \assert($row instanceof DatabaseObject);
-
- $enableEndpoint = StringUtil::encodeHTML(
- LinkHandler::getInstance()->getControllerLink(ApiAction::class, ['id' => 'rpc']) .
- \sprintf($this->enableEndpoint, $row->getObjectID())
- );
- $disableEndpoint = StringUtil::encodeHTML(
- LinkHandler::getInstance()->getControllerLink(ApiAction::class, ['id' => 'rpc']) .
- \sprintf($this->disableEndpoint, $row->getObjectID())
- );
-
- $ariaLabel = WCF::getLanguage()->get('wcf.global.button.enable');
- $checked = !$row->{$this->propertyName} ? 'checked' : '';
-
- return <<<HTML
- <woltlab-core-toggle-button aria-label="{$ariaLabel}" data-enable-endpoint="{$enableEndpoint}" data-disable-endpoint="{$disableEndpoint}" {$checked}></woltlab-core-toggle-button>
- HTML;
- }
-
- #[\Override]
- public function renderInitialization(AbstractGridView $gridView): ?string
- {
- $id = StringUtil::encodeJS($gridView->getID());
-
- return <<<HTML
- <script data-relocate="true">
- require(['WoltLabSuite/Core/Component/GridView/Action/Toggle'], ({ setup }) => {
- setup('{$id}_table');
- });
- </script>
- HTML;
- }
-
- #[\Override]
- public function isQuickAction(): bool
- {
- return true;
- }
-}
+++ /dev/null
-<?php
-
-namespace wcf\system\view\grid\filter;
-
-use wcf\data\DatabaseObjectList;
-use wcf\system\WCF;
-
-class I18nTextFilter extends TextFilter
-{
- #[\Override]
- public function applyFilter(DatabaseObjectList $list, string $id, string $value): void
- {
- $list->getConditionBuilder()->add("($id LIKE ? OR $id IN (SELECT languageItem FROM wcf1_language_item WHERE languageID = ? AND languageItemValue LIKE ?))", [
- '%' . $value . '%',
- WCF::getLanguage()->languageID,
- '%' . $value . '%'
- ]);
- }
-}
+++ /dev/null
-<?php
-
-namespace wcf\system\view\grid\filter;
-
-use wcf\data\DatabaseObjectList;
-use wcf\system\form\builder\field\AbstractFormField;
-
-interface IGridViewFilter
-{
- public function getFormField(string $id, string $label): AbstractFormField;
-
- public function applyFilter(DatabaseObjectList $list, string $id, string $value): void;
-
- public function matches(string $filterValue, string $rowValue): bool;
-
- public function renderValue(string $value): string;
-}
+++ /dev/null
-<?php
-
-namespace wcf\system\view\grid\filter;
-
-use wcf\data\DatabaseObjectList;
-use wcf\system\form\builder\field\AbstractFormField;
-use wcf\system\form\builder\field\SelectFormField;
-use wcf\system\WCF;
-
-class SelectFilter implements IGridViewFilter
-{
- public function __construct(private readonly array $options) {}
-
- #[\Override]
- public function getFormField(string $id, string $label): AbstractFormField
- {
- return SelectFormField::create($id)
- ->label($label)
- ->options($this->options);
- }
-
- #[\Override]
- public function applyFilter(DatabaseObjectList $list, string $id, string $value): void
- {
- $list->getConditionBuilder()->add("$id = ?", [$value]);
- }
-
- #[\Override]
- public function matches(string $filterValue, string $rowValue): bool
- {
- return $filterValue === $rowValue;
- }
-
- #[\Override]
- public function renderValue(string $value): string
- {
- return WCF::getLanguage()->get($this->options[$value]);
- }
-}
+++ /dev/null
-<?php
-
-namespace wcf\system\view\grid\filter;
-
-use wcf\data\DatabaseObjectList;
-use wcf\system\form\builder\field\AbstractFormField;
-use wcf\system\form\builder\field\TextFormField;
-
-class TextFilter implements IGridViewFilter
-{
- #[\Override]
- public function getFormField(string $id, string $label): AbstractFormField
- {
- return TextFormField::create($id)
- ->label($label);
- }
-
- #[\Override]
- public function applyFilter(DatabaseObjectList $list, string $id, string $value): void
- {
- $list->getConditionBuilder()->add("$id LIKE ?", ['%' . $value . '%']);
- }
-
- #[\Override]
- public function matches(string $filterValue, string $rowValue): bool
- {
- return \str_contains(\mb_strtolower($rowValue), \mb_strtolower($filterValue));
- }
-
- #[\Override]
- public function renderValue(string $value): string
- {
- return $value;
- }
-}
+++ /dev/null
-<?php
-
-namespace wcf\system\view\grid\filter;
-
-use wcf\data\DatabaseObjectList;
-use wcf\system\form\builder\field\AbstractFormField;
-use wcf\system\form\builder\field\DateRangeFormField;
-use wcf\system\WCF;
-
-class TimeFilter implements IGridViewFilter
-{
- #[\Override]
- public function getFormField(string $id, string $label): AbstractFormField
- {
- return DateRangeFormField::create($id)
- ->label($label)
- ->supportTime();
- }
-
- #[\Override]
- public function applyFilter(DatabaseObjectList $list, string $id, string $value): void
- {
- $timestamps = $this->getTimestamps($value);
-
- if (!$timestamps['from'] && !$timestamps['to']) {
- return;
- }
-
- if (!$timestamps['to']) {
- $list->getConditionBuilder()->add("$id >= ?", [$timestamps['from']]);
- } else {
- $list->getConditionBuilder()->add("$id BETWEEN ? AND ?", [$timestamps['from'], $timestamps['to']]);
- }
- }
-
- #[\Override]
- public function matches(string $filterValue, string $rowValue): bool
- {
- $timestamps = $this->getTimestamps($filterValue);
-
- if (!$timestamps['from'] && !$timestamps['to']) {
- return true;
- }
-
- if (!$timestamps['to']) {
- return $rowValue >= $timestamps['from'];
- } else {
- return $rowValue >= $timestamps['from'] && $rowValue <= $timestamps['to'];
- }
- }
-
- #[\Override]
- public function renderValue(string $value): string
- {
- $values = explode(';', $value);
- if (\count($values) !== 2) {
- return '';
- }
-
- $locale = WCF::getLanguage()->getLocale();;
- $fromString = $toString = '';
- if ($values[0] !== '') {
- $fromDateTime = \DateTime::createFromFormat(
- 'Y-m-d\TH:i:sP',
- $values[0],
- WCF::getUser()->getTimeZone()
- );
- if ($fromDateTime !== false) {
- $fromString = \IntlDateFormatter::formatObject(
- $fromDateTime,
- [
- \IntlDateFormatter::LONG,
- \IntlDateFormatter::SHORT,
- ],
- $locale
- );
- }
- }
- if ($values[1] !== '') {
- $toDateTime = \DateTime::createFromFormat(
- 'Y-m-d\TH:i:sP',
- $values[1],
- WCF::getUser()->getTimeZone()
- );
- if ($toDateTime !== false) {
- $toString = \IntlDateFormatter::formatObject(
- $toDateTime,
- [
- \IntlDateFormatter::LONG,
- \IntlDateFormatter::SHORT,
- ],
- $locale
- );
- }
- }
-
- if ($fromString && $toString) {
- return $fromString . ' ‐ ' . $toString;
- } else if ($fromString) {
- return '>= ' . $fromString;
- } else if ($toString) {
- return '<= ' . $toString;
- }
-
- return '';
- }
-
- private function getTimestamps(string $value): array
- {
- $from = 0;
- $to = 0;
-
- $values = explode(';', $value);
- if (\count($values) === 2) {
- $from = $this->getTimestamp($values[0]);
- $to = $this->getTimestamp($values[1]);
- }
-
- return [
- 'from' => $from,
- 'to' => $to,
- ];
- }
-
- private function getTimestamp(string $date): int
- {
- $dateTime = \DateTime::createFromFormat(
- 'Y-m-d\TH:i:sP',
- $date
- );
-
- if ($dateTime !== false) {
- return $dateTime->getTimestamp();
- }
-
- return 0;
- }
-}
+++ /dev/null
-<?php
-
-namespace wcf\system\view\grid\renderer;
-
-abstract class AbstractColumnRenderer implements IColumnRenderer
-{
- public function getClasses(): string
- {
- return '';
- }
-}
+++ /dev/null
-<?php
-
-namespace wcf\system\view\grid\renderer;
-
-use wcf\util\StringUtil;
-
-class DefaultColumnRenderer extends AbstractColumnRenderer
-{
- public function render(mixed $value, mixed $context = null): string
- {
- return StringUtil::encodeHTML($value);
- }
-
- public function getClasses(): string
- {
- return 'gridView__column--text';
- }
-}
+++ /dev/null
-<?php
-
-namespace wcf\system\view\grid\renderer;
-
-interface IColumnRenderer
-{
- public function render(mixed $value, mixed $context = null): string;
-
- public function getClasses(): string;
-}
+++ /dev/null
-<?php
-
-namespace wcf\system\view\grid\renderer;
-
-use wcf\data\DatabaseObject;
-use wcf\system\request\LinkHandler;
-use wcf\system\WCF;
-
-class LinkColumnRenderer extends AbstractColumnRenderer
-{
- public function __construct(
- private readonly string $controllerClass,
- private readonly array $parameters = [],
- private readonly string $titleLanguageItem = ''
- ) {}
-
- public function render(mixed $value, mixed $context = null): string
- {
- \assert($context instanceof DatabaseObject);
- $href = LinkHandler::getInstance()->getControllerLink(
- $this->controllerClass,
- \array_merge($this->parameters, ['object' => $context])
- );
-
- return '<a href="' . $href . '"'
- . ($this->titleLanguageItem ? ' title="' . WCF::getLanguage()->get($this->titleLanguageItem) . '"' : '') . '>'
- . $value
- . '</a>';
- }
-}
+++ /dev/null
-<?php
-
-namespace wcf\system\view\grid\renderer;
-
-use wcf\util\StringUtil;
-
-class NumberColumnRenderer extends AbstractColumnRenderer
-{
- public function render(mixed $value, mixed $context = null): string
- {
- return StringUtil::formatNumeric($value);
- }
-
- public function getClasses(): string
- {
- return 'gridView__column--digits';
- }
-}
+++ /dev/null
-<?php
-
-namespace wcf\system\view\grid\renderer;
-
-use wcf\system\WCF;
-use wcf\util\StringUtil;
-
-class PhraseColumnRenderer extends DefaultColumnRenderer
-{
- public function render(mixed $value, mixed $context = null): string
- {
- return StringUtil::encodeHTML(WCF::getLanguage()->get($value));
- }
-}
+++ /dev/null
-<?php
-
-namespace wcf\system\view\grid\renderer;
-
-use wcf\system\WCF;
-
-class TimeColumnRenderer extends AbstractColumnRenderer
-{
- public function render(mixed $value, mixed $context = null): string
- {
- $timestamp = \intval($value);
- if (!$timestamp) {
- return '';
- }
-
- $dateTime = new \DateTimeImmutable('@' . $timestamp);
- $dateTime = $dateTime->setTimezone(WCF::getUser()->getTimeZone());
- $locale = WCF::getLanguage()->getLocale();
-
- $isFutureDate = $dateTime->getTimestamp() > TIME_NOW;
-
- $dateAndTime = \IntlDateFormatter::formatObject(
- $dateTime,
- [
- \IntlDateFormatter::LONG,
- \IntlDateFormatter::SHORT,
- ],
- $locale
- );
-
- return \sprintf(
- '<woltlab-core-date-time date="%s"%s>%s</woltlab-core-date-time>',
- $dateTime->format('c'),
- $isFutureDate ? ' static' : '',
- $dateAndTime
- );
- }
-
- public function getClasses(): string
- {
- return 'gridView__column--date';
- }
-}
+++ /dev/null
-<?php
-
-namespace wcf\system\view\grid\renderer;
-
-class TitleColumnRenderer extends DefaultColumnRenderer
-{
- public function getClasses(): string
- {
- return 'gridView__column--title';
- }
-}