TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:38 +02:00
commit 4392dbe2ce
142 changed files with 11824 additions and 0 deletions
@@ -0,0 +1,280 @@
<?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\Scheduler\Hooks;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Scheduler\CronCommand\NormalizeCommand;
use TYPO3\CMS\Scheduler\Domain\Repository\SchedulerTaskRepository;
use TYPO3\CMS\Scheduler\Exception\InvalidDateException;
use TYPO3\CMS\Scheduler\Exception\InvalidTaskException;
use TYPO3\CMS\Scheduler\Execution;
use TYPO3\CMS\Scheduler\Service\TaskService;
use TYPO3\CMS\Scheduler\Task\AbstractTask;
/**
* DataHandler hook to validate incoming task parameters and execution_details
* when creating or updating a task.
*
* @internal This is an internal hook implementation and is not considered part of the Public TYPO3 API.
*/
#[Autoconfigure(public: true)]
final readonly class SchedulerTaskPersistenceValidator
{
private FlashMessageQueue $flashMessageQueue;
public function __construct(
private TaskService $taskService,
private SchedulerTaskRepository $taskRepository,
FlashMessageService $flashMessageService,
) {
$this->flashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
}
/**
* If the task is not valid, this hook will create an error log message AND make the incomingFieldArray
* a non-array (e.g. false) to skip saving this record.
*/
public function processDatamap_preProcessFieldArray(&$incomingFieldArray, $table, $id, DataHandler $dataHandler): void
{
if ($table !== 'tx_scheduler_task') {
return;
}
$isNewTask = false;
if (MathUtility::canBeInterpretedAsInteger($id)) {
// Only execution is updated, do not validate anything else
if (count($incomingFieldArray) === 3 && isset($incomingFieldArray['nextexecution'], $incomingFieldArray['disable'], $incomingFieldArray['execution_details'])) {
return;
}
// Update process
$fullRecord = BackendUtility::getRecord($table, $id);
$changedTaskType = ($incomingFieldArray['tasktype'] ?? false) !== ($fullRecord['tasktype'] ?? false);
if (!isset($incomingFieldArray['tasktype'])) {
$taskType = $fullRecord['tasktype'];
} else {
$taskType = $incomingFieldArray['tasktype'];
}
if (!empty($fullRecord['serialized_executions'])) {
// If there's a registered execution, the task should not be edited. May happen if a cron started the task meanwhile.
$this->addErrorMessage($dataHandler, $id, 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.maynotEditRunningTask');
}
$task = $this->taskRepository->findByUid((int)$id);
} else {
$isNewTask = true;
$changedTaskType = true;
$taskType = $incomingFieldArray['tasktype'];
try {
$task = $this->taskService->createNewTask($taskType);
} catch (InvalidTaskException $e) {
// Task can not be further processed since task type is not valid
$dataHandler->log('tx_scheduler_task', $id, 1, null, SystemLogErrorClassification::WARNING, 'Task can not be further processed since task type ' . $taskType . ' is not valid');
$incomingFieldArray = false;
return;
}
}
$decodedAndExtractedFieldArray = $this->decodeValues($incomingFieldArray);
if (!$this->isSubmittedTaskDataValid($dataHandler, $id, $decodedAndExtractedFieldArray, $task)) {
// Custom AdditionalFieldProvider may have added error messages via the FlashMessageQueue (as recommended)
// which is needed to render them properly in FormEngine via $dataHandler->printLogErrorMessages();
$this->convertErrorMessagesToDataHandlerLog($dataHandler, $id);
// Setting this to a "non-array" will skip further persistence chain
$incomingFieldArray = false;
return;
}
// Now let's transform our data
$this->setTaskDataFromRequest($task, $decodedAndExtractedFieldArray);
$incomingFieldArray = array_replace_recursive($incomingFieldArray, $this->taskService->getFieldsForRecord($task));
if ($isNewTask) {
$incomingFieldArray['parameters'] = $incomingFieldArray['parameters'] ?? [];
$incomingFieldArray['pid'] = 0;
} elseif ($changedTaskType) {
$incomingFieldArray['parameters'] = [];
$incomingFieldArray['tasktype'] = $taskType;
}
}
private function convertErrorMessagesToDataHandlerLog(DataHandler $dataHandler, string|int $taskId): void
{
$messages = $this->flashMessageQueue->getAllMessagesAndFlush();
foreach ($messages as $message) {
$messageError = match ($message->getSeverity()) {
ContextualFeedbackSeverity::WARNING => SystemLogErrorClassification::WARNING,
ContextualFeedbackSeverity::OK => SystemLogErrorClassification::MESSAGE,
default => SystemLogErrorClassification::USER_ERROR,
};
$dataHandler->log(
'tx_scheduler_task',
$taskId,
MathUtility::canBeInterpretedAsInteger($taskId) ? 2 : 1,
null,
$messageError,
$message->getMessage()
);
}
}
private function addErrorMessage(DataHandler $dataHandler, string|int $taskId, string $message, ...$args): void
{
$languageService = $this->getLanguageService();
$message = $languageService->sL($message);
$dataHandler->log(
'tx_scheduler_task',
$taskId,
MathUtility::canBeInterpretedAsInteger($taskId) ? 2 : 1,
null,
SystemLogErrorClassification::USER_ERROR,
$message,
null,
$args,
0
);
}
private function isSubmittedTaskDataValid(DataHandler $dataHandler, string|int $taskId, array $parsedBody, AbstractTask $task): bool
{
$startTime = $parsedBody['start'] ?? 0;
$endTime = $parsedBody['end'] ?? 0;
$frequency = $parsedBody['frequency'] ?? $parsedBody['cronCmd'] ?? '';
$runningType = (int)($parsedBody['runningType'] ?? ($frequency ? AbstractTask::TYPE_RECURRING : AbstractTask::TYPE_SINGLE));
$result = true;
if ($runningType !== AbstractTask::TYPE_SINGLE && $runningType !== AbstractTask::TYPE_RECURRING) {
$result = false;
$this->addErrorMessage($dataHandler, $taskId, 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.invalidRunningType');
}
if (empty($startTime)) {
$result = false;
$this->addErrorMessage($dataHandler, $taskId, 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.noStartDate');
} else {
try {
$startTime = $this->getTimestampFromDateString($startTime);
} catch (InvalidDateException) {
$result = false;
$this->addErrorMessage($dataHandler, $taskId, 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.invalidStartDate');
}
}
if ($runningType === AbstractTask::TYPE_RECURRING && !empty($endTime)) {
try {
$endTime = $this->getTimestampFromDateString($endTime);
} catch (InvalidDateException) {
$result = false;
$this->addErrorMessage($dataHandler, $taskId, 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.invalidStartDate');
}
}
if ($runningType === AbstractTask::TYPE_RECURRING && $endTime > 0 && $endTime < $startTime) {
$result = false;
$this->addErrorMessage($dataHandler, $taskId, 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.endDateSmallerThanStartDate');
}
if ($runningType === AbstractTask::TYPE_RECURRING) {
if (empty(trim($frequency))) {
$result = false;
$this->addErrorMessage($dataHandler, $taskId, 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.noFrequency');
} elseif (!is_numeric(trim($frequency))) {
try {
NormalizeCommand::normalize(trim($frequency));
} catch (\InvalidArgumentException $e) {
$result = false;
$this->addErrorMessage($dataHandler, $taskId, 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.frequencyError', $e->getMessage(), $e->getCode());
}
}
}
return $result && (!method_exists($task, 'validateTaskParameters') || $task->validateTaskParameters($parsedBody));
}
/**
* Convert input to DateTime and retrieve timestamp.
*
* @throws InvalidDateException
*/
private function getTimestampFromDateString(int|string $input): int
{
if ($input === '' || $input === 0) {
return 0;
}
if (MathUtility::canBeInterpretedAsInteger($input)) {
// Already looks like a timestamp
return (int)$input;
}
try {
// Convert from ISO 8601 dates
$value = (new \DateTime($input))->getTimestamp();
} catch (\Exception $e) {
throw new InvalidDateException($e->getMessage(), 1747813335);
}
return $value;
}
private function decodeValues(array $fieldArray): array
{
foreach (['execution_details', 'parameters'] as $possibleEncodedValueKey) {
$value = $fieldArray[$possibleEncodedValueKey] ?? [];
if (is_string($value) && $value !== '') {
try {
$value = json_decode($value, true, 512, JSON_THROW_ON_ERROR);
$fieldArray[$possibleEncodedValueKey] = $value;
} catch (\JsonException) {
// Skip failed decoding
}
}
if (is_array($value) && $value !== []) {
// Extract "values" so additional field providers can directly
// access the values without going via the json field.
$fieldArray = array_merge($value, $fieldArray);
}
}
return $fieldArray;
}
private function setTaskDataFromRequest(AbstractTask $task, array $incomingData): void
{
$endTime = $incomingData['end'] ?? '';
$frequency = $incomingData['frequency'] ?? $incomingData['cronCmd'] ?? '';
$runningType = (int)($incomingData['runningType'] ?? ($frequency ? AbstractTask::TYPE_RECURRING : AbstractTask::TYPE_SINGLE));
if ($runningType === AbstractTask::TYPE_SINGLE) {
$execution = Execution::createSingleExecution($this->getTimestampFromDateString($incomingData['start']));
} else {
$execution = Execution::createRecurringExecution(
$this->getTimestampFromDateString($incomingData['start']),
is_numeric($frequency) ? (int)$frequency : 0,
!empty($endTime) ? $this->getTimestampFromDateString($endTime) : 0,
(bool)($incomingData['multiple'] ?? false),
!is_numeric($frequency) ? $frequency : '',
);
}
$task->setExecution($execution);
$task->setDisabled($incomingData['disable'] ?? false);
$task->setDescription($incomingData['description'] ?? '');
if (str_starts_with((string)($incomingData['task_group'] ?? ''), 'tx_scheduler_task_group_')) {
$incomingData['task_group'] = (int)substr($incomingData['task_group'], 24);
}
$task->setTaskGroup((int)($incomingData['task_group'] ?? 0));
}
private function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}