TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -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'];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user