TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:09 +02:00
commit af8cc155b5
6818 changed files with 642608 additions and 0 deletions
+32
View File
@@ -0,0 +1,32 @@
<?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\Core\Log;
/**
* Attribute for Logger channel declarations
*/
#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::TARGET_CLASS)]
class Channel
{
public string $name;
public function __construct(string $name)
{
$this->name = $name;
}
}
+21
View File
@@ -0,0 +1,21 @@
<?php
/*
* 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\Core\Log;
/**
* An exception when something is wrong with the file handling
*/
class Exception extends \TYPO3\CMS\Core\Exception {}
@@ -0,0 +1,23 @@
<?php
/*
* 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\Core\Log\Exception;
use TYPO3\CMS\Core\Log\Exception;
/**
* An exception when something is wrong with the configuration for a LogProcessor
*/
class InvalidLogProcessorConfigurationException extends Exception {}
@@ -0,0 +1,23 @@
<?php
/*
* 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\Core\Log\Exception;
use TYPO3\CMS\Core\Log\Exception;
/**
* An exception when something is wrong with the configuration for a LogWriter
*/
class InvalidLogWriterConfigurationException extends Exception {}
+74
View File
@@ -0,0 +1,74 @@
<?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\Core\Log;
/**
* Helper for handling both serialize()/unserialize() and json_encode()/json_decode()
* when migrating to json-encoded strings.
*/
trait LogDataTrait
{
/**
* Useful for handling old serialized data, which might have been migrated to JSON encoded
* properties already.
*/
protected function unserializeLogData(mixed $logData): ?array
{
// The @ symbol avoids an E_NOTICE when unserialize() fails
$cleanedUpData = @unserialize((string)$logData, ['allowed_classes' => false]);
if ($cleanedUpData === false) {
$cleanedUpData = json_decode((string)$logData, true);
}
return is_array($cleanedUpData) ? $cleanedUpData : null;
}
/**
* Replaces a string with placeholders (%s or {myPlaceholder}) with its substitutes.
*/
protected function formatLogDetails(string $detailString, mixed $substitutes): string
{
if (!is_array($substitutes)) {
$substitutes = $this->unserializeLogData($substitutes) ?? [];
}
return self::formatLogDetailsStatic($detailString, $substitutes);
}
/**
* Static version for ViewHelpers etc.
*
* Replaces a string with placeholders (%s or {myPlaceholder}) with its substitutes.
*/
protected static function formatLogDetailsStatic(string $detailString, array $substitutes): string
{
// Handles placeholders with "%" first
try {
$detailString = vsprintf($detailString, $substitutes);
} catch (\ValueError|\ArgumentCountError) {
// Ignore if $substitutes doesn't contain the number of "%" found in $detailString
}
// Handles placeholders with "{myPlaceholder}"
$detailString = preg_replace_callback('/{([A-z]+)}/', static function (array $matches) use ($substitutes) {
// $matches[0] contains the unsubstituted placeholder
return $substitutes[$matches[1]] ?? $matches[0];
}, $detailString);
// Remove possible pending %s
return str_replace('%s', '', (string)$detailString);
}
}
+114
View File
@@ -0,0 +1,114 @@
<?php
/*
* 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\Core\Log;
use Psr\Log\InvalidArgumentException;
/**
* Log levels according to RFC 3164
*/
class LogLevel extends \Psr\Log\LogLevel
{
/**
* Reverse look up of log level to level name.
*
* @var array
*/
protected static $levels = [
self::EMERGENCY,
self::ALERT,
self::CRITICAL,
self::ERROR,
self::WARNING,
self::NOTICE,
self::INFO,
self::DEBUG,
];
/**
* Resolves the name of a log level and returns it in upper case letters
*
* @param int $level Log level.
* @return string Log level name.
*/
public static function getName(int $level): string
{
return strtoupper(static::getInternalName($level));
}
/**
* Resolves the name of the log level and returns its internal string representation
*/
public static function getInternalName(int $level): string
{
self::validateLevel($level);
return static::$levels[$level];
}
/**
* Checks a level for validity,
* whether it is an integer and in the range of 0-7.
*
* @param int $level log level to validate
* @return bool TRUE if the given log level is valid, FALSE otherwise
*/
public static function isValidLevel(int $level): bool
{
return isset(static::$levels[$level]);
}
/**
* Validates a log level.
*
* @param int $level log level to validate
* @throws InvalidArgumentException if the given log level is invalid
*/
public static function validateLevel(int $level): void
{
if (!self::isValidLevel($level)) {
throw new InvalidArgumentException('Invalid Log Level ' . $level, 1321637121);
}
}
/**
* Normalizes level by converting it from string to integer
*
* @param string|int $level
*/
public static function normalizeLevel($level): int
{
if (is_string($level)) {
if (!defined(__CLASS__ . '::' . strtoupper($level))) {
throw new InvalidArgumentException('Invalid Log Level ' . $level, 1550247164);
}
return array_search(strtolower($level), self::$levels, true);
}
return (int)$level;
}
/**
* Returns a list of all log levels at least as severe as the specified level.
*
* @param int|string $level
* @return array<string>
*/
public static function atLeast($level): array
{
$level = self::normalizeLevel($level);
return array_filter(self::$levels, static fn(int $intLevel): bool => $intLevel <= $level, ARRAY_FILTER_USE_KEY);
}
}
+236
View File
@@ -0,0 +1,236 @@
<?php
/*
* 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\Core\Log;
use Psr\Log\InvalidArgumentException;
use Psr\Log\LoggerInterface;
use TYPO3\CMS\Core\Core\RequestId;
use TYPO3\CMS\Core\Log\Exception\InvalidLogProcessorConfigurationException;
use TYPO3\CMS\Core\Log\Exception\InvalidLogWriterConfigurationException;
use TYPO3\CMS\Core\Log\Processor\ProcessorInterface;
use TYPO3\CMS\Core\Log\Processor\RequestIdProcessor;
use TYPO3\CMS\Core\Log\Writer\WriterInterface;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Global LogManager that keeps track of global logging information.
*
* Inspired by java.util.logging
*/
class LogManager implements SingletonInterface, LogManagerInterface
{
/**
* @var string
*/
public const CONFIGURATION_TYPE_WRITER = 'writer';
/**
* @var string
*/
public const CONFIGURATION_TYPE_PROCESSOR = 'processor';
/**
* Loggers to retrieve them for repeated use.
*/
protected array $loggers = [];
/**
* Default / global / root logger.
*/
protected Logger $rootLogger;
/**
* Unique ID of the request
*/
protected RequestId $requestId;
public function __construct(RequestId $requestId = new RequestId())
{
$this->requestId = $requestId;
$this->rootLogger = new Logger('');
$this->loggers[''] = $this->rootLogger;
}
/**
* For use in unit test context only. Resets the internal logger registry.
*/
public function reset()
{
$this->loggers = [];
}
/**
* Gets a logger instance for the given name.
*
* \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Log\LogManager::class)->getLogger('main.sub.subsub');
*
* $name can also be submitted as a underscore-separated string, which will
* be converted to dots. This is useful to call this method with __CLASS__
* as parameter.
*
* @param string $name Logger name, empty to get the global "root" logger.
* @return Logger Logger with name $name
*/
public function getLogger(string $name = ''): LoggerInterface
{
// Transform namespaces and underscore class names to the dot-name style
$separators = ['_', '\\'];
$name = str_replace($separators, '.', $name);
return $this->loggers[$name] ??= $this->makeLogger($name);
}
/**
* Instantiates a new logger object, with the appropriate attached writers and processors.
*/
protected function makeLogger(string $name): Logger
{
$logger = new Logger($name);
$this->setWritersForLogger($logger);
$this->addRequestIdProcessorToLogger($logger);
$this->setProcessorsForLogger($logger);
return $logger;
}
/**
* Attaches the request id processor for all severity levels covered by the
* configured writers of the given logger. The processor is registered before
* any configured processor and on purpose not via $TYPO3_CONF_VARS['LOG'],
* so log records are guaranteed to carry the request id, even if the global
* processor configuration is overridden. Registering it for exactly the
* most verbose writer level avoids raising the minimum log level, which
* would create and process log records no writer ever receives.
*/
protected function addRequestIdProcessorToLogger(Logger $logger): void
{
$writerLevels = array_map(LogLevel::normalizeLevel(...), array_keys($logger->getWriters()));
$minimumLevel = $writerLevels === [] ? LogLevel::normalizeLevel(\Psr\Log\LogLevel::EMERGENCY) : max($writerLevels);
$logger->addProcessor(LogLevel::getInternalName($minimumLevel), new RequestIdProcessor($this->requestId));
}
/**
* For use in unit test context only.
*
* @param string $name
*/
public function registerLogger($name)
{
$this->loggers[$name] = null;
}
/**
* For use in unit test context only.
*
* @return array
*/
public function getLoggerNames()
{
return array_keys($this->loggers);
}
/**
* Appends the writers to the given logger as configured.
*
* @param \TYPO3\CMS\Core\Log\Logger $logger Logger to configure
*/
protected function setWritersForLogger(Logger $logger)
{
$configuration = $this->getConfigurationForLogger(self::CONFIGURATION_TYPE_WRITER, $logger->getName());
foreach ($configuration as $severityLevel => $writer) {
$writer = array_filter($writer, static fn(array $options): bool => !($options['disabled'] ?? false));
foreach ($writer as $logWriterClassName => $logWriterOptions) {
try {
unset($logWriterOptions['disabled']);
/** @var WriterInterface $logWriter */
$logWriter = GeneralUtility::makeInstance($logWriterClassName, $logWriterOptions);
$logger->addWriter($severityLevel, $logWriter);
} catch (InvalidArgumentException|InvalidLogWriterConfigurationException $e) {
$logger->warning('Instantiation of LogWriter "{class_name}" failed for logger {name}', [
'class_name' => $logWriterClassName,
'name' => $logger->getName(),
'exception' => $e,
]);
}
}
}
}
/**
* Appends the processors to the given logger as configured.
*
* @param \TYPO3\CMS\Core\Log\Logger $logger Logger to configure
*/
protected function setProcessorsForLogger(Logger $logger)
{
$configuration = $this->getConfigurationForLogger(self::CONFIGURATION_TYPE_PROCESSOR, $logger->getName());
foreach ($configuration as $severityLevel => $processor) {
foreach ($processor as $logProcessorClassName => $logProcessorOptions) {
try {
/** @var ProcessorInterface $logProcessor */
$logProcessor = GeneralUtility::makeInstance($logProcessorClassName, $logProcessorOptions);
$logger->addProcessor($severityLevel, $logProcessor);
} catch (InvalidArgumentException|InvalidLogProcessorConfigurationException $e) {
$logger->warning('Instantiation of LogProcessor "{class_name}" failed for logger {name}', [
'class_name' => $logProcessorClassName,
'name' => $logger->getName(),
'exception' => $e,
]);
}
}
}
}
/**
* Returns the configuration from $TYPO3_CONF_VARS['LOG'] as
* hierarchical array for different components of the class hierarchy.
*
* @param string $configurationType Type of config to return (writer, processor)
* @param string $loggerName Logger name
* @throws \Psr\Log\InvalidArgumentException
* @return array
*/
protected function getConfigurationForLogger($configurationType, $loggerName)
{
// Split up the logger name (dot-separated) into its parts
$explodedName = explode('.', $loggerName);
// Search in the $TYPO3_CONF_VARS['LOG'] array
// for these keys, for example "writerConfiguration"
$configurationKey = $configurationType . 'Configuration';
$configuration = $GLOBALS['TYPO3_CONF_VARS']['LOG'] ?? [];
$result = $configuration[$configurationKey] ?? [];
// Walk from general to special (t3lib, t3lib.db, t3lib.db.foo)
// and search for the most specific configuration
foreach ($explodedName as $partOfClassName) {
if (!isset($configuration[$partOfClassName])) {
break;
}
if (!empty($configuration[$partOfClassName][$configurationKey])) {
$result = $configuration[$partOfClassName][$configurationKey];
}
$configuration = $configuration[$partOfClassName];
}
// Validate the config
foreach ($result as $level => $unused) {
try {
LogLevel::validateLevel(LogLevel::normalizeLevel($level));
} catch (InvalidArgumentException $e) {
throw new InvalidArgumentException('The given severity level "' . htmlspecialchars($level) . '" for ' . $configurationKey . ' of logger "' . $loggerName . '" is not valid.', 1326406447);
}
}
return $result;
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
/*
* 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\Core\Log;
use Psr\Log\LoggerInterface;
/**
* LogManager Contract for delivering log instances.
*/
interface LogManagerInterface
{
/**
* Gets a logger instance for the given name.
*/
public function getLogger(string $name = ''): LoggerInterface;
}
+259
View File
@@ -0,0 +1,259 @@
<?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\Core\Log;
/**
* Log record
*/
final class LogRecord implements \ArrayAccess
{
/**
* Unique ID of the request
*/
private string $requestId = '';
/**
* Creation timestamp with microseconds
*/
private float $created = 0.0;
/**
* The component where the record was created
*/
private string $component = '';
/**
* Severity level
*/
private string $level = \Psr\Log\LogLevel::INFO;
/**
* Log message one-liner
*/
private string $message = '';
/**
* Additional log data
*/
private array $data = [];
/**
* Gettable properties for ArrayAccess
*/
private array $gettableProperties = [
'requestId',
'created',
'component',
'level',
'message',
'data',
];
/**
* Settable properties for ArrayAccess
*/
private array $settableProperties = [
'level',
'message',
'data',
];
/**
* @param string $component Affected component
* @param string $level Severity level (see \TYPO3\CMS\Core\Log\LogLevel)
* @param string|\Stringable $message Log message
* @param array $data Additional data
* @param string $requestId Unique ID of the request
*/
public function __construct(string $component, string $level, string|\Stringable $message, array $data = [], string $requestId = '')
{
$this->setRequestId($requestId)
->setCreated(microtime(true))
->setComponent($component)
->setLevel($level)
->setMessage($message)
->setData($data);
}
public function setComponent(string $component): self
{
$this->component = $component;
return $this;
}
public function getComponent(): string
{
return $this->component;
}
public function setCreated(float $created): self
{
$this->created = $created;
return $this;
}
public function getCreated(): float
{
return $this->created;
}
/**
* @see \TYPO3\CMS\Core\Log\LogLevel
*/
public function setLevel(string $level): self
{
LogLevel::validateLevel(LogLevel::normalizeLevel($level));
$this->level = $level;
return $this;
}
/**
* @see \TYPO3\CMS\Core\Log\LogLevel
*/
public function getLevel(): string
{
return $this->level;
}
public function setData(array $data): self
{
$this->data = $data;
return $this;
}
public function getData(): array
{
return $this->data;
}
/**
* Adds additional log data to already existing data
* and overwrites previously data using the same array keys.
*/
public function addData(array $data): self
{
$this->data = array_merge($this->data, $data);
return $this;
}
public function setMessage(string|\Stringable $message): self
{
$this->message = (string)$message;
return $this;
}
public function getMessage(): string
{
return $this->message;
}
public function setRequestId(string $requestId): self
{
$this->requestId = $requestId;
return $this;
}
public function getRequestId(): string
{
return $this->requestId;
}
/**
* Convert record to string for simple output, like echo().
* Contents of data array is appended as JSON-encoded string
*/
public function __toString(): string
{
$timestamp = date('r', (int)$this->created);
$levelName = strtoupper($this->level);
$data = '';
if (!empty($this->data)) {
// According to PSR3 the exception-key may hold an \Exception
// Since json_encode() does not encode an exception, we run the _toString() here
if (isset($this->data['exception']) && $this->data['exception'] instanceof \Exception) {
$this->data['exception'] = (string)$this->data['exception'];
}
$data = '- ' . json_encode($this->data);
}
$logRecordString = sprintf(
'%s [%s] request="%s" component="%s": %s %s',
$timestamp,
$levelName,
$this->requestId,
$this->component,
$this->message,
$data
);
return $logRecordString;
}
public function toArray(): array
{
return [
'requestId' => $this->requestId,
'created' => $this->created,
'component' => $this->component,
'level' => $this->level,
'message' => $this->message,
'data' => $this->data,
];
}
/**
* Checks whether an offset exists, required by ArrayAccess interface
*/
public function offsetExists(mixed $offset): bool
{
$offsetExists = false;
if (in_array($offset, $this->gettableProperties, true) && isset($this->{$offset})) {
$offsetExists = true;
}
return $offsetExists;
}
/**
* Offset to retrieve, required by ArrayAccess interface
*/
public function offsetGet(mixed $offset): mixed
{
if (!in_array($offset, $this->gettableProperties, true)) {
return null;
}
return $this->{$offset};
}
/**
* Offset to set, required by ArrayAccess interface
*/
public function offsetSet(mixed $offset, mixed $value): void
{
if (in_array($offset, $this->settableProperties, true)) {
$this->{$offset} = $value;
}
}
/**
* Offset to unset, required by ArrayAccess interface
*/
public function offsetUnset(mixed $offset): void
{
if (in_array($offset, $this->settableProperties, true)) {
unset($this->{$offset});
}
}
}
+229
View File
@@ -0,0 +1,229 @@
<?php
/*
* 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\Core\Log;
use Psr\Log\LoggerInterface;
use Psr\Log\LoggerTrait;
use TYPO3\CMS\Core\Log\Processor\ProcessorInterface;
use TYPO3\CMS\Core\Log\Writer\WriterInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Logger to log events and data for different components.
*/
final class Logger implements LoggerInterface
{
use LoggerTrait;
/**
* Logger name or component for which this logger is meant to be used for.
*
* This should be a dot-separated name and should normally be based on
* the class name or the name of a subsystem, such as
* core.t3lib.cache.manager, core.backend.workspaces or extension.news
*/
protected string $name = '';
/**
* Minimum log level, anything below this level will be ignored.
*/
protected int $minimumLogLevel;
/**
* Writers used by this logger
*/
protected array $writers = [];
/**
* Processors used by this logger
*/
protected array $processors = [];
/**
* Constructor.
*
* @param string $name A name for the logger.
*/
public function __construct(string $name)
{
$this->name = $name;
$this->minimumLogLevel = LogLevel::normalizeLevel(LogLevel::EMERGENCY);
}
/**
* Re-initialize instance with creating a new instance with up to date information
*/
public function __wakeup()
{
$newLogger = GeneralUtility::makeInstance(LogManager::class)->getLogger($this->name);
$this->minimumLogLevel = $newLogger->minimumLogLevel;
$this->writers = $newLogger->writers;
$this->processors = $newLogger->processors;
}
/**
* Remove everything except the name, to be able to restore it on wakeup
*/
public function __sleep(): array
{
return ['name'];
}
/**
* Sets the minimum log level for which log records are written.
*
* @param int $level Minimum log level
* @return \TYPO3\CMS\Core\Log\Logger $this
*/
protected function setMinimumLogLevel($level)
{
LogLevel::validateLevel($level);
$this->minimumLogLevel = $level;
return $this;
}
/**
* Gets the minimum log level for which log records are written.
*
* @return int Minimum log level
*/
protected function getMinimumLogLevel()
{
return $this->minimumLogLevel;
}
/**
* Gets the logger's name.
*
* @return string Logger name.
*/
public function getName()
{
return $this->name;
}
/**
* Adds a writer to this logger
*
* @param \TYPO3\CMS\Core\Log\Writer\WriterInterface $writer Writer object
* @return \TYPO3\CMS\Core\Log\Logger $this
*/
public function addWriter(string $minimumLevel, WriterInterface $writer)
{
$minLevelAsNumber = LogLevel::normalizeLevel($minimumLevel);
// Cycle through all the log levels which are as severe as or higher
// than $minimumLevel and add $writer to each severity level
foreach (LogLevel::atLeast($minLevelAsNumber) as $levelName) {
$this->writers[$levelName] ??= [];
$this->writers[$levelName][] = $writer;
}
if ($minLevelAsNumber > $this->getMinimumLogLevel()) {
$this->setMinimumLogLevel($minLevelAsNumber);
}
return $this;
}
/**
* Returns all configured writers indexed by log level
*
* @return array
*/
public function getWriters()
{
return $this->writers;
}
/**
* Adds a processor to the logger.
*
* @param \TYPO3\CMS\Core\Log\Processor\ProcessorInterface $processor The processor to add.
*/
public function addProcessor(string $minimumLevel, ProcessorInterface $processor)
{
$minLevelAsNumber = LogLevel::normalizeLevel($minimumLevel);
LogLevel::validateLevel($minLevelAsNumber);
// Cycle through all the log levels which are as severe as or higher
// than $minimumLevel and add $processor to each severity level
for ($logLevelWhichTriggersProcessor = LogLevel::normalizeLevel(\Psr\Log\LogLevel::EMERGENCY); $logLevelWhichTriggersProcessor <= $minLevelAsNumber; $logLevelWhichTriggersProcessor++) {
$logLevelName = LogLevel::getInternalName($logLevelWhichTriggersProcessor);
$this->processors[$logLevelName] ??= [];
$this->processors[$logLevelName][] = $processor;
}
if ($minLevelAsNumber > $this->getMinimumLogLevel()) {
$this->setMinimumLogLevel($minLevelAsNumber);
}
}
/**
* Returns all added processors indexed by log level
*
* @return array
*/
public function getProcessors()
{
return $this->processors;
}
/**
* Adds a log record.
*
* @param int|string $level Log level. Value according to \TYPO3\CMS\Core\Log\LogLevel. Alternatively accepts a string.
* @param string|\Stringable $message Log message.
* @param array $data Additional data to log
*/
public function log($level, string|\Stringable $message, array $data = []): void
{
$level = LogLevel::normalizeLevel($level);
LogLevel::validateLevel($level);
if ($level > $this->minimumLogLevel) {
return;
}
$record = new LogRecord($this->name, LogLevel::getInternalName($level), $message, $data);
$record = $this->callProcessors($record);
$this->writeLog($record);
}
/**
* Calls all processors and returns log record
*
* @param \TYPO3\CMS\Core\Log\LogRecord $record Record to process
* @throws \RuntimeException
* @return \TYPO3\CMS\Core\Log\LogRecord Processed log record
*/
protected function callProcessors(LogRecord $record)
{
/** @var ProcessorInterface $processor */
foreach ($this->processors[$record->getLevel()] ?? [] as $processor) {
$processedRecord = $processor->processLogRecord($record);
if (!$processedRecord instanceof LogRecord) {
throw new \RuntimeException('Processor ' . get_class($processor) . ' returned invalid data. Instance of TYPO3\\CMS\\Core\\Log\\LogRecord expected', 1343593398);
}
$record = $processedRecord;
}
return $record;
}
/**
* Passes the \TYPO3\CMS\Core\Log\LogRecord to all registered writers.
*/
protected function writeLog(LogRecord $record)
{
/** @var WriterInterface $writer */
foreach ($this->writers[$record->getLevel()] ?? [] as $writer) {
$writer->writeLog($record);
}
}
}
@@ -0,0 +1,81 @@
<?php
/*
* 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\Core\Log\Processor;
/**
* Common memory processor methods.
*/
abstract class AbstractMemoryProcessor extends AbstractProcessor
{
/**
* Allocated memory usage type to use
* If set, the real size of memory allocated from system is used.
* Otherwise the memory used by emalloc() is used.
*
* @var bool
* @see memory_get_usage()
* @see memory_get_peak_usage()
*/
protected $realMemoryUsage = true;
/**
* Whether the size is formatted, e.g. in megabytes
*
* @var bool
* @see \TYPO3\CMS\Core\Utility\GeneralUtility::formatSize()
*/
protected $formatSize = true;
/**
* Sets the allocated memory usage type
*
* @param bool $realMemoryUsage Which allocated memory type to use
*/
public function setRealMemoryUsage($realMemoryUsage)
{
$this->realMemoryUsage = (bool)$realMemoryUsage;
}
/**
* Returns the allocated memory usage type
*
* @return bool
*/
public function getRealMemoryUsage()
{
return $this->realMemoryUsage;
}
/**
* Sets whether size should be formatted
*
* @param bool $formatSize
*/
public function setFormatSize($formatSize)
{
$this->formatSize = (bool)$formatSize;
}
/**
* Returns whether size should be formatted
*
* @return bool
*/
public function getFormatSize()
{
return $this->formatSize;
}
}
@@ -0,0 +1,42 @@
<?php
/*
* 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\Core\Log\Processor;
use TYPO3\CMS\Core\Log\Exception\InvalidLogProcessorConfigurationException;
/**
* Abstract implementation of a log processor
*/
abstract class AbstractProcessor implements ProcessorInterface
{
/**
* Constructs this log processor
*
* @param array $options Configuration options - depends on the actual processor
* @throws \TYPO3\CMS\Core\Log\Exception\InvalidLogProcessorConfigurationException
*/
public function __construct(array $options = [])
{
foreach ($options as $optionKey => $optionValue) {
$methodName = 'set' . ucfirst($optionKey);
if (method_exists($this, $methodName)) {
$this->{$methodName}($optionValue);
} else {
throw new InvalidLogProcessorConfigurationException('Invalid LogProcessor configuration option "' . $optionKey . '" for log processor of type "' . static::class . '"', 1321696151);
}
}
}
}
@@ -0,0 +1,161 @@
<?php
/*
* 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\Core\Log\Processor;
use TYPO3\CMS\Core\Log\LogRecord;
/**
* Introspection processor to automatically add where the log record came from.
*/
class IntrospectionProcessor extends AbstractProcessor
{
/**
* Add the full backtrace to the log entry or
* just the last entry of the backtrace
*
* @var bool
*/
protected $appendFullBackTrace = false;
/**
* Number of entries to shift from the backtrace
*
* @var int
*/
protected $shiftBackTraceLevel = 0;
/**
* Temporary storage of the preceding backtrace line number
*
* @var string
*/
private $precedingBacktraceLine = '';
/**
* Temporary storage of the preceding backtrace file
*
* @var string
*/
private $precedingBacktraceFile = '';
/**
* Set the number of levels to be shift from the backtrace
*
* @param int $shiftBackTraceLevel Numbers of levels to shift
* @return IntrospectionProcessor
*/
public function setShiftBackTraceLevel($shiftBackTraceLevel)
{
$this->shiftBackTraceLevel = (int)$shiftBackTraceLevel;
return $this;
}
/**
* Set if the full backtrace should be added to the log or just the last item
*
* @param bool $appendFullBackTrace If the full backtrace should be added
* @return IntrospectionProcessor
*/
public function setAppendFullBackTrace($appendFullBackTrace)
{
$this->appendFullBackTrace = (bool)$appendFullBackTrace;
return $this;
}
/**
* Add debug backtrace information to logRecord
* It adds: filepath, line number, class and function name
*
* @param LogRecord $logRecord The log record to process
* @return LogRecord The processed log record with additional data
* @see debug_backtrace()
*/
public function processLogRecord(LogRecord $logRecord)
{
$trace = $this->getDebugBacktrace();
// skip TYPO3\CMS\Core\Log classes
foreach ($trace as $traceEntry) {
if (isset($traceEntry['class']) && str_contains($traceEntry['class'], 'TYPO3\\CMS\\Core\\Log')) {
$trace = $this->shiftBacktraceLevel($trace);
} else {
break;
}
}
// shift a given number of entries from the trace
for ($i = 0; $i < $this->shiftBackTraceLevel; $i++) {
// shift only if afterwards there is at least one entry left after.
if (count($trace) > 1) {
$trace = $this->shiftBacktraceLevel($trace);
}
}
if ($this->appendFullBackTrace) {
// Add the line and file of the last entry that has these information
// to the first backtrace entry if it does not have this information.
// This is required in case we have shifted entries and the first entry
// is now a call_user_func that does not contain the line and file information.
if (!isset($trace[0]['line'])) {
$trace[0] = ['line' => $this->precedingBacktraceLine] + $trace[0];
}
if (!isset($trace[0]['file'])) {
$trace[0] = ['file' => $this->precedingBacktraceFile] + $trace[0];
}
$logRecord->addData([
'backtrace' => $trace,
]);
} else {
$logRecord->addData([
'file' => $trace[0]['file'] ?? null,
'line' => $trace[0]['line'] ?? null,
'class' => $trace[0]['class'] ?? null,
'function' => $trace[0]['function'] ?? null,
]);
}
return $logRecord;
}
/**
* Shift the first item from the backtrace
*
* @return array
*/
protected function shiftBacktraceLevel(array $backtrace)
{
if (isset($backtrace[0]['file'])) {
$this->precedingBacktraceFile = $backtrace[0]['file'];
}
if (isset($backtrace[0]['line'])) {
$this->precedingBacktraceLine = $backtrace[0]['line'];
}
array_shift($backtrace);
return $backtrace;
}
/**
* Get the debug backtrace
*
* @return array
*/
protected function getDebugBacktrace()
{
return debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
}
}
@@ -0,0 +1,46 @@
<?php
/*
* 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\Core\Log\Processor;
use TYPO3\CMS\Core\Log\LogRecord;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Memory peak usage processor methods.
*/
class MemoryPeakUsageProcessor extends AbstractMemoryProcessor
{
/**
* Processes a log record and adds memory peak usage information.
*
* @param \TYPO3\CMS\Core\Log\LogRecord $logRecord The log record to process
* @return \TYPO3\CMS\Core\Log\LogRecord The processed log record with additional data
* @see memory_get_peak_usage()
*/
public function processLogRecord(LogRecord $logRecord)
{
$bytes = memory_get_peak_usage($this->getRealMemoryUsage());
if ($this->formatSize) {
$size = GeneralUtility::formatSize($bytes);
} else {
$size = $bytes;
}
$logRecord->addData([
'memoryPeakUsage' => $size,
]);
return $logRecord;
}
}
@@ -0,0 +1,46 @@
<?php
/*
* 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\Core\Log\Processor;
use TYPO3\CMS\Core\Log\LogRecord;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Memory usage processor methods.
*/
class MemoryUsageProcessor extends AbstractMemoryProcessor
{
/**
* Processes a log record and adds memory usage information.
*
* @param \TYPO3\CMS\Core\Log\LogRecord $logRecord The log record to process
* @return \TYPO3\CMS\Core\Log\LogRecord The processed log record with additional data
* @see memory_get_usage()
*/
public function processLogRecord(LogRecord $logRecord)
{
$bytes = memory_get_usage($this->getRealMemoryUsage());
if ($this->formatSize) {
$size = GeneralUtility::formatSize($bytes);
} else {
$size = $bytes;
}
$logRecord->addData([
'memoryUsage' => $size,
]);
return $logRecord;
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
/*
* 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\Core\Log\Processor;
use TYPO3\CMS\Core\Log\LogRecord;
/**
* A log processor that does nothing. Used in unit tests.
*/
class NullProcessor extends AbstractProcessor
{
/**
* Processes a log record and returns the same.
*
* @param \TYPO3\CMS\Core\Log\LogRecord $logRecord The log record to process
* @return \TYPO3\CMS\Core\Log\LogRecord The processed log record with additional data
*/
public function processLogRecord(LogRecord $logRecord)
{
return $logRecord;
}
}
@@ -0,0 +1,35 @@
<?php
/*
* 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\Core\Log\Processor;
use TYPO3\CMS\Core\Log\LogRecord;
/**
* Log processor interface
*
* Processors provide additional data in an automatic way, without having to
* collect that data yourself.
*/
interface ProcessorInterface
{
/**
* Processes a log record and adds additional data.
*
* @param \TYPO3\CMS\Core\Log\LogRecord $logRecord The log record to process
* @return \TYPO3\CMS\Core\Log\LogRecord The processed log record with additional data
*/
public function processLogRecord(LogRecord $logRecord);
}
@@ -0,0 +1,35 @@
<?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\Core\Log\Processor;
use TYPO3\CMS\Core\Core\RequestId;
use TYPO3\CMS\Core\Log\LogRecord;
/**
* Adds the unique ID of the current request to log records, in order
* to correlate all log entries written during a single request.
*/
final class RequestIdProcessor implements ProcessorInterface
{
public function __construct(private readonly RequestId $requestId) {}
public function processLogRecord(LogRecord $logRecord): LogRecord
{
return $logRecord->setRequestId((string)$this->requestId);
}
}
+60
View File
@@ -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\Core\Log\Processor;
use TYPO3\CMS\Core\Http\NormalizedParams;
use TYPO3\CMS\Core\Log\LogRecord;
/**
* Web log processor to automatically add web request related data to a log
* record.
*/
class WebProcessor extends AbstractProcessor
{
public function processLogRecord(LogRecord $logRecord): LogRecord
{
$normalizedParams = ($GLOBALS['TYPO3_REQUEST'] ?? null)?->getAttribute('normalizedParams');
if ($normalizedParams instanceof NormalizedParams) {
$logRecord->addData([
'HTTP_HOST' => $normalizedParams->getHttpHost(),
'TYPO3_HOST_ONLY' => $normalizedParams->getRequestHostOnly(),
'TYPO3_PORT' => $normalizedParams->getRequestPort(),
'PATH_INFO' => $normalizedParams->getPathInfo(),
'QUERY_STRING' => $normalizedParams->getQueryString(),
'REQUEST_URI' => $normalizedParams->getRequestUri(),
'HTTP_REFERER' => $normalizedParams->getHttpReferer(),
'TYPO3_REQUEST_HOST' => $normalizedParams->getRequestHost(),
'TYPO3_REQUEST_URL' => $normalizedParams->getRequestUrl(),
'TYPO3_REQUEST_SCRIPT' => $normalizedParams->getRequestScript(),
'TYPO3_REQUEST_DIR' => $normalizedParams->getRequestDir(),
'TYPO3_SITE_URL' => $normalizedParams->getSiteUrl(),
'TYPO3_SITE_SCRIPT' => $normalizedParams->getSiteScript(),
'TYPO3_SSL' => $normalizedParams->isHttps(),
'TYPO3_REV_PROXY' => $normalizedParams->isBehindReverseProxy(),
'SCRIPT_NAME' => $normalizedParams->getScriptName(),
'TYPO3_DOCUMENT_ROOT' => $normalizedParams->getDocumentRoot(),
'SCRIPT_FILENAME' => $normalizedParams->getScriptFilename(),
'REMOTE_ADDR' => $normalizedParams->getRemoteAddress(),
'REMOTE_HOST' => $normalizedParams->getRemoteHost(),
'HTTP_USER_AGENT' => $normalizedParams->getHttpUserAgent(),
'HTTP_ACCEPT_LANGUAGE' => $normalizedParams->getHttpAcceptLanguage(),
]);
}
return $logRecord;
}
}
+97
View File
@@ -0,0 +1,97 @@
<?php
/*
* 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\Core\Log\Writer;
use TYPO3\CMS\Core\Log\Exception\InvalidLogWriterConfigurationException;
use TYPO3\CMS\Core\Security\BlockSerializationTrait;
/**
* Abstract implementation of a log writer
*/
abstract class AbstractWriter implements WriterInterface
{
use BlockSerializationTrait;
/**
* Constructs this log writer
*
* @param array $options Configuration options - depends on the actual log writer
* @throws \TYPO3\CMS\Core\Log\Exception\InvalidLogWriterConfigurationException
*/
public function __construct(array $options = [])
{
foreach ($options as $optionKey => $optionValue) {
$methodName = 'set' . ucfirst($optionKey);
if (method_exists($this, $methodName)) {
$this->{$methodName}($optionValue);
} else {
throw new InvalidLogWriterConfigurationException('Invalid LogWriter configuration option "' . $optionKey . '" for log writer of type "' . static::class . '"', 1321696152);
}
}
}
/**
* Interpolates context values into the message placeholders.
*/
protected function interpolate(string $message, array $context = []): string
{
// Build a replacement array with braces around the context keys.
$replace = [];
foreach ($context as $key => $val) {
if (!is_array($val) && !is_null($val) && (!is_object($val) || method_exists($val, '__toString'))) {
$replace['{' . $key . '}'] = $this->formatContextValue($val);
}
}
// Interpolate replacement values into the message and return.
return strtr($message, $replace);
}
/**
* Escape or quote a value from the context appropriate for the output.
*
* Note: In some output cases, escaping should not be done here but later on output,
* such as if it's being written to a database for later display.
*
* @param string $value
*/
protected function formatContextValue(string $value): string
{
return $value;
}
/**
* Formats an exception into a string.
*
* The format here is nearly the same as just casting an exception to a string,
* but omits the full class namespace and stack trace, as those get very long.
*/
protected function formatException(\Throwable $ex): string
{
$classname = get_class($ex);
if ($pos = strrpos($classname, '\\')) {
$classname = substr($classname, $pos + 1);
}
return sprintf(
'- %s: %s, in file %s:%s',
$classname,
$ex->getMessage(),
$ex->getFile(),
$ex->getLine(),
);
}
}
+75
View File
@@ -0,0 +1,75 @@
<?php
/*
* 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\Core\Log\Writer;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Log\LogLevel;
use TYPO3\CMS\Core\Log\LogRecord;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Log writer that writes the log records into a database table.
*/
class DatabaseWriter extends AbstractWriter
{
/**
* Writes the log record
*
* @param LogRecord $record Log record
* @return \TYPO3\CMS\Core\Log\Writer\WriterInterface $this
*/
public function writeLog(LogRecord $record)
{
try {
// Avoid ConnectionPool usage prior boot completion (see #96291).
if (!GeneralUtility::getContainer()->get('boot.state')->complete) {
return $this;
}
} catch (\LogicException $e) {
// LogicException will be thrown if the container isn't available yet.
return $this;
}
$data = '';
$context = $record->getData();
if (!empty($context)) {
// Fold an exception into the message, and string-ify it into context so it can be jsonified.
if (isset($context['exception']) && $context['exception'] instanceof \Throwable) {
$context['exception'] = (string)$context['exception'];
}
$data = json_encode($context);
}
$fieldValues = [
'request_id' => $record->getRequestId(),
'time_micro' => $record->getCreated(),
'component' => $record->getComponent(),
'level' => LogLevel::normalizeLevel($record->getLevel()),
'message' => $record->getMessage(),
'data' => $data,
];
// sys_log uses tstamp for garbage collection via TableGarbageCollectionTask.
// Without it, tstamp defaults to 0 (1970-01-01), causing immediate deletion (see #109290).
$fieldValues['tstamp'] = (int)$record->getCreated();
GeneralUtility::makeInstance(ConnectionPool::class)
->getConnectionForTable('sys_log')
->insert('sys_log', $fieldValues);
return $this;
}
}
+36
View File
@@ -0,0 +1,36 @@
<?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\Core\Log\Writer\Enum;
enum Interval: string
{
case DAILY = 'daily';
case WEEKLY = 'weekly';
case MONTHLY = 'monthly';
case YEARLY = 'yearly';
public function getDateInterval(): string
{
return match ($this) {
self::DAILY => 'P1D',
self::WEEKLY => 'P1W',
self::MONTHLY => 'P1M',
self::YEARLY => 'P1Y',
};
}
}
+276
View File
@@ -0,0 +1,276 @@
<?php
/*
* 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\Core\Log\Writer;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Crypto\HashService;
use TYPO3\CMS\Core\Log\Exception\InvalidLogWriterConfigurationException;
use TYPO3\CMS\Core\Log\LogRecord;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
/**
* Log writer that writes the log records into a file.
*/
class FileWriter extends AbstractWriter
{
/**
* Log file path, relative to TYPO3's base project folder
*/
protected string $logFile = '';
protected string $logFileInfix = '';
/**
* Default log file path
*/
protected string $defaultLogFileTemplate = '/log/typo3_%s.log';
/**
* Log file handle storage
*
* To avoid concurrent file handles on a the same file when using several FileWriter instances,
* we share the file handles in a static class variable
*
* @static
*/
protected static array $logFileHandles = [];
/**
* Keep track of used file handles by different fileWriter instances
*
* As the logger gets instantiated by class name but the resources
* are shared via the static $logFileHandles we need to track usage
* of file handles to avoid closing handles that are still needed
* by different instances. Only if the count is zero may the file
* handle be closed.
*/
protected static array $logFileHandlesCount = [];
/**
* Constructor, opens the log file handle
*/
public function __construct(array $options = [])
{
// the parent constructor reads $options and sets them
parent::__construct($options);
if (empty($options['logFile'])
// omit logging if TYPO3 has not been configured (avoid creating a guessable filename)
&& ($GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] ?? '') !== ''
) {
$this->setLogFile($this->getDefaultLogFileName());
}
}
/**
* Destructor, closes the log file handle
*/
public function __destruct()
{
if ($this->logFile === '') {
return;
}
self::$logFileHandlesCount[$this->logFile]--;
if (self::$logFileHandlesCount[$this->logFile] <= 0) {
$this->closeLogFile();
}
}
public function setLogFileInfix(string $infix)
{
$this->logFileInfix = $infix;
}
/**
* Sets the path to the log file.
*
* @param string $relativeLogFile path to the log file, relative to public web dir
* @return WriterInterface
* @throws InvalidLogWriterConfigurationException
*/
public function setLogFile(string $relativeLogFile)
{
$logFile = $relativeLogFile;
if (!PathUtility::isAbsolutePath($logFile)) {
$logFile = GeneralUtility::getFileAbsFileName($logFile);
if (empty($logFile)) {
throw new InvalidLogWriterConfigurationException(
'Log file path "' . $relativeLogFile . '" is not valid!',
1444374805
);
}
}
$this->logFile = $logFile;
$this->openLogFile();
return $this;
}
/**
* Gets the path to the log file.
*/
public function getLogFile(): string
{
return $this->logFile;
}
/**
* Writes the log record
*
* @param LogRecord $record Log record
* @return WriterInterface $this
* @throws \RuntimeException
*/
public function writeLog(LogRecord $record)
{
if ($this->logFile === '') {
return $this;
}
$data = '';
$context = $record->getData();
$message = $record->getMessage();
if (!empty($context)) {
// Fold an exception into the message, and string-ify it into context so it can be jsonified.
if (isset($context['exception']) && $context['exception'] instanceof \Throwable) {
$message .= $this->formatException($context['exception']);
$context['exception'] = (string)$context['exception'];
}
$data = '- ' . json_encode($context, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
$message = sprintf(
'%s [%s] request="%s" component="%s": %s %s',
date('r', (int)$record->getCreated()),
strtoupper($record->getLevel()),
$record->getRequestId(),
$record->getComponent(),
$this->interpolate($message, $context),
$data
);
if (fwrite(self::$logFileHandles[$this->logFile], $message . LF) === false) {
throw new \RuntimeException('Could not write log record to log file', 1345036335);
}
return $this;
}
/**
* Opens the log file handle
*
* @throws \RuntimeException if the log file can't be opened.
*/
protected function openLogFile()
{
if (isset(self::$logFileHandlesCount[$this->logFile])) {
self::$logFileHandlesCount[$this->logFile]++;
} else {
self::$logFileHandlesCount[$this->logFile] = 1;
}
if (isset(self::$logFileHandles[$this->logFile]) && is_resource(self::$logFileHandles[$this->logFile] ?? false)) {
return;
}
$this->createLogFile();
self::$logFileHandles[$this->logFile] = fopen($this->logFile, 'a');
if (!is_resource(self::$logFileHandles[$this->logFile])) {
throw new \RuntimeException('Could not open log file "' . $this->logFile . '"', 1321804422);
}
}
/**
* Closes the log file handle.
*/
protected function closeLogFile()
{
if (!empty(self::$logFileHandles[$this->logFile]) && is_resource(self::$logFileHandles[$this->logFile])) {
fclose(self::$logFileHandles[$this->logFile]);
unset(self::$logFileHandles[$this->logFile]);
}
}
/**
* Creates the log file with correct permissions
* and parent directories, if needed
*/
protected function createLogFile()
{
if (file_exists($this->logFile)) {
return;
}
// skip mkdir if logFile refers to any scheme but file:// or empty
$scheme = parse_url($this->logFile, PHP_URL_SCHEME);
if ($scheme === null || $scheme === 'file' || PathUtility::isAbsolutePath($this->logFile)) {
// remove file:/ before creating the directory
$logFileDirectory = PathUtility::dirname((string)preg_replace('#^file:/#', '', $this->logFile));
if (!@is_dir($logFileDirectory)) {
GeneralUtility::mkdir_deep($logFileDirectory);
// create .htaccess file if log file is within the site path
if (PathUtility::getCommonPrefix([Environment::getPublicPath() . '/', $logFileDirectory]) === (Environment::getPublicPath() . '/')) {
// only create .htaccess, if we created the directory on our own
$this->createHtaccessFile($logFileDirectory . '/.htaccess');
}
}
}
// create the log file
GeneralUtility::writeFile($this->logFile, '', true);
}
/**
* Creates .htaccess file inside a new directory to access protect it
*
* @param string $htaccessFile Path of .htaccess file
*/
protected function createHtaccessFile($htaccessFile)
{
// write .htaccess file to protect the log file
if (!empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['generateApacheHtaccess']) && !file_exists($htaccessFile)) {
$htaccessContent = <<<END
# Apache < 2.3
<IfModule !mod_authz_core.c>
Order allow,deny
Deny from all
Satisfy All
</IfModule>
# Apache ≥ 2.3
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
END;
GeneralUtility::writeFile($htaccessFile, $htaccessContent, true);
}
}
/**
* Returns the path to the default log file.
* Uses the defaultLogFileTemplate and replaces the %s placeholder with a short MD5 hash
* based on a static string and the current encryption key.
*
* @return string
*/
protected function getDefaultLogFileName()
{
$hashService = GeneralUtility::makeInstance(HashService::class);
$namePart = substr($hashService->hmac($this->defaultLogFileTemplate, 'defaultLogFile'), 0, 10);
if ($this->logFileInfix !== '') {
$namePart = $this->logFileInfix . '_' . $namePart;
}
return Environment::getVarPath() . sprintf($this->defaultLogFileTemplate, $namePart);
}
}
+36
View File
@@ -0,0 +1,36 @@
<?php
/*
* 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\Core\Log\Writer;
use TYPO3\CMS\Core\Log\LogRecord;
/**
* Null writer - just forgets about everything
*/
class NullWriter extends AbstractWriter
{
/**
* Writes the log record
*
* @param LogRecord $record Log record
* @return \TYPO3\CMS\Core\Log\Writer\WriterInterface $this
*/
public function writeLog(LogRecord $record)
{
// do nothing
return $this;
}
}
+59
View File
@@ -0,0 +1,59 @@
<?php
/*
* 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\Core\Log\Writer;
use TYPO3\CMS\Core\Log\LogRecord;
/**
* Log writer that writes the log records into PHP error log.
*/
class PhpErrorLogWriter extends AbstractWriter
{
/**
* Writes the log record
*
* @param LogRecord $record Log record
* @return \TYPO3\CMS\Core\Log\Writer\WriterInterface $this
* @throws \RuntimeException
*/
public function writeLog(LogRecord $record)
{
$data = '';
$context = $record->getData();
$message = $record->getMessage();
if (!empty($context)) {
// Fold an exception into the message, and string-ify it into context so it can be jsonified.
if (isset($context['exception']) && $context['exception'] instanceof \Throwable) {
$message .= $this->formatException($context['exception']);
$context['exception'] = (string)$context['exception'];
}
$data = '- ' . json_encode($context);
}
$message = sprintf(
'TYPO3 [%s] request="%s" component="%s": %s %s',
strtoupper($record->getLevel()),
$record->getRequestId(),
$record->getComponent(),
$this->interpolate($message, $context),
$data
);
if (error_log($message) === false) {
throw new \RuntimeException('Could not write log record to PHP error log', 1345036336);
}
return $this;
}
}
+170
View File
@@ -0,0 +1,170 @@
<?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\Core\Log\Writer;
use TYPO3\CMS\Core\Locking\Exception\LockAcquireException;
use TYPO3\CMS\Core\Locking\Exception\LockAcquireWouldBlockException;
use TYPO3\CMS\Core\Locking\Exception\LockCreateException;
use TYPO3\CMS\Core\Locking\LockFactory;
use TYPO3\CMS\Core\Log\LogRecord;
use TYPO3\CMS\Core\Log\Writer\Enum\Interval;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Write logs into files while providing basic rotation capabilities. This is a very basic approach, suitable for
* environments where established tools like logrotate are not available.
*/
class RotatingFileWriter extends FileWriter
{
private const string ROTATION_DATE_FORMAT = 'YmdHis';
private Interval $interval = Interval::DAILY;
private int $maxFiles = 5;
private \DateTimeImmutable $lastRotation;
private \DateTimeImmutable $nextRotation;
public function __construct(array $options = [])
{
parent::__construct($options);
}
public function setLogFile(string $relativeLogFile): self
{
parent::setLogFile($relativeLogFile);
$this->updateRuntimeRotationState($this->getLastRotation());
return $this;
}
/**
* Internal setter called by FileWriter constructor
*/
protected function setInterval(string|Interval $interval): void
{
if (is_string($interval)) {
// String support is required for use in system/settings.php
$this->interval = Interval::tryFrom($interval) ?? Interval::DAILY;
} else {
$this->interval = $interval;
}
}
/**
* Internal setter called by FileWriter constructor
*/
protected function setMaxFiles(int $maxFiles): void
{
$this->maxFiles = max(0, $maxFiles);
}
public function writeLog(LogRecord $record)
{
if ($this->needsRotation()) {
$lockFactory = GeneralUtility::makeInstance(LockFactory::class);
try {
$lock = $lockFactory->createLocker('rotate-' . $this->logFile);
if ($lock->acquire()) {
$this->updateRuntimeRotationState($this->getLastRotation());
// check again if rotation is still needed (could have happened in the meantime)
if ($this->needsRotation()) {
$this->rotate();
}
}
$lock->release();
} catch (LockCreateException|LockAcquireException|LockAcquireWouldBlockException) {
}
}
return parent::writeLog($record);
}
/**
* This method rotates all log files found by using `glob()` to take all already rotated logs into account, even
* after a configuration change.
*
* Log files are rotated using the "copytruncate" approach: the current open log file is copied as-is to a new
* location, the current log file gets flushed afterward. This way, file handles don't need to get re-created.
*/
protected function rotate(): void
{
$rotationSuffix = date(self::ROTATION_DATE_FORMAT);
// copytruncate: Rotate the currently used log file
copy($this->logFile, $this->logFile . '.' . $rotationSuffix);
ftruncate(self::$logFileHandles[$this->logFile], 0);
$rotatedLogFiles = glob($this->logFile . '.*');
rsort($rotatedLogFiles, SORT_NATURAL);
// Remove any excess files
if ($this->maxFiles > 0) {
$excessFiles = array_slice($rotatedLogFiles, $this->maxFiles);
foreach ($excessFiles as $excessFile) {
unlink($excessFile);
}
}
$this->updateRuntimeRotationState(new \DateTimeImmutable());
}
protected function getLastRotation(): \DateTimeImmutable
{
// Rotate already rotated files again
$rotatedLogFiles = glob($this->logFile . '.*');
if ($rotatedLogFiles !== []) {
// Sort rotated files to handle the newest one first
rsort($rotatedLogFiles, SORT_NATURAL);
$newestLog = current($rotatedLogFiles);
$rotationDelimiterPosition = strrpos($newestLog, '.');
$timestamp = substr($newestLog, $rotationDelimiterPosition + 1);
$latestRotationDateTime = \DateTimeImmutable::createFromFormat(self::ROTATION_DATE_FORMAT, $timestamp);
if ($latestRotationDateTime instanceof \DateTimeImmutable) {
return $latestRotationDateTime;
}
}
return new \DateTimeImmutable('@0');
}
protected function determineNextRotation(): \DateTimeImmutable
{
return $this->lastRotation->add(new \DateInterval($this->interval->getDateInterval()));
}
/**
* Check if log files need to be rotated under following conditions:
*
* 1.
* a) either the next rotation is due
* b) logs were never rotated before
* 2. the log file is not empty - FileWriter::setLogFile() creates one if missing
*/
protected function needsRotation(): bool
{
return ($this->nextRotation <= new \DateTimeImmutable() || $this->lastRotation->getTimestamp() === 0) && filesize($this->logFile) > 0;
}
protected function updateRuntimeRotationState(\DateTimeImmutable $lastRotation): void
{
$this->lastRotation = $lastRotation;
$this->nextRotation = $this->determineNextRotation();
}
}
+133
View File
@@ -0,0 +1,133 @@
<?php
/*
* 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\Core\Log\Writer;
use TYPO3\CMS\Core\Log\LogLevel;
use TYPO3\CMS\Core\Log\LogRecord;
/**
* Log writer that writes to syslog
*/
class SyslogWriter extends AbstractWriter
{
/**
* List of valid syslog facility names.
* private as it's not supposed to be changed.
*
* @var array<string, int> Facilities
*/
private $facilities = [
'auth' => LOG_AUTH,
'authpriv' => LOG_AUTHPRIV,
'cron' => LOG_CRON,
'daemon' => LOG_DAEMON,
'kern' => LOG_KERN,
'lpr' => LOG_LPR,
'mail' => LOG_MAIL,
'news' => LOG_NEWS,
'syslog' => LOG_SYSLOG,
'user' => LOG_USER,
'uucp' => LOG_UUCP,
];
/**
* Type of program that is logging to syslog.
*
* @var int
*/
protected $facility = LOG_USER;
/**
* Constructor, adds facilities on *nix environments.
*
* @param array $options Configuration options
* @throws \RuntimeException if connection to syslog cannot be opened
* @see \TYPO3\CMS\Core\Log\Writer\AbstractWriter
*/
public function __construct(array $options = [])
{
// additional facilities for *nix environments
if (!defined('PHP_WINDOWS_VERSION_BUILD')) {
$this->facilities['local0'] = LOG_LOCAL0;
$this->facilities['local1'] = LOG_LOCAL1;
$this->facilities['local2'] = LOG_LOCAL2;
$this->facilities['local3'] = LOG_LOCAL3;
$this->facilities['local4'] = LOG_LOCAL4;
$this->facilities['local5'] = LOG_LOCAL5;
$this->facilities['local6'] = LOG_LOCAL6;
$this->facilities['local7'] = LOG_LOCAL7;
}
parent::__construct($options);
openlog('TYPO3', LOG_ODELAY | LOG_PID, $this->facility);
}
/**
* Destructor, closes connection to syslog.
*/
public function __destruct()
{
closelog();
}
/**
* Sets the facility to use when logging to syslog.
*
* @param string $facility Facility to use when logging.
*/
public function setFacility($facility): void
{
if (array_key_exists(strtolower($facility), $this->facilities)) {
$this->facility = $this->facilities[strtolower($facility)];
}
}
/**
* Returns the data of the record in syslog format
*/
public function getMessageForSyslog(LogRecord $record): string
{
$data = '';
$context = $record->getData();
$message = $record->getMessage();
if (!empty($context)) {
// Fold an exception into the message, and string-ify it into context so it can be jsonified.
if (isset($context['exception']) && $context['exception'] instanceof \Throwable) {
$message .= $this->formatException($context['exception']);
$context['exception'] = (string)$context['exception'];
}
$data = '- ' . json_encode($context);
}
return sprintf(
'[request="%s" component="%s"] %s %s',
$record->getRequestId(),
$record->getComponent(),
$this->interpolate($message, $context),
$data
);
}
/**
* Writes the log record to syslog
*
* @return \TYPO3\CMS\Core\Log\Writer\WriterInterface
*/
public function writeLog(LogRecord $record)
{
syslog(LogLevel::normalizeLevel($record->getLevel()), $this->getMessageForSyslog($record));
return $this;
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
/*
* 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\Core\Log\Writer;
use TYPO3\CMS\Core\Log\LogRecord;
/**
* Log writer interface
*/
interface WriterInterface
{
/**
* Writes the log record
*
* @param \TYPO3\CMS\Core\Log\LogRecord $record Log record
* @return \TYPO3\CMS\Core\Log\Writer\WriterInterface $this
* @throws \Exception
*/
public function writeLog(LogRecord $record);
}