281 lines
12 KiB
PHP
281 lines
12 KiB
PHP
<?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'];
|
|
}
|
|
}
|