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']; } }