TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:02 +02:00
commit b53992613d
22 changed files with 2485 additions and 0 deletions
+346
View File
@@ -0,0 +1,346 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Belog\Controller;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Belog\Domain\Model\Constraint;
use TYPO3\CMS\Belog\Domain\Model\LogEntry;
use TYPO3\CMS\Belog\Domain\Repository\LogEntryRepository;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Domain\DateTimeFormat;
use TYPO3\CMS\Core\Http\AllowedMethodsTrait;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
use TYPO3\CMS\Extbase\Property\TypeConverter\DateTimeConverter;
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
/**
* Show log entries from sys_log
*
* @internal This class is a TYPO3 Backend implementation and is not considered part of the Public TYPO3 API.
*/
class BackendLogController extends ActionController
{
use AllowedMethodsTrait;
public function __construct(
protected readonly ModuleTemplateFactory $moduleTemplateFactory,
protected readonly LogEntryRepository $logEntryRepository,
protected readonly ConnectionPool $connectionPool,
) {}
/**
* Initialize list action
*/
public function initializeListAction(): void
{
if (!isset($this->settings['dateFormat'])) {
$this->settings['dateFormat'] = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] ?: 'd-m-Y';
}
if (!isset($this->settings['timeFormat'])) {
$this->settings['timeFormat'] = $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'];
}
// Static format needed for date picker (flatpickr), see BackendController::generateJavascript() and #91606
$this->settings['dateTimeFormat'] = 'H:i d-m-Y';
$constraintConfiguration = $this->arguments->getArgument('constraint')->getPropertyMappingConfiguration();
$constraintConfiguration->allowAllProperties();
$constraintConfiguration->forProperty('manualDateStart')->setTypeConverterOption(DateTimeConverter::class, DateTimeConverter::CONFIGURATION_DATE_FORMAT, DateTimeFormat::ISO8601_LOCALTIME);
$constraintConfiguration->forProperty('manualDateStop')->setTypeConverterOption(DateTimeConverter::class, DateTimeConverter::CONFIGURATION_DATE_FORMAT, DateTimeFormat::ISO8601_LOCALTIME);
}
/**
* Show general information and the installed modules
*/
public function listAction(?Constraint $constraint = null, string $operation = ''): ResponseInterface
{
if ($operation === 'reset-filters') {
$constraint = new Constraint();
} elseif ($constraint === null) {
$constraint = $this->getConstraintFromBeUserData();
}
$access = true;
$pageId = $constraint->getPageId();
$permsClause = $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW);
if ($pageId === 0 || (BackendUtility::readPageAccess($pageId, $permsClause) ?: []) === []) {
if (!$this->getBackendUser()->isAdmin()) {
// User does not have access to selected site
$access = false;
}
if ($pageId === 0) {
// In case no page is selected, set depth to 0 to display only "global" logs
$constraint->setDepth(0);
}
}
$this->persistConstraintInBeUserData($constraint);
$this->resetConstraintsOnMemoryExhaustionError();
$this->setStartAndEndTimeFromTimeSelector($constraint);
$showWorkspaceSelector = $this->forceWorkspaceSelectionIfInWorkspace($constraint);
$viewVariables = [
'access' => $access,
'settings' => $this->settings,
'pageId' => $pageId,
'constraint' => $constraint,
'userGroups' => $this->createUserAndGroupListForSelectOptions(),
'selectableNumberOfLogEntries' => $this->createSelectableNumberOfLogEntriesOptions(),
'workspaces' => $this->createWorkspaceListForSelectOptions(),
'pageDepths' => $this->createPageDepthOptions(),
'channels' => $this->logEntryRepository->getUsedChannels(),
'channel' => $constraint->getChannel(),
'levels' => $this->logEntryRepository->getUsedLevels(),
'level' => $constraint->getLevel(),
'showWorkspaceSelector' => $showWorkspaceSelector,
];
if ($access) {
// Only fetch log entries if user has access
$logEntries = $this->logEntryRepository->findByConstraint($constraint);
$groupedLogEntries = $this->groupLogEntriesDay($logEntries);
$viewVariables['groupedLogEntries'] = $groupedLogEntries;
}
$view = $this->moduleTemplateFactory->create($this->request);
$view->getDocHeaderComponent()->setShortcutContext(
'system_log',
$this->getLanguageService()->translate('title', 'belog.module')
);
return $view->setFlashMessageQueue($this->getFlashMessageQueue())
->setTitle(LocalizationUtility::translate('title', 'belog.module'))
->assignMultiple($viewVariables)
->renderResponse('BackendLog/List');
}
public function initializeDeleteMessageAction(): void
{
$this->assertAllowedHttpMethod($this->request, 'POST');
}
/**
* Delete all log entries that share the same message with the log entry given
* in $errorUid
*/
public function deleteMessageAction(int $errorUid): ResponseInterface
{
$logEntry = $this->logEntryRepository->findByUid($errorUid);
if (!$logEntry) {
$this->addFlashMessage(LocalizationUtility::translate('actions.delete.noRowFound', 'belog') ?? '', '', ContextualFeedbackSeverity::WARNING);
return $this->redirect('list');
}
$numberOfDeletedRows = $this->logEntryRepository->deleteByMessageDetails($logEntry);
$this->addFlashMessage(sprintf(LocalizationUtility::translate('actions.delete.message', 'belog') ?? '', $numberOfDeletedRows));
BackendUtility::setUpdateSignal('updateSystemInformationMenu');
return $this->redirect('list');
}
/**
* Get module states (the constraint object) from user data
*/
protected function getConstraintFromBeUserData(): Constraint
{
$serializedConstraint = $this->request->getAttribute('moduleData')->get('constraint');
$constraint = null;
if (is_string($serializedConstraint) && !empty($serializedConstraint)) {
$constraint = @unserialize($serializedConstraint, ['allowed_classes' => [Constraint::class, \DateTime::class]]);
}
return $constraint ?: GeneralUtility::makeInstance(Constraint::class);
}
/**
* Save current constraint object in be user settings (uC)
*/
protected function persistConstraintInBeUserData(Constraint $constraint): void
{
$moduleData = $this->request->getAttribute('moduleData');
$moduleData->set('constraint', serialize($constraint));
$this->getBackendUser()->pushModuleData($moduleData->getModuleIdentifier(), $moduleData->toArray());
}
/**
* In case the script execution fails, because the user requested too many results
* (memory exhaustion in php), reset the constraints in be user settings, so
* the belog can be accessed again in the next call.
*/
protected function resetConstraintsOnMemoryExhaustionError(): void
{
$reservedMemory = new \SplFixedArray(187500); // 3M
register_shutdown_function(function () use (&$reservedMemory): void {
$reservedMemory = null; // free the reserved memory
$error = error_get_last();
if (str_contains($error['message'] ?? '', 'Allowed memory size of')) {
$constraint = GeneralUtility::makeInstance(Constraint::class);
$this->persistConstraintInBeUserData($constraint);
}
});
}
/**
* Create a sorted array for day from the query result of the sys log repository.
*
* pid is always -1 to render a flat list.
* '12345' is a sub array to split entries by day, number is first second of day
*
* [pid][dayTimestamp][items]
*
* @param array<LogEntry> $logEntries
*/
protected function groupLogEntriesDay(array $logEntries): array
{
$targetStructure = [];
foreach ($logEntries as $entry) {
$pid = -1;
// Create array if it is not defined yet
if (!is_array($targetStructure[$pid] ?? false)) {
$targetStructure[-1] = [];
}
// Get day timestamp of log entry and create sub array if needed
$timestampDay = strtotime($entry->getTstamp()->format('Y-m-d'));
if (!is_array($targetStructure[$pid][$timestampDay] ?? false)) {
$targetStructure[$pid][$timestampDay] = [];
}
// Add row
$targetStructure[$pid][$timestampDay][] = $entry;
}
ksort($targetStructure);
return $targetStructure;
}
/**
* Create options for the user / group drop down.
* This is not moved to a repository by intention to not mix up this 'meta' data
* with real repository work.
*/
protected function createUserAndGroupListForSelectOptions(): array
{
$items = [];
foreach (BackendUtility::getGroupNames() as $group) {
$items['groups']['gr-' . $group['uid']] = BackendUtility::getRecordTitle('be_groups', $group);
}
foreach (BackendUtility::getUserNames() as $user) {
$items['users']['us-' . $user['uid']] = BackendUtility::getRecordTitle('be_users', $user);
}
return $items;
}
/**
* Options for the "max" drop down
*/
protected function createSelectableNumberOfLogEntriesOptions(): array
{
return [
50 => 50,
100 => 100,
200 => 200,
500 => 500,
1000 => 1000,
1000000 => LocalizationUtility::translate('any', 'Belog'),
];
}
/**
* Create options for the workspace selector
*
* @return array Key is uid of workspace, value its label
*/
protected function createWorkspaceListForSelectOptions(): array
{
if (!ExtensionManagementUtility::isLoaded('workspaces')) {
return [];
}
$workspaceArray = [];
// Two meta entries: 'all' and 'live'
$workspaceArray[-99] = LocalizationUtility::translate('any', 'Belog');
$workspaceArray[0] = LocalizationUtility::translate('live', 'Belog');
$resultSet = $this->connectionPool->getQueryBuilderForTable('sys_workspace')
->select('uid', 'title')
->from('sys_workspace')
->executeQuery();
while ($row = $resultSet->fetchAssociative()) {
$workspaceArray[$row['uid']] = $row['uid'] . ': ' . $row['title'];
}
return $workspaceArray;
}
/**
* If the user is in a workspace different than LIVE,
* we force to show only log entries from the selected workspace,
* and the workspace selector is not shown.
*/
protected function forceWorkspaceSelectionIfInWorkspace(Constraint $constraint): bool
{
if (!ExtensionManagementUtility::isLoaded('workspaces')) {
return false;
}
if ($this->getBackendUser()->workspace !== 0) {
$constraint->setWorkspaceUid($this->getBackendUser()->workspace);
return false;
}
return true;
}
/**
* Create options for the 'depth of page levels' selector.
*
* @return array Key is depth identifier (1 = One level), value the localized select option label
*/
protected function createPageDepthOptions(): array
{
return [
0 => LocalizationUtility::translate('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_0'),
1 => LocalizationUtility::translate('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_1'),
2 => LocalizationUtility::translate('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_2'),
3 => LocalizationUtility::translate('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_3'),
4 => LocalizationUtility::translate('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_4'),
999 => LocalizationUtility::translate('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_infi'),
];
}
/**
* Calculate the start- and end timestamp
*/
protected function setStartAndEndTimeFromTimeSelector(Constraint $constraint): void
{
$startTime = $constraint->getManualDateStart() ? $constraint->getManualDateStart()->getTimestamp() : 0;
$endTime = $constraint->getManualDateStop() ? $constraint->getManualDateStop()->getTimestamp() : 0;
if ($endTime <= $startTime) {
$endTime = $GLOBALS['EXEC_TIME'];
}
$constraint->setStartTimestamp($startTime);
$constraint->setEndTimestamp($endTime);
}
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
+192
View File
@@ -0,0 +1,192 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Belog\Domain\Model;
use Psr\Log\LogLevel;
/**
* Constraints for log entries
* @internal This class is a TYPO3 Backend implementation and is not considered part of the Public TYPO3 API.
*/
class Constraint
{
/**
* Selected user/group; possible values are "gr-<uid>" for a group, "us-<uid>" for a user or -1 for "all users"
*/
protected string $userOrGroup = '0';
/**
* Number of log rows to show
*/
protected int $number = 20;
/**
* UID of selected workspace
*/
protected int $workspaceUid = -99;
/**
* Selected channel
*/
protected string $channel = '';
/**
* Selected level
*/
protected string $level = LogLevel::DEBUG;
/**
* Calculated start timestamp
*/
protected int $startTimestamp = 0;
/**
* Calculated end timestamp
*/
protected int $endTimestamp = 0;
/**
* Manual date start
*/
protected ?\DateTime $manualDateStart = null;
/**
* Manual date stop
*/
protected ?\DateTime $manualDateStop = null;
/**
* Selected page ID in page context
*/
protected int $pageId = 0;
/**
* Page level depth
*/
protected int $depth = 0;
public function setUserOrGroup(string $user): void
{
$this->userOrGroup = $user;
}
public function getUserOrGroup(): string
{
return $this->userOrGroup;
}
public function setNumber(int $number): void
{
$this->number = $number;
}
public function getNumber(): int
{
return $this->number;
}
public function setWorkspaceUid(int $workspace): void
{
$this->workspaceUid = $workspace;
}
public function getWorkspaceUid(): int
{
return $this->workspaceUid;
}
public function setChannel(string $channel): void
{
$this->channel = $channel;
}
public function getChannel(): string
{
return $this->channel;
}
public function setLevel(string $level): void
{
$this->level = $level;
}
public function getLevel(): string
{
return $this->level;
}
public function setStartTimestamp(int $timestamp): void
{
$this->startTimestamp = $timestamp;
}
public function getStartTimestamp(): int
{
return $this->startTimestamp;
}
public function setEndTimestamp(int $timestamp): void
{
$this->endTimestamp = $timestamp;
}
public function getEndTimestamp(): int
{
return $this->endTimestamp;
}
public function setPageId(?int $id): void
{
$this->pageId = $id ?? 0;
}
public function getPageId(): int
{
return $this->pageId;
}
public function setDepth(int $depth): void
{
$this->depth = $depth;
}
public function getDepth(): int
{
return $this->depth;
}
public function setManualDateStart(?\DateTime $manualDateStart = null): void
{
$this->manualDateStart = $manualDateStart;
}
public function getManualDateStart(): ?\DateTime
{
return $this->manualDateStart;
}
public function setManualDateStop(?\DateTime $manualDateStop = null): void
{
$this->manualDateStop = $manualDateStop;
}
public function getManualDateStop(): ?\DateTime
{
return $this->manualDateStop;
}
}
+248
View File
@@ -0,0 +1,248 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Belog\Domain\Model;
use TYPO3\CMS\Core\Log\LogDataTrait;
/**
* A sys log entry
* This model is 'complete': All current database properties are in there.
*
* @internal This class is a TYPO3 Backend implementation and is not considered part of the Public TYPO3 API.
*/
class LogEntry
{
use LogDataTrait;
/**
* @var int<0, max>
*/
protected int $uid = 0;
/**
* This is not a relation to BeUser model, since the user does
* not always exist, but we want the uid in then anyway.
* This case is ugly in extbase, the best way we
* have found now is to resolve the username (if it exists) in a
* view helper and just use the uid of the be user here.
*/
protected int $backendUserUid = 0;
/**
* Action ID of the action that happened, for example 3 was a file action
*/
protected int $action = 0;
/**
* UID of the record the event happened to
*/
protected int $recordUid = 0;
/**
* Table name
*/
protected string $tableName = '';
/**
* PID of the record the event happened to
*/
protected int $recordPid = 0;
/**
* Error code
*/
protected int $error = 0;
/**
* This is the log message itself, but possibly with %s substitutions.
*/
protected string $details = '';
/**
* Timestamp when the log entry was written
*/
protected \DateTimeInterface $tstamp;
/**
* Type code
*/
protected int $type = 0;
/**
* Channel name.
*/
protected string $channel = '';
/**
* Level.
*/
protected string $level = '';
/**
* IP address of client
*/
protected string $ip = '';
/**
* Serialized log data. This is a serialized array with substitutions for $this->details.
*/
protected string $logData = '';
/**
* Event PID
*/
protected int $eventPid = 0;
/**
* This is only the UID and not the full workspace object for the same reason as in $beUserUid.
*/
protected int $workspaceUid = 0;
public function getUid(): int
{
return $this->uid;
}
public function getBackendUserUid(): int
{
return $this->backendUserUid;
}
public function getAction(): int
{
return $this->action;
}
public function getRecordUid(): int
{
return $this->recordUid;
}
public function getTableName(): string
{
return $this->tableName;
}
public function getRecordPid(): int
{
return $this->recordPid;
}
public function setError(int $error): void
{
$this->error = $error;
}
public function getError(): int
{
return $this->error;
}
public function getErrorIconClass(): string
{
return match ($this->getError()) {
1 => 'status-dialog-warning',
2, 3 => 'status-dialog-error',
default => 'empty-empty',
};
}
public function getDetails(): string
{
if ($this->type === 255) {
return str_replace('###IP###', $this->ip, $this->details);
}
return $this->details;
}
public function getTstamp(): \DateTimeInterface
{
return $this->tstamp;
}
public function getType(): int
{
return $this->type;
}
public function getChannel(): string
{
return $this->channel;
}
public function getLevel(): string
{
return $this->level;
}
public function getIp(): string
{
return $this->ip;
}
public function setLogData(string $logData): void
{
$this->logData = $logData;
}
public function getLogData(): array
{
if ($this->logData === '') {
return [];
}
$logData = $this->unserializeLogData($this->logData);
return $logData ?? [];
}
public function getLogDataRaw(): string
{
return $this->logData;
}
public function getEventPid(): int
{
return $this->eventPid;
}
public function getWorkspaceUid(): int
{
return $this->workspaceUid;
}
public static function createFromDatabaseRecord(array $row): self
{
$obj = new self();
$obj->uid = $row['uid'] ?? $obj->uid;
$obj->tstamp = new \DateTimeImmutable(date('Y-m-d\TH:i:s', $row['tstamp'] ?? 0));
$obj->backendUserUid = $row['userid'] ?? $obj->backendUserUid;
$obj->action = $row['action'] ?? $obj->action;
$obj->recordUid = $row['recuid'] ?? $obj->recordUid;
$obj->tableName = $row['tablename'] ?? $obj->tableName;
$obj->recordPid = $row['recpid'] ?? $obj->recordPid;
$obj->error = $row['error'] ?? $obj->error;
$obj->type = $row['type'] ?? $obj->type;
$obj->details = $row['details'] ?? $obj->details;
$obj->ip = $row['IP'] ?? $obj->ip;
$obj->logData = $row['log_data'] ?? $obj->logData;
$obj->eventPid = $row['event_pid'] ?? $obj->eventPid;
$obj->workspaceUid = $row['workspace'] ?? $obj->workspaceUid;
$obj->channel = $row['channel'] ?? $obj->channel;
$obj->level = $row['level'] ?? $obj->level;
return $obj;
}
}
@@ -0,0 +1,209 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Belog\Domain\Repository;
use Psr\Log\LogLevel;
use TYPO3\CMS\Backend\Tree\Repository\PageTreeRepository;
use TYPO3\CMS\Belog\Domain\Model\Constraint;
use TYPO3\CMS\Belog\Domain\Model\LogEntry;
use TYPO3\CMS\Core\Authentication\GroupResolver;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\Log\LogLevel as Typo3LogLevel;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Sys log entry repository
* @internal This class is a TYPO3 Backend implementation and is not considered part of the Public TYPO3 API.
*/
readonly class LogEntryRepository
{
public function __construct(
private GroupResolver $groupResolver,
private ConnectionPool $connectionPool,
) {}
public function findByUid($uid): ?LogEntry
{
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_log');
$row = $queryBuilder
->select('*')
->from('sys_log')
->where(
$queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT))
)
->fetchAssociative();
return $row ? LogEntry::createFromDatabaseRecord($row) : null;
}
/**
* Finds all log entries that match all given constraints.
*
* @return array<LogEntry>
*/
public function findByConstraint(Constraint $constraint): array
{
$query = $this->connectionPool->getQueryBuilderForTable('sys_log');
$query->select('*')
->from('sys_log')
->orderBy('uid', 'DESC');
$queryConstraints = $this->createQueryConstraints($query, $constraint);
$stmt = $query
->where(...$queryConstraints)
->setMaxResults($constraint->getNumber())
->executeQuery();
$result = [];
while ($row = $stmt->fetchAssociative()) {
$result[] = LogEntry::createFromDatabaseRecord($row);
}
return $result;
}
/**
* Create an array of query constraints from constraint object
*/
protected function createQueryConstraints(QueryBuilder $query, Constraint $constraint): array
{
// User / group handling
$queryConstraints = $this->addUsersAndGroupsToQueryConstraints($constraint, $query);
// Workspace
if ($constraint->getWorkspaceUid() !== -99) {
$queryConstraints[] = $query->expr()->eq('workspace', $query->createNamedParameter($constraint->getWorkspaceUid(), Connection::PARAM_INT));
}
// Channel
if ($channel = $constraint->getChannel()) {
$queryConstraints[] = $query->expr()->eq('channel', $query->createNamedParameter($channel));
}
// Level
if ($level = $constraint->getLevel()) {
$queryConstraints[] = $query->expr()->in('level', $query->createNamedParameter(Typo3LogLevel::atLeast($level), Connection::PARAM_STR_ARRAY));
}
// Start / endtime handling: The timestamp calculation was already done
// in the controller, since we need those calculated values in the view as well.
$queryConstraints[] = $query->expr()->gte('tstamp', $query->createNamedParameter($constraint->getStartTimestamp(), Connection::PARAM_INT));
$queryConstraints[] = $query->expr()->lt('tstamp', $query->createNamedParameter($constraint->getEndTimestamp(), Connection::PARAM_INT));
// Page and level constraint if in page context
$constraint = $this->addPageTreeConstraintsToQuery($constraint, $query);
if ($constraint) {
$queryConstraints[] = $constraint;
}
return $queryConstraints;
}
/**
* Adds constraints for the page(s) to the query; this could be one single page or a whole subtree beneath a given
* page.
*/
protected function addPageTreeConstraintsToQuery(Constraint $constraint, QueryBuilder $query): ?string
{
$pageIds = [];
// Check if we should get a whole tree of pages and not only a single page
if ($constraint->getDepth() > 0) {
$repository = GeneralUtility::makeInstance(PageTreeRepository::class);
$repository->setAdditionalWhereClause($GLOBALS['BE_USER']->getPagePermsClause(Permission::PAGE_SHOW));
$pages = $repository->getFlattenedPages([$constraint->getPageId()], $constraint->getDepth());
foreach ($pages as $page) {
$pageIds[] = (int)$page['uid'];
}
}
if (!empty($constraint->getPageId())) {
$pageIds[] = $constraint->getPageId();
}
if (!empty($pageIds)) {
return $query->expr()->in('event_pid', $query->createNamedParameter($pageIds, Connection::PARAM_INT_ARRAY));
}
return null;
}
/**
* Adds users and groups to the query constraints.
*/
protected function addUsersAndGroupsToQueryConstraints(Constraint $constraint, QueryBuilder $query): array
{
$userOrGroup = $constraint->getUserOrGroup();
if ($userOrGroup === '') {
return [];
}
$queryConstraints = [];
// Constraint for a group
if (str_starts_with($userOrGroup, 'gr-')) {
$groupId = (int)substr($userOrGroup, 3);
$userIds = $this->groupResolver->findAllUsersInGroups([$groupId], 'be_groups', 'be_users');
if (!empty($userIds)) {
$userIds = array_column($userIds, 'uid');
$userIds = array_map(intval(...), $userIds);
$queryConstraints[] = $query->expr()->in('userid', $query->createNamedParameter($userIds, Connection::PARAM_INT_ARRAY));
} else {
// If there are no group members -> use -1 as constraint to not find anything
$queryConstraints[] = $query->expr()->eq('userid', $query->createNamedParameter(-1, Connection::PARAM_INT));
}
} elseif (str_starts_with($userOrGroup, 'us-')) {
$queryConstraints[] = $query->expr()->in('userid', $query->createNamedParameter((int)substr($userOrGroup, 3), Connection::PARAM_INT));
} elseif ($userOrGroup === '-1') {
$queryConstraints[] = $query->expr()->in('userid', $query->createNamedParameter((int)$GLOBALS['BE_USER']->user['uid'], Connection::PARAM_INT));
}
return $queryConstraints;
}
/**
* Deletes all messages which have the same message details
*/
public function deleteByMessageDetails(LogEntry $logEntry): int
{
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_log');
return $queryBuilder->delete('sys_log')
->where($queryBuilder->expr()->eq('details', $queryBuilder->createNamedParameter($logEntry->getDetails())))
->executeStatement();
}
public function getUsedChannels(): array
{
$channels = $this->connectionPool->getQueryBuilderForTable('sys_log')
->select('channel')
->distinct()
->from('sys_log')
->orderBy('channel')
->executeQuery()
->fetchFirstColumn();
return array_combine($channels, $channels);
}
public function getUsedLevels(): array
{
static $allLevels = [
LogLevel::EMERGENCY,
LogLevel::ALERT,
LogLevel::CRITICAL,
LogLevel::ERROR,
LogLevel::WARNING,
LogLevel::NOTICE,
LogLevel::INFO,
LogLevel::DEBUG,
];
$levels = $this->connectionPool->getQueryBuilderForTable('sys_log')
->select('level')
->distinct()
->from('sys_log')
->executeQuery()
->fetchFirstColumn();
$levelsUsed = array_intersect($allLevels, $levels);
return array_combine($levelsUsed, $levelsUsed);
}
}
@@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Belog\EventListener;
use TYPO3\CMS\Backend\Backend\Event\SystemInformationToolbarCollectorEvent;
use TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Toolbar\InformationStatus;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Localization\LanguageService;
/**
* Count latest exceptions for the system information menu.
*
* @internal This class is a TYPO3 Backend implementation and is not considered part of the Public TYPO3 API.
*/
final readonly class SystemInformationEventListener
{
public function __construct(
private ConnectionPool $connectionPool,
private UriBuilder $uriBuilder
) {}
/**
* Modifies the SystemInformation toolbar to inject a new message
* @throws RouteNotFoundException
*/
#[AsEventListener('belog/show-latest-errors')]
public function appendMessage(SystemInformationToolbarCollectorEvent $event): void
{
// we can't use the extbase repository here as the required TypoScript may not be parsed yet
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_log');
$count = $queryBuilder->count('error')
->from('sys_log')
->where(
$queryBuilder->expr()->gte(
'tstamp',
$queryBuilder->createNamedParameter($this->fetchLastAccessTimestamp(), Connection::PARAM_INT)
),
$queryBuilder->expr()->in(
'error',
$queryBuilder->createNamedParameter([-1, 1, 2], Connection::PARAM_INT_ARRAY)
),
$queryBuilder->expr()->eq(
'channel',
$queryBuilder->createNamedParameter('php', Connection::PARAM_STR)
)
)
->executeQuery()
->fetchOne();
if ($count > 0) {
$moduleIdentifier = 'system_log';
$moduleParams = ['constraint' => ['channel' => 'php']];
$text = $this->getLanguageService()->translate(
'systemmessage.errorsInPeriod',
'belog.messages',
[
$count,
(string)$this->uriBuilder->buildUriFromRoute($moduleIdentifier, $moduleParams),
]
);
$systemInformationToolbarItem = $event->getToolbarItem();
$systemInformationToolbarItem->addSystemMessage(
$text,
InformationStatus::ERROR,
$count,
$moduleIdentifier,
http_build_query($moduleParams)
);
}
}
private function fetchLastAccessTimestamp(): int
{
if (!isset($this->getBackendUser()->uc['systeminformation'])) {
return 0;
}
$systemInformationUc = json_decode($this->getBackendUser()->uc['systeminformation'], true, 512, JSON_THROW_ON_ERROR);
return (int)($systemInformationUc['system_log']['lastAccess'] ?? 0);
}
private function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
private function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}
@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Belog\ViewHelpers;
use TYPO3\CMS\Belog\Domain\Model\LogEntry;
use TYPO3\CMS\Core\Log\LogDataTrait;
use TYPO3\CMS\Core\Utility\PathUtility;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* ViewHelper to create detail string from a log entry.
*
* ```
* <belog:formatDetails logEntry="{logItem}" />
* ```
*
* @internal
*/
final class FormatDetailsViewHelper extends AbstractViewHelper
{
use LogDataTrait;
public function initializeArguments(): void
{
$this->registerArgument('logEntry', LogEntry::class, 'Log entry instance to be rendered', true);
}
/**
* Create formatted detail string from log row.
*
* The method handles two properties of the model: details and logData
* Details is a string with possible %s placeholders, and logData an array
* with the substitutions.
* Furthermore, possible files in logData are stripped to their basename if
* the action logged was a file action
*/
public function render(): string
{
/** @var LogEntry $logEntry */
$logEntry = $this->arguments['logEntry'];
$detailString = $logEntry->getDetails();
$substitutes = $logEntry->getLogData();
// Strip paths from file names if the log was a file action
if ($logEntry->getType() === 2) {
$substitutes = self::stripPathFromFilenames($substitutes);
}
return self::formatLogDetailsStatic($detailString, $substitutes);
}
/**
* Strips path from array of file names
*/
private static function stripPathFromFilenames(array $files = []): array
{
foreach ($files as $key => $file) {
$files[$key] = PathUtility::basename((string)$file);
}
return $files;
}
}
@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Belog\ViewHelpers;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* ViewHelper to get a username from a backend user id.
*
* ```
* <belog:username uid="{logItem.backendUserUid}" />
* ```
*
* @internal
*/
final class UsernameViewHelper extends AbstractViewHelper
{
public function __construct(
#[Autowire(service: 'cache.runtime')]
private readonly FrontendInterface $usernameRuntimeCache
) {}
public function initializeArguments(): void
{
$this->registerArgument('uid', 'int', 'Uid of the user', true);
}
/**
* Resolve username from backend user id. Can return empty string if there is no user with that UID.
*/
public function render(): string
{
$uid = $this->arguments['uid'];
$cacheIdentifier = 'belog-viewhelper-username_' . $uid;
if ($this->usernameRuntimeCache->has($cacheIdentifier)) {
return $this->usernameRuntimeCache->get($cacheIdentifier);
}
$username = BackendUtility::getRecord('be_users', $uid)['username'] ?? '';
$this->usernameRuntimeCache->set($cacheIdentifier, $username);
return $username;
}
}
@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Belog\ViewHelpers;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* ViewHelper to get workspace title from a workspace id.
*
* ```
* belog:workspaceTitle uid="{logItem.workspaceUid}" />
* ```
*
* @internal
*/
final class WorkspaceTitleViewHelper extends AbstractViewHelper
{
public function __construct(
#[Autowire(service: 'cache.runtime')]
private readonly FrontendInterface $workspaceTitleRuntimeCache
) {}
public function initializeArguments(): void
{
$this->registerArgument('uid', 'int', 'UID of the workspace', true);
}
/**
* Return resolved workspace title or empty string if it can not be resolved.
*
* @throws \InvalidArgumentException
*/
public function render(): string
{
$uid = $this->arguments['uid'];
$cacheIdentifier = 'belog-viewhelper-workspace-title_' . $uid;
if ($this->workspaceTitleRuntimeCache->has($cacheIdentifier)) {
return $this->workspaceTitleRuntimeCache->get($cacheIdentifier);
}
if ($uid === 0) {
$this->workspaceTitleRuntimeCache->set($cacheIdentifier, htmlspecialchars(self::getLanguageService()->sL(
'LLL:EXT:belog/Resources/Private/Language/locallang.xlf:live'
)));
} elseif (!ExtensionManagementUtility::isLoaded('workspaces')) {
$this->workspaceTitleRuntimeCache->set($cacheIdentifier, '');
} else {
$workspace = BackendUtility::getRecord('sys_workspace', $uid);
$this->workspaceTitleRuntimeCache->set($cacheIdentifier, $workspace['title'] ?? '');
}
return $this->workspaceTitleRuntimeCache->get($cacheIdentifier);
}
private static function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}